diff --git a/interface-definitions/include/firewall/common-rule-inet.xml.i b/interface-definitions/include/firewall/common-rule-inet.xml.i index 55ffa3a8bb8..5d3a99c4127 100644 --- a/interface-definitions/include/firewall/common-rule-inet.xml.i +++ b/interface-definitions/include/firewall/common-rule-inet.xml.i @@ -20,5 +20,6 @@ #include #include #include +#include #include diff --git a/interface-definitions/include/firewall/gre.xml.i b/interface-definitions/include/firewall/gre.xml.i new file mode 100644 index 00000000000..a498a807102 --- /dev/null +++ b/interface-definitions/include/firewall/gre.xml.i @@ -0,0 +1,126 @@ + + + + GRE fields to match + + + + + GRE flag bits to match + + + + + Header includes optional key field + + + + + + Header includes optional checksum + + + + + + Header includes a sequence number field + + + + + + Match flags not set + + + + + Header does not include optional key field + + + + + + Header does not include optional checksum + + + + + + Header does not include a sequence number field + + + + + + + + + + EtherType of encapsulated packet + + ip ip6 arp vlan 8021q 8021ad + + + u32:0-65535 + Ethernet protocol number + + + ip + IPv4 + + + ip6 + IPv6 + + + arp + Address Resolution Protocol + + + vlan + VLAN-tagged Ethernet + + + 8021q + VLAN-tagged Ethernet + + + 8021ad + Bridged Ethernet + + + + + + + + + Tunnel Key + + u32 + Tunnel Key ID + + + + + + + + + GRE Version + + gre + Standard GRE + + + pptp + Point to Point Protocol + + + (gre|pptp) + + + + + + diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py index 664df28cc6d..75a8e957acb 100644 --- a/python/vyos/firewall.py +++ b/python/vyos/firewall.py @@ -390,6 +390,31 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name): time = rule_conf['recent']['time'] output.append(f'add @RECENT{def_suffix}_{hook}_{fw_name}_{rule_id} {{ {ip_name} saddr limit rate over {count}/{time} burst {count} packets }}') + if 'gre' in rule_conf: + gre_key = dict_search_args(rule_conf, 'gre', 'key') + + gre_flags = dict_search_args(rule_conf, 'gre', 'flags') + output.append(parse_gre_flags(gre_flags or {}, force_keyed=gre_key is not None)) + + gre_proto = dict_search_args(rule_conf, 'gre', 'inner_proto') + if gre_proto is not None: + output.append(f'gre protocol {gre_proto}') + + gre_ver = dict_search_args(rule_conf, 'gre', 'version') + if gre_ver == 'gre': + output.append('gre version 0') + elif gre_ver == 'pptp': + output.append('gre version 1') + + if gre_key: + # The offset of the key within the packet shifts depending on the C-flag. + # We make sure the user has a specific match for the checksum flag in validation. + # I don't like this but nft can't handle complex enough expressions to handle both inline + if gre_flags and 'checksum' in gre_flags: + output.append(f'@th,64,32 == {gre_key}') + else: # checksum will be in not flags, if the validator did its job + output.append(f'@th,32,32 == {gre_key}') + if 'time' in rule_conf: output.append(parse_time(rule_conf['time'])) @@ -516,6 +541,26 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name): output.append(f'comment "{family}-{hook}-{fw_name}-{rule_id}"') return " ".join(output) +def parse_gre_flags(flags, force_keyed=False): + flag_map = { # nft does not have symbolic names for these. + 'checksum': 1<<0, + 'routing': 1<<1, + 'key': 1<<2, + 'sequence': 1<<3, + 'strict_routing': 1<<4, + } + + include = sum([ flag_map[flag] for flag in flags if flag != 'not' ]) + exclude = 0 + if 'not' in flags: + exclude = sum([ flag_map[flag] for flag in flags['not'] ]) + + if force_keyed: + # Implied by a key-match. + include += flag_map['key'] + + return f'gre flags & {include + exclude} == {include}' + def parse_tcp_flags(flags): include = [flag for flag in flags if flag != 'not'] exclude = list(flags['not']) if 'not' in flags else [] diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index ec6b86ef2f2..3bb81f207bf 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -192,6 +192,36 @@ def verify_rule(firewall, rule_conf, ipv6): if not {'count', 'time'} <= set(rule_conf['recent']): raise ConfigError('Recent "count" and "time" values must be defined') + if 'gre' in rule_conf: + if dict_search_args(rule_conf, 'protocol') != 'gre': + raise ConfigError('Protocol must be gre when matching GRE flags and fields') + + gre_checking_key = dict_search_args(rule_conf, 'gre', 'key') + if gre_checking_key: + if dict_search_args(rule_conf, 'gre', 'version') == 'pptp': + raise ConfigError('GRE tunnel keys are not present in PPTP') + + if dict_search_args(rule_conf, 'gre', 'flags', 'checksum') is None and \ + dict_search_args(rule_conf, 'gre', 'flags', 'not', 'checksum') is None: + # There is no builtin match in nftables for the GRE key, so we need to do a raw lookup. + # The offset of the key within the packet shifts depending on the C-flag. + # 99% of the time, nobody will have checksums enabled - it's usually a manual config option. + # We can either assume it is unset unless otherwise directed + # (confusing, requires doco to explain why it doesn't work sometimes) + # or, demand an explicit selection to be made for this specific match rule. + # This check enforces the latter. The user is free to create rules for both cases. + raise ConfigError('Matching GRE tunnel key requires an explicit checksum flag match. For most cases, use "gre flags not checksum"') + + gre_flags = dict_search_args(rule_conf, 'gre', 'flags') + if gre_flags: + gre_not_flags = dict_search_args(rule_conf, 'gre', 'flags', 'not') + if gre_not_flags: + duplicates = [flag for flag in gre_flags if flag in gre_not_flags] + if duplicates: + raise ConfigError(f'Cannot match a GRE flag as set and not set') + if 'key' in gre_not_flags: + raise ConfigError('Matching GRE tunnel key implies "flags key", cannot specify "flags not key"') + tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags') if tcp_flags: if dict_search_args(rule_conf, 'protocol') != 'tcp': diff --git a/src/validators/gre-protocol b/src/validators/gre-protocol new file mode 100755 index 00000000000..97817fd9a22 --- /dev/null +++ b/src/validators/gre-protocol @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import re +from sys import argv,exit + +if __name__ == '__main__': + if len(argv) != 2: + exit(1) + + input = argv[1] + try: + # It's common practice to pass hex for ethtype (eg, 0x8100), so we allow + # other radix prefixes with the int conversion: + if int(input, 0) in range(0, 65535): + exit(0) + except ValueError: + pass + + pattern = "!?\\b(ip|ip6|arp|vlan|8021q|8021ad)\\b" + if re.match(pattern, input): + exit(0) + + print(f'Error: {input} is not a valid GRE inner protocol') + exit(1)