diff --git a/capirca/lib/nftables.py b/capirca/lib/nftables.py index df50b95e..104d98fb 100644 --- a/capirca/lib/nftables.py +++ b/capirca/lib/nftables.py @@ -803,38 +803,66 @@ def _ProcessHeader(self, header_options): raise HeaderError('Invalid address family in header: %s. Supported: %s' % (header_options[0], Nftables._HEADER_AF)) netfilter_family = self.NF_TABLE_AF_MAP.get(header_options[0]) + + # Default action is drop. policy_default_action = 'drop' + # If 'ACCEPT' (case sensitive) is specified anywhere in header, set default + # action to accept. if 'ACCEPT' in header_options: policy_default_action = 'accept' + + # Second header element should dictate the netfilter hook. netfilter_hook = header_options[1].lower() if netfilter_hook not in self._SUPPORTED_HOOKS: raise HeaderError( '%s is not a supported nftables hook. Supported hooks: %s' % (netfilter_hook, list(self._SUPPORTED_HOOKS))) + + # If any element of the header is an integer, use it as the priority. + # Currently, there must be exactly one integer in the header. + netfilter_priority = self._HOOK_PRIORITY_DEFAULT if len(header_options) >= 2: numbers = [x for x in header_options if x.isdigit()] if not numbers: - netfilter_priority = self._HOOK_PRIORITY_DEFAULT logging.info( 'INFO: NFtables priority not specified in header.' - 'Defaulting to %s', self._HOOK_PRIORITY_DEFAULT) + 'Defaulting to %s', netfilter_priority) if len(numbers) == 1: # A single integer value is used to set priority. netfilter_priority = numbers[0] if len(numbers) > 1: raise HeaderError('Too many integers in header.') + + # If the string noverbose appears anywhere in the header, set verbose to + # False. verbose = True if 'noverbose' in header_options: verbose = False header_options.remove('noverbose') + + # Process other options, especially those that take arguments. base_chain_name = self._BASE_CHAIN_PREFIX table_name = 'filtering_policies' - for option in header_options: - if option.startswith('base_chain_name='): - base_chain_name = option.split('=')[1].strip() - if option.startswith('table_name='): - table_name = option.split('=')[1].strip() - as_regular_chain = True if 'as_regular_chain' in header_options else False + as_regular_chain = False + skip_n = 0 # How many options should be skipped + for index in range(len(header_options)): + if skip_n > 0: + skip_n -= 1 + continue + option = header_options[index] + if option == 'base-chain-name': + if index + 1 >= len(header_options): + raise HeaderError('base-chain-name option requires a value.') + base_chain_name = header_options[index + 1] + skip_n = 1 + if option == 'table-name': + if index + 1 >= len(header_options): + raise HeaderError('table-name option requires a value.') + table_name = header_options[index + 1] + skip_n = 1 + if option == 'as-regular-chain': + as_regular_chain = True + return ( netfilter_family, netfilter_hook, diff --git a/doc/generators/nftables.md b/doc/generators/nftables.md index 2213fd89..f726908f 100644 --- a/doc/generators/nftables.md +++ b/doc/generators/nftables.md @@ -13,6 +13,9 @@ Unless otherwise stated, all fields are required unless they're marked optional. - default_policy_override: **OPTIONAL** defines the default action (ACCEPT, DROP) for non-matching packets. Default behavior is DROP. (Does not support specifying 'REJECT', only 'ACCEPT'. Case sensitive.) - priority: **OPTIONAL** By default, this generator creates base chains with a starting priority of 0. Defining an integer value will override this behavior. - noverbose: **OPTIONAL** Disable header and term comments in final ACL output. Default behavior is verbose. +- base-chain-name: **OPTIONAL** Takes one argument, and changes the name of the base chain from root to the passed argument (for example, root7 becomes my-chain7) +- table-name: **OPTIONAL** Takes one argument, and changes the name of the table from filtering_policies to the passed argument (for example, table ip6 filtering_policies {} becomes table ip6 my_table {}) +- as-regular-chain: takes no arguments, and removes type filter line from the root chain generated by this header. #### Important: stateful firewall only diff --git a/policies/pol/sample_nftables.pol b/policies/pol/sample_nftables.pol index 41767452..3def75f6 100644 --- a/policies/pol/sample_nftables.pol +++ b/policies/pol/sample_nftables.pol @@ -204,3 +204,43 @@ term excludeed-platform { action:: accept platform-exclude:: nftables } + +header { + comment:: "Demonstrate use of base chain name option. There will be a new chain with the passed name and a number (instead of 'root'), which will jump into member chains for individual terms." + target:: nftables inet6 OUTPUT base-chain-name my-chain +} + +term awesome-term-on-another-chain { + comment:: "Awesomeness on another chain." + action:: accept +} + +header { + comment:: "Demonstrate use of table name option. The table name will be used to create a new nftables table and the policy will be applied to it." + target:: nftables inet6 OUTPUT table-name my-table +} + +term awesome-term-on-another-table { + comment:: "Awesomeness on another table." + action:: accept +} + +header { + comment:: "Demonstrate use of as-regular-chain option." + target:: nftables inet6 OUTPUT as-regular-chain +} + +term awesome-term-as-regular-chain { + comment:: "Awesomeness with a regular chain." + action:: accept +} + +header { + comment:: "Demonstrate the use of multiple options with arguments at once." + target:: nftables inet6 OUTPUT 300 noverbose base-chain-name multipleoptions-chain table-name multipleoptions-table as-regular-chain +} + +term awesome-term-with-multiple-options { + comment:: "Awesomeness with multiple options at once." + action:: accept +} diff --git a/tests/lib/nftables_test.py b/tests/lib/nftables_test.py index 8780d0af..1e0eba2d 100644 --- a/tests/lib/nftables_test.py +++ b/tests/lib/nftables_test.py @@ -19,7 +19,6 @@ from absl import logging from absl.testing import absltest from absl.testing import parameterized -from capirca.lib import aclgenerator from capirca.lib import nacaddr from capirca.lib import naming from capirca.lib import nftables @@ -186,6 +185,30 @@ def __init__(self, in_dict: dict): } """ +GOOD_HEADER_5 = """ +header { + target:: nftables inet input base-chain-name my-chain +} +""" + +GOOD_HEADER_6 = """ +header { + target:: nftables inet6 output table-name my-table +} +""" + +GOOD_HEADER_7 = """ +header { + target:: nftables inet6 output as-regular-chain +} +""" + +BAD_HEADER_MULTIPLE_INTEGERS = """ +header { + target:: nftables inet6 OUTPUT 300 noverbose base-chain-name 1337 table-name 12345 as-regular-chain +} +""" + DENY_TERM = """ term deny-term { comment:: "Dual-stack IPv4/v6 deny all" @@ -1007,9 +1030,26 @@ def testRulesetGeneratorAF(self, policy_data: str, expected_inet: str): TEST_IPS, ' iifname eth123 meta l4proto', ), + ( + GOOD_HEADER_5 + ICMP_SINGLE_TYPE, + TEST_IPS, + 'chain my-chain0', + ), + ( + GOOD_HEADER_6 + ICMP_SINGLE_TYPE, + TEST_IPS, + 'table ip6 my-table', + ), + ( + GOOD_HEADER_7 + ICMP_SINGLE_TYPE, + TEST_IPS, + 'chain good-icmp-single-type', + # Should have chain etc, but not 'type filter...' + ), ) - def testRulesetGenerator(self, policy_data: str, IPs, contains: str): - self.naming.GetNetAddr.return_value = IPs + def testRulesetGenerator(self, policy_data: str, ips: list[str], + contains: str): + self.naming.GetNetAddr.return_value = ips nft = str( nftables.Nftables( policy.ParsePolicy(policy_data, self.naming), EXP_INFO @@ -1026,8 +1066,13 @@ def testRulesetGenerator(self, policy_data: str, IPs, contains: str): GOOD_HEADER_1 + EXCLUDE_NFTABLES_PLATFORM_TERM, 'eth123', ), + ( + # Passing as-regular-chain disables 'type filter hook...'. + GOOD_HEADER_7 + ICMP_SINGLE_TYPE, + 'type filter hook', + ), ) - def testRulesetGeneratorSkippedPlatform( + def testRulesetGenerator_DoesNotContain( self, policy_data: str, does_not_contain: str ): self.naming.GetNetAddr.return_value = TEST_IPS @@ -1038,5 +1083,17 @@ def testRulesetGeneratorSkippedPlatform( ) self.assertNotIn(does_not_contain, nft) + def testRulesetGenerator_RaisesOnMultipleIntegers(self): + self.naming.GetNetAddr.return_value = TEST_IPS + with self.assertRaises(nftables.HeaderError): + _ = str( + nftables.Nftables( + policy.ParsePolicy( + BAD_HEADER_MULTIPLE_INTEGERS + ICMP_SINGLE_TYPE, self.naming + ), + EXP_INFO, + ) + ) + if __name__ == '__main__': absltest.main()