ruleset: support non-contiguous address masks
authorJo-Philipp Wich <jo@mein.io>
Wed, 26 Jan 2022 11:05:39 +0000 (12:05 +0100)
committerJo-Philipp Wich <jo@mein.io>
Thu, 27 Jan 2022 15:22:15 +0000 (16:22 +0100)
Support non-contiguous address masks (such as `::1234/::ffff`) for zone
subnet and rule src_ip / dest_ip options and translate them into appropriate
bitwise & expressions internally.

Add appropriate logic to calculate permutations of inverted, non-inverted,
contiguous and non-contiguous address matches since bitwise calculation
expressions can not appear within sets which means that any non-inverted,
non-contiguous mask addresses must be put into separate rules while the
remaining addresses (if any) may be grouped into a common set.

Signed-off-by: Jo-Philipp Wich <jo@mein.io>
root/usr/share/firewall4/templates/redirect.uc
root/usr/share/firewall4/templates/rule.uc
root/usr/share/firewall4/templates/ruleset.uc
root/usr/share/firewall4/templates/zone-masq.uc [new file with mode: 0644]
root/usr/share/firewall4/templates/zone-match.uc
root/usr/share/ucode/fw4.uc
tests/02_zones/05_subnet_mask_matches [new file with mode: 0644]
tests/03_rules/06_subnet_mask_matches [new file with mode: 0644]

index 592af0f7949b4ca0ab2586cb936d80212cc8d543..a77e3d59747d2edf8db62bd6018d03ce5e31cde8 100644 (file)
        {{ fw4.ipproto(redirect.family) }} saddr {{ fw4.set(redirect.saddrs_pos) }} {%+ endif -%}
 {%+ if (redirect.saddrs_neg): -%}
        {{ fw4.ipproto(redirect.family) }} saddr != {{ fw4.set(redirect.saddrs_neg) }} {%+ endif -%}
+{%+ for (let a in redirect.saddrs_masked): -%}
+       {{ fw4.ipproto(redirect.family) }} saddr & {{ a.mask }} {{ a.invert ? '!=' : '==' }} {{ a.addr }} {%+ endfor -%}
 {%+ if (redirect.daddrs_pos): -%}
        {{ fw4.ipproto(redirect.family) }} daddr {{ fw4.set(redirect.daddrs_pos) }} {%+ endif -%}
 {%+ if (redirect.daddrs_neg): -%}
        {{ fw4.ipproto(redirect.family) }} daddr != {{ fw4.set(redirect.daddrs_neg) }} {%+ endif -%}
+{%+ for (let a in redirect.daddrs_masked): -%}
+       {{ fw4.ipproto(redirect.family) }} daddr & {{ a.mask }} {{ a.invert ? '!=' : '==' }} {{ a.addr }} {%+ endfor -%}
 {%+ if (redirect.sports_pos): -%}
        {{ redirect.proto.name }} sport {{ fw4.set(redirect.sports_pos) }} {%+ endif -%}
 {%+ if (redirect.sports_neg): -%}
index c8bab595a6ee9973bae5577601553acb89a8e444..518b9ccdae77db68be363bce1bc6dfa9f3faf768 100644 (file)
@@ -8,10 +8,14 @@
        {{ fw4.ipproto(rule.family) }} saddr {{ fw4.set(rule.saddrs_pos) }} {%+ endif -%}
 {%+ if (rule.saddrs_neg): -%}
        {{ fw4.ipproto(rule.family) }} saddr != {{ fw4.set(rule.saddrs_neg) }} {%+ endif -%}
+{%+ for (let a in rule.saddrs_masked): -%}
+       {{ fw4.ipproto(rule.family) }} saddr & {{ a.mask }} {{ a.invert ? '!=' : '==' }} {{ a.addr }} {%+ endfor -%}
 {%+ if (rule.daddrs_pos): -%}
        {{ fw4.ipproto(rule.family) }} daddr {{ fw4.set(rule.daddrs_pos) }} {%+ endif -%}
 {%+ if (rule.daddrs_neg): -%}
        {{ fw4.ipproto(rule.family) }} daddr != {{ fw4.set(rule.daddrs_neg) }} {%+ endif -%}
+{%+ for (let a in rule.daddrs_masked): -%}
+       {{ fw4.ipproto(rule.family) }} daddr & {{ a.mask }} {{ a.invert ? '!=' : '==' }} {{ a.addr }} {%+ endfor -%}
 {%+ if (rule.sports_pos): -%}
        {{ rule.proto.name }} sport {{ fw4.set(rule.sports_pos) }} {%+ endif -%}
 {%+ if (rule.sports_neg): -%}
index 2a0a8a8d6cbbc5555019a20139a7169669a3577e..75f0679f4852448e42a191a99e07add101fe7293 100644 (file)
@@ -230,38 +230,30 @@ table inet fw4 {
 {% for (let zone in fw4.zones()): %}
 {%  if (zone.dflags.dnat): %}
        chain dstnat_{{ zone.name }} {
-{% for (let redirect in fw4.redirects("dstnat_"+zone.name)): %}
+{%   for (let redirect in fw4.redirects("dstnat_"+zone.name)): %}
                {%+ include("redirect.uc", { fw4, redirect }) %}
-{% endfor %}
+{%   endfor %}
        }
 
 {%  endif %}
 {%  if (zone.dflags.snat): %}
        chain srcnat_{{ zone.name }} {
-{% for (let redirect in fw4.redirects("srcnat_"+zone.name)): %}
+{%   for (let redirect in fw4.redirects("srcnat_"+zone.name)): %}
                {%+ include("redirect.uc", { fw4, redirect }) %}
-{% endfor %}
+{%   endfor %}
 {%   if (zone.masq): %}
-               meta nfproto ipv4 {%+ if (zone.masq4_src_pos): -%}
-                       ip saddr {{ fw4.set(zone.masq4_src_pos) }} {%+ endif -%}
-               {%+ if (zone.masq4_src_neg): -%}
-                       ip saddr != {{ fw4.set(zone.masq4_src_neg) }} {%+ endif -%}
-               {%+ if (zone.masq4_dest_pos): -%}
-                       ip daddr {{ fw4.set(zone.masq4_dest_pos) }} {%+ endif -%}
-               {%+ if (zone.masq4_dest_neg): -%}
-                       ip daddr != {{ fw4.set(zone.masq4_dest_neg) }} {%+ endif -%}
-               masquerade comment "!fw4: Masquerade IPv4 {{ zone.name }} traffic"
+{%    for (let saddrs in zone.masq4_src_subnets): %}
+{%     for (let daddrs in zone.masq4_dest_subnets): %}
+               {%+ include("zone-masq.uc", { fw4, zone, family: 4, saddrs, daddrs }) %}
+{%     endfor %}
+{%    endfor %}
 {%   endif %}
 {%   if (zone.masq6): %}
-               meta nfproto ipv6 {%+ if (zone.masq6_src_pos): -%}
-                       ip6 saddr {{ fw4.set(zone.masq6_src_pos) }} {%+ endif -%}
-               {%+ if (zone.masq6_src_neg): -%}
-                       ip6 saddr != {{ fw4.set(zone.masq6_src_neg) }} {%+ endif -%}
-               {%+ if (zone.masq6_dest_pos): -%}
-                       ip6 daddr {{ fw4.set(zone.masq6_dest_pos) }} {%+ endif -%}
-               {%+ if (zone.masq6_dest_neg): -%}
-                       ip6 daddr != {{ fw4.set(zone.masq6_dest_neg) }} {%+ endif -%}
-               masquerade comment "!fw4: Masquerade IPv6 {{ zone.name }} traffic"
+{%    for (let saddrs in zone.masq6_src_subnets): %}
+{%     for (let daddrs in zone.masq6_dest_subnets): %}
+               {%+ include("zone-masq.uc", { fw4, zone, family: 6, saddrs, daddrs }) %}
+{%     endfor %}
+{%    endfor %}
 {%   endif %}
        }
 
diff --git a/root/usr/share/firewall4/templates/zone-masq.uc b/root/usr/share/firewall4/templates/zone-masq.uc
new file mode 100644 (file)
index 0000000..c3e66d8
--- /dev/null
@@ -0,0 +1,13 @@
+meta nfproto {{ fw4.nfproto(family) }} {%+ if (saddrs && saddrs[0]): -%}
+       {{ fw4.ipproto(family) }} saddr {{ fw4.set(map(saddrs[0], fw4.cidr)) }} {%+ endif -%}
+{%+ if (saddrs && saddrs[1]): -%}
+       {{ fw4.ipproto(family) }} saddr != {{ fw4.set(map(saddrs[1], fw4.cidr)) }} {%+ endif -%}
+{%+ for (let a in (saddrs ? saddrs[2] : [])): -%}
+       {{ fw4.ipproto(family) }} saddr & {{ a.mask }} {{ a.invert ? '!=' : '==' }} {{ a.addr }} {%+ endfor -%}
+{%+ if (daddrs && daddrs[0]): -%}
+       {{ fw4.ipproto(family) }} daddr {{ fw4.set(map(daddrs[0], fw4.cidr)) }} {%+ endif -%}
+{%+ if (daddrs && daddrs[1]): -%}
+       {{ fw4.ipproto(family) }} daddr != {{ fw4.set(map(daddrs[1], fw4.cidr)) }} {%+ endif -%}
+{%+ for (let a in (daddrs ? daddrs[2] : [])): -%}
+       {{ fw4.ipproto(family) }} daddr & {{ a.mask }} {{ a.invert ? '!=' : '==' }} {{ a.addr }} {%+ endfor -%}
+masquerade comment "!fw4: Masquerade {{ fw4.nfproto(family, true) }} {{ zone.name }} traffic"
index f33644702eecdb4bce6eacc2d789a48b8a516fd1..b069b0828c2d761e30f00f1a5435f66d2d6f74db 100644 (file)
@@ -8,3 +8,5 @@
        {{ fw4.ipproto(rule.family) }} {{ egress ? "daddr" : "saddr" }} {{ fw4.set(rule.subnets_pos) }} {%+ endif -%}
 {%+ if (rule.subnets_neg): -%}
        {{ fw4.ipproto(rule.family) }} {{ egress ? "daddr" : "saddr" }} != {{ fw4.set(rule.subnets_neg) }} {%+ endif -%}
+{%+ for (let subnet in rule.subnets_masked): -%}
+       {{ fw4.ipproto(rule.family) }} {{ egress ? "daddr" : "saddr" }} & {{ subnet.mask }} {{ subnet.invert ? '!=' : '==' }} {{ subnet.addr }} {%+ endfor -%}
index c39bffcd8c2b9e44242c2811d7ebfb17b3775743..66cc8f67f539d1993e028fc36591ea77faf3413f 100644 (file)
@@ -242,6 +242,41 @@ function subnets_split_af(x) {
        return rv;
 }
 
+function subnets_group_by_masking(x) {
+       let groups = [], plain = [], nc = [], invert_plain = [], invert_masked = [];
+
+       for (let a in to_array(x)) {
+               if (a.bits == -1 && !a.invert)
+                       push(nc, a);
+               else if (!a.invert)
+                       push(plain, a);
+               else if (a.bits == -1)
+                       push(invert_masked, a);
+               else
+                       push(invert_plain, a);
+       }
+
+       for (let a in nc)
+               push(groups, [ null, null_if_empty(invert_plain), [ a, ...invert_masked ] ]);
+
+       if (length(plain)) {
+               push(groups, [
+                       plain,
+                       null_if_empty(invert_plain),
+                       null_if_empty(invert_masked)
+               ]);
+       }
+       else if (!length(groups)) {
+               push(groups, [
+                       null,
+                       null_if_empty(invert_plain),
+                       null_if_empty(invert_masked)
+               ]);
+       }
+
+       return groups;
+}
+
 function ensure_tcpudp(x) {
        if (length(filter(x, p => (p.name == "tcp" || p.name == "udp"))))
                return true;
@@ -664,8 +699,13 @@ return {
 
                                b = to_bits(parts[1]);
 
-                               if (b == null)
-                                       return null;
+                               /* allow non-contiguous masks such as `::ffff:ffff:ffff:ffff` */
+                               if (b == null) {
+                                       b = -1;
+
+                                       for (let i, x in m)
+                                               a[i] &= x;
+                               }
 
                                m = arrtoip(m);
                        }
@@ -1402,7 +1442,10 @@ return {
                    (a.family == 6 && a.bits == 128))
                    return a.addr;
 
-               return sprintf("%s/%d", apply_mask(a.addr, a.bits), a.bits);
+               if (a.bits >= 0)
+                       return sprintf("%s/%d", apply_mask(a.addr, a.bits), a.bits);
+
+               return sprintf("%s/%s", a.addr, a.mask);
        },
 
        host: function(a) {
@@ -1765,8 +1808,9 @@ return {
                        r.devices_neg = null_if_empty(devices[1]);
                        r.devices_neg_wildcard = null_if_empty(devices[2]);
 
-                       r.subnets_pos = map(filter_pos(subnets), this.cidr);
-                       r.subnets_neg = map(filter_neg(subnets), this.cidr);
+                       r.subnets_pos = map(subnets[0], this.cidr);
+                       r.subnets_neg = map(subnets[1], this.cidr);
+                       r.subnets_masked = subnets[2];
 
                        push(match_rules, r);
                };
@@ -1831,41 +1875,29 @@ return {
                        for (let devgroup in devices) {
                                // check if there's no AF specific bits, in this case we can do AF agnostic matching
                                if (!family && !length(match_subnets[0]) && !length(match_subnets[1])) {
-                                       add_rule(0, devgroup, null, zone);
+                                       add_rule(0, devgroup, [], zone);
                                }
 
                                // we need to emit one or two AF specific rules
                                else {
                                        if (family_is_ipv4(zone) && length(match_subnets[0]))
-                                               add_rule(4, devgroup, match_subnets[0], zone);
+                                               for (let subnets in subnets_group_by_masking(match_subnets[0]))
+                                                       add_rule(4, devgroup, subnets, zone);
 
                                        if (family_is_ipv6(zone) && length(match_subnets[1]))
-                                               add_rule(6, devgroup, match_subnets[1], zone);
+                                               for (let subnets in subnets_group_by_masking(match_subnets[1]))
+                                                       add_rule(6, devgroup, subnets, zone);
                                }
                        }
                }
 
                zone.match_rules = match_rules;
 
-               if (masq_src_subnets[0]) {
-                       zone.masq4_src_pos = map(filter_pos(masq_src_subnets[0]), this.cidr);
-                       zone.masq4_src_neg = map(filter_neg(masq_src_subnets[0]), this.cidr);
-               }
-
-               if (masq_src_subnets[1]) {
-                       zone.masq6_src_pos = map(filter_pos(masq_src_subnets[1]), this.cidr);
-                       zone.masq6_src_neg = map(filter_neg(masq_src_subnets[1]), this.cidr);
-               }
-
-               if (masq_dest_subnets[0]) {
-                       zone.masq4_dest_pos = map(filter_pos(masq_dest_subnets[0]), this.cidr);
-                       zone.masq4_dest_neg = map(filter_neg(masq_dest_subnets[0]), this.cidr);
-               }
+               zone.masq4_src_subnets = subnets_group_by_masking(masq_src_subnets[0]);
+               zone.masq4_dest_subnets = subnets_group_by_masking(masq_dest_subnets[0]);
 
-               if (masq_dest_subnets[1]) {
-                       zone.masq6_dest_pos = map(filter_pos(masq_dest_subnets[1]), this.cidr);
-                       zone.masq6_dest_neg = map(filter_neg(masq_dest_subnets[1]), this.cidr);
-               }
+               zone.masq6_src_subnets = subnets_group_by_masking(masq_src_subnets[1]);
+               zone.masq6_dest_subnets = subnets_group_by_masking(masq_dest_subnets[1]);
 
                zone.sflags = {};
                zone.sflags[zone.input] = true;
@@ -1875,7 +1907,7 @@ return {
                zone.dflags[zone.forward] = true;
 
                zone.match_devices = map(filter(match_devices, d => !d.invert), d => d.device);
-               zone.match_subnets = map(filter(related_subnets, s => !s.invert), this.cidr);
+               zone.match_subnets = map(filter(related_subnets, s => !s.invert && s.bits != -1), this.cidr);
 
                zone.related_subnets = related_subnets;
 
@@ -2093,12 +2125,14 @@ return {
 
                                family: family,
                                proto: proto,
-                               has_addrs: !!(length(saddrs) || length(daddrs)),
+                               has_addrs: !!(saddrs[0] || saddrs[1] || saddrs[2] || daddrs[0] || daddrs[1] || daddrs[2]),
                                has_ports: !!(length(sports) || length(dports)),
-                               saddrs_pos: map(filter_pos(saddrs), this.cidr),
-                               saddrs_neg: map(filter_neg(saddrs), this.cidr),
-                               daddrs_pos: map(filter_pos(daddrs), this.cidr),
-                               daddrs_neg: map(filter_neg(daddrs), this.cidr),
+                               saddrs_pos: map(saddrs[0], this.cidr),
+                               saddrs_neg: map(saddrs[1], this.cidr),
+                               saddrs_masked: saddrs[2],
+                               daddrs_pos: map(daddrs[0], this.cidr),
+                               daddrs_neg: map(daddrs[1], this.cidr),
+                               daddrs_masked: daddrs[2],
                                sports_pos: map(filter_pos(sports), this.port),
                                sports_neg: map(filter_neg(sports), this.port),
                                dports_pos: map(filter_pos(dports), this.port),
@@ -2246,7 +2280,7 @@ return {
 
                        /* check if there's no AF specific bits, in this case we can do an AF agnostic rule */
                        if (!family && rule.target != "dscp" && !has_ipv4_specifics && !has_ipv6_specifics) {
-                               add_rule(0, proto, null, null, sports, dports, null, null, null, rule);
+                               add_rule(0, proto, [], [], sports, dports, null, null, null, rule);
                        }
 
                        /* we need to emit one or two AF specific rules */
@@ -2255,22 +2289,30 @@ return {
                                        let icmp_types = filter(itypes4, i => (i.code_min == 0 && i.code_max == 0xFF));
                                        let icmp_codes = filter(itypes4, i => (i.code_min != 0 || i.code_max != 0xFF));
 
-                                       if (length(icmp_types) || (!length(icmp_types) && !length(icmp_codes)))
-                                               add_rule(4, proto, sip[0], dip[0], sports, dports, icmp_types, null, ipset, rule);
+                                       for (let saddrs in subnets_group_by_masking(sip[0])) {
+                                               for (let daddrs in subnets_group_by_masking(dip[0])) {
+                                                       if (length(icmp_types) || (!length(icmp_types) && !length(icmp_codes)))
+                                                               add_rule(4, proto, saddrs, daddrs, sports, dports, icmp_types, null, ipset, rule);
 
-                                       if (length(icmp_codes))
-                                               add_rule(4, proto, sip[0], dip[0], sports, dports, null, icmp_codes, ipset, rule);
+                                                       if (length(icmp_codes))
+                                                               add_rule(4, proto, saddrs, daddrs, sports, dports, null, icmp_codes, ipset, rule);
+                                               }
+                                       }
                                }
 
                                if (family == 0 || family == 6) {
                                        let icmp_types = filter(itypes6, i => (i.code_min == 0 && i.code_max == 0xFF));
                                        let icmp_codes = filter(itypes6, i => (i.code_min != 0 || i.code_max != 0xFF));
 
-                                       if (length(icmp_types) || (!length(icmp_types) && !length(icmp_codes)))
-                                               add_rule(6, proto, sip[1], dip[1], sports, dports, icmp_types, null, ipset, rule);
+                                       for (let saddrs in subnets_group_by_masking(sip[1])) {
+                                               for (let daddrs in subnets_group_by_masking(dip[1])) {
+                                                       if (length(icmp_types) || (!length(icmp_types) && !length(icmp_codes)))
+                                                               add_rule(6, proto, saddrs, daddrs, sports, dports, icmp_types, null, ipset, rule);
 
-                                       if (length(icmp_codes))
-                                               add_rule(6, proto, sip[1], dip[1], sports, dports, null, icmp_codes, ipset, rule);
+                                                       if (length(icmp_codes))
+                                                               add_rule(6, proto, saddrs, daddrs, sports, dports, null, icmp_codes, ipset, rule);
+                                               }
+                                       }
                                }
                        }
                }
@@ -2388,6 +2430,8 @@ return {
                                return this.warn_section(r, "must not have source '*' for dnat target");
                        else if (redir.dest_ip && redir.dest_ip.invert)
                                return this.warn_section(r, "must not specify a negated 'dest_ip' value");
+                       else if (redir.dest_ip && length(filter(redir.dest_ip.addrs, a => a.bits == -1)))
+                               return this.warn_section(data, "must not use non-contiguous masks in 'dest_ip'");
 
                        if (!redir.dest && redir.dest_ip && resolve_dest(redir))
                                this.warn_section(r, "does not specify a destination, assuming '" + redir.dest.zone.name + "'");
@@ -2415,6 +2459,8 @@ return {
                                return this.warn_section(data, "has no 'src_dip' option specified");
                        else if (redir.src_dip.invert)
                                return this.warn_section(data, "must not specify a negated 'src_dip' value");
+                       else if (length(filter(redir.src_dip.addrs, a => a.bits == -1)))
+                               return this.warn_section(data, "must not use non-contiguous masks in 'src_dip'");
                        else if (redir.src_mac)
                                return this.warn_section(data, "must not use 'src_mac' option for snat target");
                        else if (redir.helper)
@@ -2430,12 +2476,14 @@ return {
 
                                family: family,
                                proto: proto,
-                               has_addrs: !!(length(saddrs) || length(daddrs)),
+                               has_addrs: !!(saddrs[0] || saddrs[1] || saddrs[2] || daddrs[0] || daddrs[1] || daddrs[2]),
                                has_ports: !!(sport || dport || rport),
-                               saddrs_pos: map(filter_pos(saddrs), this.cidr),
-                               saddrs_neg: map(filter_neg(saddrs), this.cidr),
-                               daddrs_pos: map(filter_pos(daddrs), this.cidr),
-                               daddrs_neg: map(filter_neg(daddrs), this.cidr),
+                               saddrs_pos: map(saddrs[0], this.cidr),
+                               saddrs_neg: map(saddrs[1], this.cidr),
+                               saddrs_masked: saddrs[2],
+                               daddrs_pos: map(daddrs[0], this.cidr),
+                               daddrs_neg: map(daddrs[1], this.cidr),
+                               daddrs_masked: daddrs[2],
                                sports_pos: map(filter_pos(to_array(sport)), this.port),
                                sports_neg: map(filter_neg(to_array(sport)), this.port),
                                dports_pos: map(filter_pos(to_array(dport)), this.port),
@@ -2572,13 +2620,19 @@ return {
                                                                refredir.src = rzone;
                                                                refredir.dest = null;
                                                                refredir.target = "dnat";
-                                                               add_rule(i ? 6 : 4, proto, iaddrs[i], eaddrs[i], rip[i], sport, dport, rport, null, refredir);
+
+                                                               for (let saddrs in subnets_group_by_masking(iaddrs[i]))
+                                                                       for (let daddrs in subnets_group_by_masking(eaddrs[i]))
+                                                                               add_rule(i ? 6 : 4, proto, saddrs, daddrs, rip[i], sport, dport, rport, null, refredir);
 
                                                                for (let refaddr in refaddrs[i]) {
                                                                        refredir.src = null;
                                                                        refredir.dest = rzone;
                                                                        refredir.target = "snat";
-                                                                       add_rule(i ? 6 : 4, proto, iaddrs[i], rip[i], [ refaddr ], null, rport, null, null, refredir);
+
+                                                                       for (let saddrs in subnets_group_by_masking(iaddrs[i]))
+                                                                               for (let daddrs in subnets_group_by_masking(rip[i]))
+                                                                                       add_rule(i ? 6 : 4, proto, saddrs, daddrs, [ refaddr ], null, rport, null, null, refredir);
                                                                }
                                                        }
                                                }
@@ -2614,16 +2668,22 @@ return {
                                if (family == null)
                                        family = 4;
 
-                               add_rule(family, proto, null, null, null, sport, dport, rport, null, redir);
+                               add_rule(family, proto, [], [], null, sport, dport, rport, null, redir);
                        }
 
                        /* we need to emit one or two AF specific rules */
                        else {
-                               if ((!family || family == 4) && (length(sip[0]) || length(dip[0]) || length(rip[0])))
-                                       add_rule(4, proto, sip[0], dip[0], rip[0], sport, dport, rport, ipset, redir);
+                               if ((!family || family == 4) && (length(sip[0]) || length(dip[0]) || length(rip[0]))) {
+                                       for (let saddrs in subnets_group_by_masking(sip[0]))
+                                               for (let daddrs in subnets_group_by_masking(dip[0]))
+                                                       add_rule(4, proto, saddrs, daddrs, rip[0], sport, dport, rport, ipset, redir);
+                               }
 
-                               if ((!family || family == 6) && (length(sip[1]) || length(dip[1]) || length(rip[1])))
-                                       add_rule(6, proto, sip[1], dip[1], rip[1], sport, dport, rport, ipset, redir);
+                               if ((!family || family == 6) && (length(sip[1]) || length(dip[1]) || length(rip[1]))) {
+                                       for (let saddrs in subnets_group_by_masking(sip[1]))
+                                               for (let daddrs in subnets_group_by_masking(dip[1]))
+                                                       add_rule(6, proto, saddrs, daddrs, rip[1], sport, dport, rport, ipset, redir);
+                               }
                        }
                }
        },
@@ -2703,6 +2763,11 @@ return {
                        return;
                }
 
+               if (snat.snat_ip && length(filter(snat.snat_ip.addrs, a => a.bits == -1 || a.invert))) {
+                       this.warn_section(data, "must not use inversion or non-contiguous masks in 'snat_ip', ignoring section");
+                       return;
+               }
+
                if (snat.src && snat.src.zone)
                        snat.src.zone.dflags.snat = true;
 
@@ -2712,12 +2777,14 @@ return {
 
                                family: family,
                                proto: proto,
-                               has_addrs: !!(length(saddrs) || length(daddrs) || length(raddrs)),
+                               has_addrs: !!(saddrs[0] || saddrs[1] || saddrs[2] || daddrs[0] || daddrs[1] || daddrs[2]),
                                has_ports: !!(sport || dport),
-                               saddrs_pos: map(filter_pos(saddrs), this.cidr),
-                               saddrs_neg: map(filter_neg(saddrs), this.cidr),
-                               daddrs_pos: map(filter_pos(daddrs), this.cidr),
-                               daddrs_neg: map(filter_neg(daddrs), this.cidr),
+                               saddrs_pos: map(saddrs[0], this.cidr),
+                               saddrs_neg: map(saddrs[1], this.cidr),
+                               saddrs_masked: saddrs[2],
+                               daddrs_pos: map(daddrs[0], this.cidr),
+                               daddrs_neg: map(daddrs[1], this.cidr),
+                               daddrs_masked: daddrs[2],
                                sports_pos: map(filter_pos(to_array(sport)), this.port),
                                sports_neg: map(filter_neg(to_array(sport)), this.port),
                                dports_pos: map(filter_pos(to_array(dport)), this.port),
@@ -2778,16 +2845,20 @@ return {
 
                        /* check if there's no AF specific bits, in this case we can do an AF agnostic rule */
                        if (!family && !length(sip[0]) && !length(sip[1]) && !length(dip[0]) && !length(dip[1]) && !length(rip[0]) && !length(rip[1])) {
-                               add_rule(0, proto, null, null, null, sport, dport, rport, snat);
+                               add_rule(0, proto, [], [], null, sport, dport, rport, snat);
                        }
 
                        /* we need to emit one or two AF specific rules */
                        else {
                                if (family == 0 || family == 4)
-                                       add_rule(4, proto, sip[0], dip[0], rip[0], sport, dport, rport, snat);
+                                       for (let saddr in subnets_group_by_masking(sip[0]))
+                                               for (let daddr in subnets_group_by_masking(dip[0]))
+                                                       add_rule(4, proto, saddr, daddr, rip[0], sport, dport, rport, snat);
 
                                if (family == 0 || family == 6)
-                                       add_rule(6, proto, sip[1], dip[1], rip[1], sport, dport, rport, snat);
+                                       for (let saddr in subnets_group_by_masking(sip[1]))
+                                               for (let daddr in subnets_group_by_masking(dip[1]))
+                                                       add_rule(6, proto, saddr, daddr, rip[1], sport, dport, rport, snat);
                        }
                }
        },
diff --git a/tests/02_zones/05_subnet_mask_matches b/tests/02_zones/05_subnet_mask_matches
new file mode 100644 (file)
index 0000000..54a86a1
--- /dev/null
@@ -0,0 +1,222 @@
+Test that non-contiguous subnet masks are properly handled. Such masks need
+to be translated into bitwise expressions which may not appear as part of
+sets, so various permutations of rules need to be emitted.
+
+-- Testcase --
+{%
+       include("./root/usr/share/firewall4/main.uc", {
+               getenv: function(varname) {
+                       switch (varname) {
+                       case 'ACTION':
+                               return 'print';
+                       }
+               }
+       })
+%}
+-- End --
+
+-- File uci/helpers.json --
+{}
+-- End --
+
+-- File uci/firewall.json --
+{
+       "zone": [
+               {
+                       ".description": "IP addrs with non-contiguous masks should be translated to bitwise comparisons",
+                       "name": "test1",
+                       "subnet": [
+                               "::1/::ffff",
+                               "!::2/::ffff"
+                       ]
+               },
+
+               {
+                       ".description": "IP addrs with non-contiguous masks should not be part of sets",
+                       "name": "test2",
+                       "subnet": [
+                               "::1/::ffff",
+                               "::2/::ffff",
+                               "::3/128",
+                               "::4/128",
+                               "!::5/::ffff",
+                               "!::6/::ffff",
+                               "!::7/128",
+                               "!::8/128"
+                       ]
+               }
+       ]
+}
+-- End --
+
+-- Expect stdout --
+table inet fw4
+flush table inet fw4
+
+table inet fw4 {
+       #
+       # Set definitions
+       #
+
+
+       #
+       # Defines
+       #
+
+       define test2_subnets = { ::3, ::4 }
+
+       #
+       # User includes
+       #
+
+       include "/etc/nftables.d/*.nft"
+
+
+       #
+       # Filter rules
+       #
+
+       chain input {
+               type filter hook input priority filter; policy drop;
+
+               iifname "lo" accept comment "!fw4: Accept traffic from loopback"
+
+               ct state established,related accept comment "!fw4: Allow inbound established and related flows"
+               meta nfproto ipv6 ip6 saddr & ::ffff == ::1 ip6 saddr & ::ffff != ::2 jump input_test1 comment "!fw4: Handle test1 IPv6 input traffic"
+               meta nfproto ipv6 ip6 saddr != { ::7, ::8 } ip6 saddr & ::ffff == ::1 ip6 saddr & ::ffff != ::5 ip6 saddr & ::ffff != ::6 jump input_test2 comment "!fw4: Handle test2 IPv6 input traffic"
+               meta nfproto ipv6 ip6 saddr != { ::7, ::8 } ip6 saddr & ::ffff == ::2 ip6 saddr & ::ffff != ::5 ip6 saddr & ::ffff != ::6 jump input_test2 comment "!fw4: Handle test2 IPv6 input traffic"
+               meta nfproto ipv6 ip6 saddr { ::3, ::4 } ip6 saddr != { ::7, ::8 } ip6 saddr & ::ffff != ::5 ip6 saddr & ::ffff != ::6 jump input_test2 comment "!fw4: Handle test2 IPv6 input traffic"
+       }
+
+       chain forward {
+               type filter hook forward priority filter; policy drop;
+
+               ct state established,related accept comment "!fw4: Allow forwarded established and related flows"
+               meta nfproto ipv6 ip6 saddr & ::ffff == ::1 ip6 saddr & ::ffff != ::2 jump forward_test1 comment "!fw4: Handle test1 IPv6 forward traffic"
+               meta nfproto ipv6 ip6 saddr != { ::7, ::8 } ip6 saddr & ::ffff == ::1 ip6 saddr & ::ffff != ::5 ip6 saddr & ::ffff != ::6 jump forward_test2 comment "!fw4: Handle test2 IPv6 forward traffic"
+               meta nfproto ipv6 ip6 saddr != { ::7, ::8 } ip6 saddr & ::ffff == ::2 ip6 saddr & ::ffff != ::5 ip6 saddr & ::ffff != ::6 jump forward_test2 comment "!fw4: Handle test2 IPv6 forward traffic"
+               meta nfproto ipv6 ip6 saddr { ::3, ::4 } ip6 saddr != { ::7, ::8 } ip6 saddr & ::ffff != ::5 ip6 saddr & ::ffff != ::6 jump forward_test2 comment "!fw4: Handle test2 IPv6 forward traffic"
+       }
+
+       chain output {
+               type filter hook output priority filter; policy drop;
+
+               oifname "lo" accept comment "!fw4: Accept traffic towards loopback"
+
+               ct state established,related accept comment "!fw4: Allow outbound established and related flows"
+               meta nfproto ipv6 ip6 daddr & ::ffff == ::1 ip6 daddr & ::ffff != ::2 jump output_test1 comment "!fw4: Handle test1 IPv6 output traffic"
+               meta nfproto ipv6 ip6 daddr != { ::7, ::8 } ip6 daddr & ::ffff == ::1 ip6 daddr & ::ffff != ::5 ip6 daddr & ::ffff != ::6 jump output_test2 comment "!fw4: Handle test2 IPv6 output traffic"
+               meta nfproto ipv6 ip6 daddr != { ::7, ::8 } ip6 daddr & ::ffff == ::2 ip6 daddr & ::ffff != ::5 ip6 daddr & ::ffff != ::6 jump output_test2 comment "!fw4: Handle test2 IPv6 output traffic"
+               meta nfproto ipv6 ip6 daddr { ::3, ::4 } ip6 daddr != { ::7, ::8 } ip6 daddr & ::ffff != ::5 ip6 daddr & ::ffff != ::6 jump output_test2 comment "!fw4: Handle test2 IPv6 output traffic"
+       }
+
+       chain handle_reject {
+               meta l4proto tcp reject with tcp reset comment "!fw4: Reject TCP traffic"
+               reject with icmpx type port-unreachable comment "!fw4: Reject any other traffic"
+       }
+
+       chain input_test1 {
+               jump drop_from_test1
+       }
+
+       chain output_test1 {
+               jump drop_to_test1
+       }
+
+       chain forward_test1 {
+               jump drop_to_test1
+       }
+
+       chain drop_from_test1 {
+               meta nfproto ipv6 ip6 saddr & ::ffff == ::1 ip6 saddr & ::ffff != ::2 counter drop comment "!fw4: drop test1 IPv6 traffic"
+       }
+
+       chain drop_to_test1 {
+               meta nfproto ipv6 ip6 daddr & ::ffff == ::1 ip6 daddr & ::ffff != ::2 counter drop comment "!fw4: drop test1 IPv6 traffic"
+       }
+
+       chain input_test2 {
+               jump drop_from_test2
+       }
+
+       chain output_test2 {
+               jump drop_to_test2
+       }
+
+       chain forward_test2 {
+               jump drop_to_test2
+       }
+
+       chain drop_from_test2 {
+               meta nfproto ipv6 ip6 saddr != { ::7, ::8 } ip6 saddr & ::ffff == ::1 ip6 saddr & ::ffff != ::5 ip6 saddr & ::ffff != ::6 counter drop comment "!fw4: drop test2 IPv6 traffic"
+               meta nfproto ipv6 ip6 saddr != { ::7, ::8 } ip6 saddr & ::ffff == ::2 ip6 saddr & ::ffff != ::5 ip6 saddr & ::ffff != ::6 counter drop comment "!fw4: drop test2 IPv6 traffic"
+               meta nfproto ipv6 ip6 saddr { ::3, ::4 } ip6 saddr != { ::7, ::8 } ip6 saddr & ::ffff != ::5 ip6 saddr & ::ffff != ::6 counter drop comment "!fw4: drop test2 IPv6 traffic"
+       }
+
+       chain drop_to_test2 {
+               meta nfproto ipv6 ip6 daddr != { ::7, ::8 } ip6 daddr & ::ffff == ::1 ip6 daddr & ::ffff != ::5 ip6 daddr & ::ffff != ::6 counter drop comment "!fw4: drop test2 IPv6 traffic"
+               meta nfproto ipv6 ip6 daddr != { ::7, ::8 } ip6 daddr & ::ffff == ::2 ip6 daddr & ::ffff != ::5 ip6 daddr & ::ffff != ::6 counter drop comment "!fw4: drop test2 IPv6 traffic"
+               meta nfproto ipv6 ip6 daddr { ::3, ::4 } ip6 daddr != { ::7, ::8 } ip6 daddr & ::ffff != ::5 ip6 daddr & ::ffff != ::6 counter drop comment "!fw4: drop test2 IPv6 traffic"
+       }
+
+
+       #
+       # NAT rules
+       #
+
+       chain dstnat {
+               type nat hook prerouting priority dstnat; policy accept;
+       }
+
+       chain srcnat {
+               type nat hook postrouting priority srcnat; policy accept;
+       }
+
+
+       #
+       # Raw rules (notrack & helper)
+       #
+
+       chain raw_prerouting {
+               type filter hook prerouting priority raw; policy accept;
+               meta nfproto ipv6 ip6 saddr != { ::7, ::8 } ip6 saddr & ::ffff == ::1 ip6 saddr & ::ffff != ::5 ip6 saddr & ::ffff != ::6 jump helper_test2 comment "!fw4: test2 IPv6 CT helper assignment"
+               meta nfproto ipv6 ip6 saddr != { ::7, ::8 } ip6 saddr & ::ffff == ::2 ip6 saddr & ::ffff != ::5 ip6 saddr & ::ffff != ::6 jump helper_test2 comment "!fw4: test2 IPv6 CT helper assignment"
+               meta nfproto ipv6 ip6 saddr { ::3, ::4 } ip6 saddr != { ::7, ::8 } ip6 saddr & ::ffff != ::5 ip6 saddr & ::ffff != ::6 jump helper_test2 comment "!fw4: test2 IPv6 CT helper assignment"
+       }
+
+       chain raw_output {
+               type filter hook output priority raw; policy accept;
+       }
+
+       chain helper_test1 {
+       }
+
+       chain helper_test2 {
+       }
+
+
+       #
+       # Mangle rules
+       #
+
+       chain mangle_prerouting {
+               type filter hook prerouting priority mangle; policy accept;
+       }
+
+       chain mangle_postrouting {
+               type filter hook postrouting priority mangle; policy accept;
+       }
+
+       chain mangle_input {
+               type filter hook input priority mangle; policy accept;
+       }
+
+       chain mangle_output {
+               type filter hook output priority mangle; policy accept;
+       }
+
+       chain mangle_forward {
+               type filter hook forward priority mangle; policy accept;
+       }
+}
+-- End --
diff --git a/tests/03_rules/06_subnet_mask_matches b/tests/03_rules/06_subnet_mask_matches
new file mode 100644 (file)
index 0000000..f791376
--- /dev/null
@@ -0,0 +1,321 @@
+Test that non-contiguous subnet masks are properly handled in rule source
+or destination IP expressions. Such masks need to be translated into
+bitwise expressions which may not appear as part of sets, so various
+permutations of rules need to be emitted.
+
+-- Testcase --
+{%
+       include("./root/usr/share/firewall4/main.uc", {
+               getenv: function(varname) {
+                       switch (varname) {
+                       case 'ACTION':
+                               return 'print';
+                       }
+               }
+       })
+%}
+-- End --
+
+-- File uci/helpers.json --
+{}
+-- End --
+
+-- File uci/firewall.json --
+{
+       "zone": [
+               {
+                       "name": "wan",
+                       "network": "wan6",
+                       "masq6": 1
+               },
+               {
+                       "name": "lan",
+                       "network": "lan",
+                       "auto_helper": 0
+               },
+               {
+                       "name": "guest",
+                       "network": "guest",
+                       "auto_helper": 0
+               }
+       ],
+       "rule": [
+               {
+                       ".description": "Ensure that IPs with non-contiguous masks are properly translated",
+                       "proto": "all",
+                       "name": "Mask rule #1",
+                       "src_ip": "::1/::ffff",
+                       "dest_ip": "!::2/::ffff"
+               },
+               {
+                       ".description": "Ensure that combinations of multiple masked and not masked IPs yield the proper rule permutations",
+                       "proto": "all",
+                       "name": "Mask rule #2",
+                       "src_ip": [
+                               "::1/::ffff",
+                               "::2/::ffff",
+                               "::3/128",
+                               "::4/128",
+                               "!::5/::ffff",
+                               "!::6/::ffff",
+                               "!::7/128",
+                               "!::8/128"
+                       ],
+                       "dest_ip": [
+                               "::9/::ffff",
+                               "::10/::ffff",
+                               "::11/128",
+                               "::12/128",
+                               "!::13/::ffff",
+                               "!::14/::ffff",
+                               "!::15/128",
+                               "!::16/128"
+                       ]
+               }
+       ],
+       "redirect": [
+               {
+                       ".description": "Ensure that masked IPs are properly handled in reflection rules",
+                       "proto": "all",
+                       "name": "Mask rule #3",
+                       "src": "wan",
+                       "dest": "lan",
+                       "src_ip": "::1/::ffff",
+                       "src_dip": "::9/::ffff",
+                       "dest_ip": "::99",
+                       "dest_port": "22",
+                       "target": "DNAT",
+                       "reflection_zone": [ "lan", "guest" ]
+               }
+       ]
+}
+-- End --
+
+-- Expect stdout --
+table inet fw4
+flush table inet fw4
+
+table inet fw4 {
+       #
+       # Set definitions
+       #
+
+
+       #
+       # Defines
+       #
+
+       define wan_devices = { "eth1" }
+       define wan_subnets = { 2001:db8:54:321::/64 }
+       define lan_devices = { "br-lan" }
+       define lan_subnets = { 10.0.0.0/24, 192.168.26.0/24, 2001:db8:1000::/60, fd63:e2f:f706::/60 }
+       define guest_devices = { "br-guest" }
+       define guest_subnets = { 10.1.0.0/24, 192.168.27.0/24, 2001:db8:1000::/60, fd63:e2f:f706::/60 }
+
+       #
+       # User includes
+       #
+
+       include "/etc/nftables.d/*.nft"
+
+
+       #
+       # Filter rules
+       #
+
+       chain input {
+               type filter hook input priority filter; policy drop;
+
+               iifname "lo" accept comment "!fw4: Accept traffic from loopback"
+
+               ct state established,related accept comment "!fw4: Allow inbound established and related flows"
+               iifname "eth1" jump input_wan comment "!fw4: Handle wan IPv4/IPv6 input traffic"
+               iifname "br-lan" jump input_lan comment "!fw4: Handle lan IPv4/IPv6 input traffic"
+               iifname "br-guest" jump input_guest comment "!fw4: Handle guest IPv4/IPv6 input traffic"
+       }
+
+       chain forward {
+               type filter hook forward priority filter; policy drop;
+
+               ct state established,related accept comment "!fw4: Allow forwarded established and related flows"
+               iifname "eth1" jump forward_wan comment "!fw4: Handle wan IPv4/IPv6 forward traffic"
+               iifname "br-lan" jump forward_lan comment "!fw4: Handle lan IPv4/IPv6 forward traffic"
+               iifname "br-guest" jump forward_guest comment "!fw4: Handle guest IPv4/IPv6 forward traffic"
+       }
+
+       chain output {
+               type filter hook output priority filter; policy drop;
+
+               oifname "lo" accept comment "!fw4: Accept traffic towards loopback"
+
+               ct state established,related accept comment "!fw4: Allow outbound established and related flows"
+               ip6 saddr & ::ffff == ::1 ip6 daddr & ::ffff != ::2 counter comment "!fw4: Mask rule #1"
+               ip6 saddr != { ::7, ::8 } ip6 saddr & ::ffff == ::1 ip6 saddr & ::ffff != ::5 ip6 saddr & ::ffff != ::6 ip6 daddr != { ::15, ::16 } ip6 daddr & ::ffff == ::9 ip6 daddr & ::ffff != ::13 ip6 daddr & ::ffff != ::14 counter comment "!fw4: Mask rule #2"
+               ip6 saddr != { ::7, ::8 } ip6 saddr & ::ffff == ::1 ip6 saddr & ::ffff != ::5 ip6 saddr & ::ffff != ::6 ip6 daddr != { ::15, ::16 } ip6 daddr & ::ffff == ::10 ip6 daddr & ::ffff != ::13 ip6 daddr & ::ffff != ::14 counter comment "!fw4: Mask rule #2"
+               ip6 saddr != { ::7, ::8 } ip6 saddr & ::ffff == ::1 ip6 saddr & ::ffff != ::5 ip6 saddr & ::ffff != ::6 ip6 daddr { ::11, ::12 } ip6 daddr != { ::15, ::16 } ip6 daddr & ::ffff != ::13 ip6 daddr & ::ffff != ::14 counter comment "!fw4: Mask rule #2"
+               ip6 saddr != { ::7, ::8 } ip6 saddr & ::ffff == ::2 ip6 saddr & ::ffff != ::5 ip6 saddr & ::ffff != ::6 ip6 daddr != { ::15, ::16 } ip6 daddr & ::ffff == ::9 ip6 daddr & ::ffff != ::13 ip6 daddr & ::ffff != ::14 counter comment "!fw4: Mask rule #2"
+               ip6 saddr != { ::7, ::8 } ip6 saddr & ::ffff == ::2 ip6 saddr & ::ffff != ::5 ip6 saddr & ::ffff != ::6 ip6 daddr != { ::15, ::16 } ip6 daddr & ::ffff == ::10 ip6 daddr & ::ffff != ::13 ip6 daddr & ::ffff != ::14 counter comment "!fw4: Mask rule #2"
+               ip6 saddr != { ::7, ::8 } ip6 saddr & ::ffff == ::2 ip6 saddr & ::ffff != ::5 ip6 saddr & ::ffff != ::6 ip6 daddr { ::11, ::12 } ip6 daddr != { ::15, ::16 } ip6 daddr & ::ffff != ::13 ip6 daddr & ::ffff != ::14 counter comment "!fw4: Mask rule #2"
+               ip6 saddr { ::3, ::4 } ip6 saddr != { ::7, ::8 } ip6 saddr & ::ffff != ::5 ip6 saddr & ::ffff != ::6 ip6 daddr != { ::15, ::16 } ip6 daddr & ::ffff == ::9 ip6 daddr & ::ffff != ::13 ip6 daddr & ::ffff != ::14 counter comment "!fw4: Mask rule #2"
+               ip6 saddr { ::3, ::4 } ip6 saddr != { ::7, ::8 } ip6 saddr & ::ffff != ::5 ip6 saddr & ::ffff != ::6 ip6 daddr != { ::15, ::16 } ip6 daddr & ::ffff == ::10 ip6 daddr & ::ffff != ::13 ip6 daddr & ::ffff != ::14 counter comment "!fw4: Mask rule #2"
+               ip6 saddr { ::3, ::4 } ip6 saddr != { ::7, ::8 } ip6 saddr & ::ffff != ::5 ip6 saddr & ::ffff != ::6 ip6 daddr { ::11, ::12 } ip6 daddr != { ::15, ::16 } ip6 daddr & ::ffff != ::13 ip6 daddr & ::ffff != ::14 counter comment "!fw4: Mask rule #2"
+               oifname "eth1" jump output_wan comment "!fw4: Handle wan IPv4/IPv6 output traffic"
+               oifname "br-lan" jump output_lan comment "!fw4: Handle lan IPv4/IPv6 output traffic"
+               oifname "br-guest" jump output_guest comment "!fw4: Handle guest IPv4/IPv6 output traffic"
+       }
+
+       chain handle_reject {
+               meta l4proto tcp reject with tcp reset comment "!fw4: Reject TCP traffic"
+               reject with icmpx type port-unreachable comment "!fw4: Reject any other traffic"
+       }
+
+       chain input_wan {
+               ct status dnat accept comment "!fw4: Accept port redirections"
+               jump drop_from_wan
+       }
+
+       chain output_wan {
+               jump drop_to_wan
+       }
+
+       chain forward_wan {
+               ct status dnat accept comment "!fw4: Accept port forwards"
+               jump drop_to_wan
+       }
+
+       chain drop_from_wan {
+               iifname "eth1" counter drop comment "!fw4: drop wan IPv4/IPv6 traffic"
+       }
+
+       chain drop_to_wan {
+               oifname "eth1" counter drop comment "!fw4: drop wan IPv4/IPv6 traffic"
+       }
+
+       chain input_lan {
+               ct status dnat accept comment "!fw4: Accept port redirections"
+               jump drop_from_lan
+       }
+
+       chain output_lan {
+               jump drop_to_lan
+       }
+
+       chain forward_lan {
+               ct status dnat accept comment "!fw4: Accept port forwards"
+               jump drop_to_lan
+       }
+
+       chain drop_from_lan {
+               iifname "br-lan" counter drop comment "!fw4: drop lan IPv4/IPv6 traffic"
+       }
+
+       chain drop_to_lan {
+               oifname "br-lan" counter drop comment "!fw4: drop lan IPv4/IPv6 traffic"
+       }
+
+       chain input_guest {
+               ct status dnat accept comment "!fw4: Accept port redirections"
+               jump drop_from_guest
+       }
+
+       chain output_guest {
+               jump drop_to_guest
+       }
+
+       chain forward_guest {
+               ct status dnat accept comment "!fw4: Accept port forwards"
+               jump drop_to_guest
+       }
+
+       chain drop_from_guest {
+               iifname "br-guest" counter drop comment "!fw4: drop guest IPv4/IPv6 traffic"
+       }
+
+       chain drop_to_guest {
+               oifname "br-guest" counter drop comment "!fw4: drop guest IPv4/IPv6 traffic"
+       }
+
+
+       #
+       # NAT rules
+       #
+
+       chain dstnat {
+               type nat hook prerouting priority dstnat; policy accept;
+               iifname "eth1" jump dstnat_wan comment "!fw4: Handle wan IPv4/IPv6 dstnat traffic"
+               iifname "br-lan" jump dstnat_lan comment "!fw4: Handle lan IPv4/IPv6 dstnat traffic"
+               iifname "br-guest" jump dstnat_guest comment "!fw4: Handle guest IPv4/IPv6 dstnat traffic"
+       }
+
+       chain srcnat {
+               type nat hook postrouting priority srcnat; policy accept;
+               oifname "eth1" jump srcnat_wan comment "!fw4: Handle wan IPv4/IPv6 srcnat traffic"
+               oifname "br-lan" jump srcnat_lan comment "!fw4: Handle lan IPv4/IPv6 srcnat traffic"
+               oifname "br-guest" jump srcnat_guest comment "!fw4: Handle guest IPv4/IPv6 srcnat traffic"
+       }
+
+       chain dstnat_wan {
+               ip6 saddr & ::ffff == ::1 ip6 daddr & ::ffff == ::9 counter dnat ::99 comment "!fw4: Mask rule #3"
+       }
+
+       chain srcnat_wan {
+               meta nfproto ipv6 masquerade comment "!fw4: Masquerade IPv6 wan traffic"
+       }
+
+       chain dstnat_lan {
+               ip6 saddr { 2001:db8:1000::/60, fd63:e2f:f706::/60 } ip6 daddr & ::ffff == ::9 dnat ::99 comment "!fw4: Mask rule #3 (reflection)"
+       }
+
+       chain srcnat_lan {
+               ip6 saddr 2001:db8:1000::/60 ip6 daddr ::99 snat 2001:db8:1000:1::1 comment "!fw4: Mask rule #3 (reflection)"
+               ip6 saddr fd63:e2f:f706::/60 ip6 daddr ::99 snat fd63:e2f:f706:1::1 comment "!fw4: Mask rule #3 (reflection)"
+       }
+
+       chain dstnat_guest {
+               ip6 saddr { 2001:db8:1000::/60, fd63:e2f:f706::/60 } ip6 daddr & ::ffff == ::9 dnat ::99 comment "!fw4: Mask rule #3 (reflection)"
+       }
+
+       chain srcnat_guest {
+               ip6 saddr 2001:db8:1000::/60 ip6 daddr ::99 snat 2001:db8:1000:2::1 comment "!fw4: Mask rule #3 (reflection)"
+               ip6 saddr fd63:e2f:f706::/60 ip6 daddr ::99 snat fd63:e2f:f706:2::1 comment "!fw4: Mask rule #3 (reflection)"
+       }
+
+
+       #
+       # Raw rules (notrack & helper)
+       #
+
+       chain raw_prerouting {
+               type filter hook prerouting priority raw; policy accept;
+       }
+
+       chain raw_output {
+               type filter hook output priority raw; policy accept;
+       }
+
+
+       #
+       # Mangle rules
+       #
+
+       chain mangle_prerouting {
+               type filter hook prerouting priority mangle; policy accept;
+       }
+
+       chain mangle_postrouting {
+               type filter hook postrouting priority mangle; policy accept;
+       }
+
+       chain mangle_input {
+               type filter hook input priority mangle; policy accept;
+       }
+
+       chain mangle_output {
+               type filter hook output priority mangle; policy accept;
+       }
+
+       chain mangle_forward {
+               type filter hook forward priority mangle; policy accept;
+       }
+}
+-- End --