summaryrefslogtreecommitdiff
path: root/python/vyos/firewall.py
diff options
context:
space:
mode:
authorAndrew Topp <andrewt@telekinetica.net>2024-08-04 17:52:57 +1000
committerAndrew Topp <andrewt@telekinetica.net>2024-08-04 17:52:57 +1000
commit60b0614296874c144665417130d4881461114db0 (patch)
tree404eb8bf72582b60cad69d9c23535b41a49094f6 /python/vyos/firewall.py
parent15c77978f30bebe7c6d4f4e9a87c56e12e1382cd (diff)
downloadvyos-1x-60b0614296874c144665417130d4881461114db0.tar.gz
vyos-1x-60b0614296874c144665417130d4881461114db0.zip
firewall: T4694: Adding GRE flags & fields matches to firewall rules
* Only matching flags and fields used by modern RFC2890 "extended GRE" - this is backwards-compatible, but does not match all possible flags. * There are no nftables helpers for the GRE key field, which is critical to match individual tunnel sessions (more detail in the forum post) * nft expression syntax is not flexible enough for multiple field matches in a single rule and the key offset changes depending on flags. * Thus, clumsy compromise in requiring an explicit match on the "checksum" flag if a key is present, so we know where key will be. In most cases, nobody uses the checksum, but assuming it to be off or automatically adding a "not checksum" match unless told otherwise would be confusing * The automatic "flags key" check when specifying a key doesn't have similar validation, I added it first and it makes sense. I would still like to find a workaround to the "checksum" offset problem. * If we could add 2 rules from 1 config definition, we could match both cases with appropriate offsets, but this would break existing FW generation logic, logging, etc. * Added a "test_gre_match" smoketest
Diffstat (limited to 'python/vyos/firewall.py')
-rw-r--r--python/vyos/firewall.py61
1 files changed, 61 insertions, 0 deletions
diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py
index cac6d2953..3976a5580 100644
--- a/python/vyos/firewall.py
+++ b/python/vyos/firewall.py
@@ -409,6 +409,41 @@ 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_alias_map = {
+ '802.1q': '8021q',
+ '802.1ad': '8021ad',
+ 'gretap': '0x6558',
+ }
+
+ gre_proto = dict_search_args(rule_conf, 'gre', 'inner_proto')
+ if gre_proto is not None:
+ gre_proto = gre_proto_alias_map.get(gre_proto, gre_proto)
+ 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.
+ # nftables cannot handle complex enough expressions to match multiple
+ # offsets based on bitfields elsewhere.
+ # We enforce a specific match for the checksum flag in validation, so the
+ # gre_flags dict will always have a 'checksum' key when gre_key is populated.
+ if not gre_flags['checksum']:
+ # No "unset" child node means C is set, we offset key lookup +32 bits
+ output.append(f'@th,64,32 == {gre_key}')
+ else:
+ output.append(f'@th,32,32 == {gre_key}')
+
if 'time' in rule_conf:
output.append(parse_time(rule_conf['time']))
@@ -544,6 +579,32 @@ 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 = 0
+ exclude = 0
+ for fl_name, fl_state in flags.items():
+ if not fl_state:
+ include |= flag_map[fl_name]
+ else: # 'unset' child tag
+ exclude |= flag_map[fl_name]
+
+ if force_keyed:
+ # Implied by a key-match.
+ include |= flag_map['key']
+
+ if include == 0 and exclude == 0:
+ return '' # Don't bother extracting and matching no bits
+
+ 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 []