From e60bb4b47ff9aad6806afc0468f4217a344a7cf0 Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Wed, 26 Jan 2022 12:05:39 +0100 Subject: [PATCH] ruleset: support non-contiguous address masks 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 --- .../usr/share/firewall4/templates/redirect.uc | 4 + root/usr/share/firewall4/templates/rule.uc | 4 + root/usr/share/firewall4/templates/ruleset.uc | 36 +- .../share/firewall4/templates/zone-masq.uc | 13 + .../share/firewall4/templates/zone-match.uc | 2 + root/usr/share/ucode/fw4.uc | 193 +++++++---- tests/02_zones/05_subnet_mask_matches | 222 ++++++++++++ tests/03_rules/06_subnet_mask_matches | 321 ++++++++++++++++++ 8 files changed, 712 insertions(+), 83 deletions(-) create mode 100644 root/usr/share/firewall4/templates/zone-masq.uc create mode 100644 tests/02_zones/05_subnet_mask_matches create mode 100644 tests/03_rules/06_subnet_mask_matches diff --git a/root/usr/share/firewall4/templates/redirect.uc b/root/usr/share/firewall4/templates/redirect.uc index 592af0f..a77e3d5 100644 --- a/root/usr/share/firewall4/templates/redirect.uc +++ b/root/usr/share/firewall4/templates/redirect.uc @@ -10,10 +10,14 @@ {{ 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): -%} diff --git a/root/usr/share/firewall4/templates/rule.uc b/root/usr/share/firewall4/templates/rule.uc index c8bab59..518b9cc 100644 --- a/root/usr/share/firewall4/templates/rule.uc +++ b/root/usr/share/firewall4/templates/rule.uc @@ -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): -%} diff --git a/root/usr/share/firewall4/templates/ruleset.uc b/root/usr/share/firewall4/templates/ruleset.uc index 2a0a8a8..75f0679 100644 --- a/root/usr/share/firewall4/templates/ruleset.uc +++ b/root/usr/share/firewall4/templates/ruleset.uc @@ -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 index 0000000..c3e66d8 --- /dev/null +++ b/root/usr/share/firewall4/templates/zone-masq.uc @@ -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" diff --git a/root/usr/share/firewall4/templates/zone-match.uc b/root/usr/share/firewall4/templates/zone-match.uc index f336447..b069b08 100644 --- a/root/usr/share/firewall4/templates/zone-match.uc +++ b/root/usr/share/firewall4/templates/zone-match.uc @@ -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 -%} diff --git a/root/usr/share/ucode/fw4.uc b/root/usr/share/ucode/fw4.uc index c39bffc..66cc8f6 100644 --- a/root/usr/share/ucode/fw4.uc +++ b/root/usr/share/ucode/fw4.uc @@ -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 index 0000000..54a86a1 --- /dev/null +++ b/tests/02_zones/05_subnet_mask_matches @@ -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 index 0000000..f791376 --- /dev/null +++ b/tests/03_rules/06_subnet_mask_matches @@ -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 -- -- 2.30.2