Skip to content

Commit

Permalink
firewall: T4694: Adding GRE flags & fields matches to firewall rules
Browse files Browse the repository at this point in the history
Work in progress:
* Only matching flags and fields used by modern RFC2890 "extended GRE"
* There are no NFT helpers for the GRE key field, which is critical to match individual tunnel sessions
* NFT 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 "gre-protocol" validator for the fields we can pass to nft's gre matches.
  * The protocol names are out of synch with other parts of the firewall def, but for eg,
    I can't call a completion+valueHelp "ipv6" without VyOS deciding to show an IPv6 pattern instead
    of my help text.
  * I've allowed arbitrary radix numbers for the ethertype, for eg, it's common to use 0x8100
    for .1q instead of 33024. nft should accept these as well.
  • Loading branch information
talmakion committed Jun 16, 2024
1 parent 6129138 commit 3798c6d
Show file tree
Hide file tree
Showing 5 changed files with 240 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@
#include <include/firewall/synproxy.xml.i>
#include <include/firewall/tcp-flags.xml.i>
#include <include/firewall/tcp-mss.xml.i>
#include <include/firewall/gre.xml.i>
#include <include/firewall/time.xml.i>
<!-- include end -->
126 changes: 126 additions & 0 deletions interface-definitions/include/firewall/gre.xml.i
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<!-- include start from firewall/gre.xml.i -->
<node name="gre">
<properties>
<help>GRE fields to match</help>
</properties>
<children>
<node name="flags">
<properties>
<help>GRE flag bits to match</help>
</properties>
<children>
<leafNode name="key">
<properties>
<help>Header includes optional key field</help>
<valueless/>
</properties>
</leafNode>
<leafNode name="checksum">
<properties>
<help>Header includes optional checksum</help>
<valueless/>
</properties>
</leafNode>
<leafNode name="sequence">
<properties>
<help>Header includes a sequence number field</help>
<valueless/>
</properties>
</leafNode>
<node name="not">
<properties>
<help>Match flags not set</help>
</properties>
<children>
<leafNode name="key">
<properties>
<help>Header does not include optional key field</help>
<valueless/>
</properties>
</leafNode>
<leafNode name="checksum">
<properties>
<help>Header does not include optional checksum</help>
<valueless/>
</properties>
</leafNode>
<leafNode name="sequence">
<properties>
<help>Header does not include a sequence number field</help>
<valueless/>
</properties>
</leafNode>
</children>
</node>
</children>
</node>
<leafNode name="inner-proto">
<properties>
<help>EtherType of encapsulated packet</help>
<completionHelp>
<list>ip ip6 arp vlan 8021q 8021ad</list>
</completionHelp>
<valueHelp>
<format>u32:0-65535</format>
<description>Ethernet protocol number</description>
</valueHelp>
<valueHelp>
<format>ip</format>
<description>IPv4</description>
</valueHelp>
<valueHelp>
<format>ip6</format>
<description>IPv6</description>
</valueHelp>
<valueHelp>
<format>arp</format>
<description>Address Resolution Protocol</description>
</valueHelp>
<valueHelp>
<format>vlan</format>
<description>VLAN-tagged Ethernet</description>
</valueHelp>
<valueHelp>
<format>8021q</format>
<description>VLAN-tagged Ethernet</description>
</valueHelp>
<valueHelp>
<format>8021ad</format>
<description>Bridged Ethernet</description>
</valueHelp>
<constraint>
<validator name="gre-protocol"/>
</constraint>
</properties>
</leafNode>
<leafNode name="key">
<properties>
<help>Tunnel Key</help>
<valueHelp>
<format>u32</format>
<description>Tunnel Key ID</description>
</valueHelp>
<constraint>
<validator name="numeric" />
</constraint>
</properties>
</leafNode>
<leafNode name="version">
<properties>
<help>GRE Version</help>
<valueHelp>
<format>gre</format>
<description>Standard GRE</description>
</valueHelp>
<valueHelp>
<format>pptp</format>
<description>Point to Point Protocol</description>
</valueHelp>
<constraint>
<regex>(gre|pptp)</regex>
</constraint>
</properties>
</leafNode>
</children>
</node>
<!-- include end -->
45 changes: 45 additions & 0 deletions python/vyos/firewall.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']))

Expand Down Expand Up @@ -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 []
Expand Down
30 changes: 30 additions & 0 deletions src/conf_mode/firewall.py
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down
38 changes: 38 additions & 0 deletions src/validators/gre-protocol
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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)

0 comments on commit 3798c6d

Please sign in to comment.