Skip to content

Commit

Permalink
nftable: Add ability to override the rendered table address family
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 675594322
  • Loading branch information
Capirca Team committed Sep 17, 2024
1 parent 3b86521 commit a230c93
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 9 deletions.
28 changes: 27 additions & 1 deletion capirca/lib/nftables.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,7 @@ class Nftables(aclgenerator.ACLGenerator):
SUFFIX = '.nft'
_HEADER_AF = frozenset(('inet', 'inet6', 'mixed'))
_SUPPORTED_HOOKS = frozenset(('forward', 'input', 'output'))
_SUPPORTED_AF_OVERRIDE = frozenset(['bridge'])
_HOOK_PRIORITY_DEFAULT = 0
_BASE_CHAIN_PREFIX = 'root'
_LOGGING = set()
Expand Down Expand Up @@ -709,6 +710,7 @@ def _TranslatePolicy(self, pol, exp_info):
base_chain_name,
table_name,
as_regular_chain,
address_family_override,
) = self._ProcessHeader(filter_options)

# Base chain determine name based on iteration of header.
Expand Down Expand Up @@ -777,7 +779,7 @@ def _TranslatePolicy(self, pol, exp_info):
self.nftables_policies.append(
(header, base_chain_name, nf_af, nf_hook, nf_priority,
filter_policy_default_action, verbose,
child_chains, table_name, as_regular_chain))
child_chains, table_name, as_regular_chain, address_family_override))

def _ProcessHeader(self, header_options):
"""Capirca policy header processing.
Expand All @@ -795,6 +797,12 @@ def _ProcessHeader(self, header_options):
netfilter_priority: numbers = [x for x in filter_options if x.isdigit()]
policy_default_action: nftable action to take on unmatched packets.
verbose: header and term verbosity.
child_chains: dictionary of child chains.
table_name: table name for the generated nftables table.
as_regular_chain: if true, generate nftables ACLs as regular chains
instead of base chains.
address_family_override: address family for the generated nftables table;
if value is None, use netfilter_family from above.
"""
if len(header_options) < 2:
raise HeaderError('Invalid header for Nftables. Required fields missing.')
Expand Down Expand Up @@ -844,6 +852,7 @@ def _ProcessHeader(self, header_options):
base_chain_name = self._BASE_CHAIN_PREFIX
table_name = 'filtering_policies'
as_regular_chain = False
address_family_override = None
skip_n = 0 # How many options should be skipped
for index in range(len(header_options)):
if skip_n > 0:
Expand All @@ -862,6 +871,19 @@ def _ProcessHeader(self, header_options):
skip_n = 1
if option == 'as-regular-chain':
as_regular_chain = True
if option == 'address-family-override':
if index + 1 >= len(header_options):
raise HeaderError('address-family-override option requires a value.')
address_family_override = header_options[index + 1]
if address_family_override not in self._SUPPORTED_AF_OVERRIDE:
raise HeaderError(
'Unsupported address family override: %s. Supported overrides: %s'
% (
address_family_override,
list(self._SUPPORTED_AF_OVERRIDE),
)
)
skip_n = 1

return (
netfilter_family,
Expand All @@ -872,6 +894,7 @@ def _ProcessHeader(self, header_options):
base_chain_name,
table_name,
as_regular_chain,
address_family_override,
)

def _ConfigurationDictionary(self, nft_pol):
Expand All @@ -898,11 +921,14 @@ def _ConfigurationDictionary(self, nft_pol):
child_chains,
table_name,
as_regular_chain,
address_family_override,
) in nft_pol:
base_chain_comment = ''
# TODO: If child_chain ruleset is empty don't store term.
if verbose:
base_chain_comment = header.comment
if address_family_override:
nf_af = address_family_override
nftables[table_name] = nftables.get(table_name, {})
nftables[table_name][nf_af] = nftables[table_name].get(nf_af, {})
nftables[table_name][nf_af][base_chain_name] = {
Expand Down
5 changes: 3 additions & 2 deletions doc/generators/nftables.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
The NFTables header designation has the following format:

```
target:: newnftables [nf_address_family] [nf_hook] {default_policy_override} {int: base chain priority} {noverbose} {base-chain-name chainname} {table-name tablename} {as-regular-chain}
target:: newnftables [nf_address_family] [nf_hook] {default_policy_override} {int: base chain priority} {noverbose} {base-chain-name [chainname]} {table-name [tablename]} {as-regular-chain} {address-family-override [addressfamily]}
```

Unless otherwise stated, all fields are required unless they're marked optional.
Expand All @@ -15,7 +15,8 @@ Unless otherwise stated, all fields are required unless they're marked optional.
- 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.
- as-regular-chain: **OPTIONAL** takes no arguments, and removes type filter line from the root chain generated by this header.
- address-family-override: **OPTIONAL** Takes one argument, and overrides the address family of the table. By default, the generator creates an nftables table with the address family based on nf_address_family header above. Only bridge override is supported at the moment (for example, `address-family-override bridge` will generate the following table: `table bridge filtering_policies {...}`).

#### Important: stateful firewall only

Expand Down
11 changes: 10 additions & 1 deletion policies/pol/sample_nftables.pol
Original file line number Diff line number Diff line change
Expand Up @@ -237,10 +237,19 @@ term awesome-term-as-regular-chain {

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
target:: nftables inet6 OUTPUT 300 noverbose base-chain-name multipleoptions-chain table-name multipleoptions-table as-regular-chain address-family-override bridge
}

term awesome-term-with-multiple-options {
comment:: "Awesomeness with multiple options at once."
action:: accept
}

header {
comment:: "nftables with bridge address family override"
target:: nftables mixed INPUT address-family-override bridge
}

term allow-anything-bridge {
action:: accept
}
54 changes: 49 additions & 5 deletions tests/lib/nftables_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
# limitations under the License.
"""Unittest for Nftables rendering module."""

import datetime
import re
from unittest import mock
from absl import logging
Expand Down Expand Up @@ -161,6 +160,24 @@ def __init__(self, in_dict: dict):
}
"""

HEADER_AF_OVERRIDE_1 = """
header {
target:: nftables mixed output address-family-override bridge
}
"""

HEADER_AF_OVERRIDE_2 = """
header {
target:: nftables inet input noverbose base-chain-name multipleoptions-chain table-name multipleoptions-table as-regular-chain address-family-override bridge
}
"""

HEADER_AF_OVERRIDE_3 = """
header {
target:: nftables inet6 forward address-family-override bridge noverbose base-chain-name multipleoptions-chain table-name multipleoptions-table as-regular-chain
}
"""

GOOD_HEADER_1 = """
header {
target:: nftables inet6 INPUT
Expand Down Expand Up @@ -641,12 +658,13 @@ def testPortsAndProtocols(self, af, proto, src_p, dst_p, icmp_type, expected):

@parameterized.parameters(
'chain_name input 0 inet extraneous_target_option',
'ip6 OUTPUT 300 400 mixed input', # pylint: disable=implicit-str-concat
'ip6 OUTPUT 300 400 mixed input', # pylint: disable=implicit-str-concat
'ip forwarding',
'ip7 0 spaghetti',
'ip6 prerouting',
'chain_name',
'',
'mixed output address-family-override ipv8',
)
def testBadHeader(self, case):
logging.info('Testing bad header case %s.', case)
Expand All @@ -661,7 +679,7 @@ def testBadHeader(self, case):
def testVerboseHeader(self, header_to_use, expected_output):
pol = policy.ParsePolicy(header_to_use + GOOD_TERM_1, self.naming)
data = nftables.Nftables(pol, EXP_INFO)
for _, _, _, _, _, _, verbose, _, _, _ in data.nftables_policies:
for _, _, _, _, _, _, verbose, _, _, _, _ in data.nftables_policies:
result = verbose
self.assertEqual(result, expected_output)

Expand Down Expand Up @@ -735,7 +753,7 @@ def testOverridePolicyHeader(self):
HEAD_OVERRIDE_DEFAULT_ACTION + GOOD_TERM_1, self.naming
)
data = nftables.Nftables(pol, EXP_INFO)
for _, _, _, _, _, default_policy, _, _, _, _ in data.nftables_policies:
for _, _, _, _, _, default_policy, _, _, _, _, _ in data.nftables_policies:
result = default_policy
self.assertEqual(result, expected_output)

Expand Down Expand Up @@ -955,7 +973,9 @@ def testRulesetGeneratorAF(self, policy_data: str, expected_inet: str):
)
for header, terms in nft.policy.filters:
filter_options = header.FilterOptions('nftables')
nf_af, nf_hook, _, _, verbose, _, _, _ = nft._ProcessHeader(filter_options)
nf_af, nf_hook, _, _, verbose, _, _, _, _ = nft._ProcessHeader(
filter_options
)
for term in terms:
term_object = nftables.Term(term, nf_af, nf_hook, verbose)

Expand Down Expand Up @@ -1095,5 +1115,29 @@ def testRulesetGenerator_RaisesOnMultipleIntegers(self):
)
)

@parameterized.parameters(
(HEADER_AF_OVERRIDE_1, True),
(HEADER_AF_OVERRIDE_2, True),
(HEADER_AF_OVERRIDE_3, True),
(GOOD_HEADER_1, False),
(GOOD_HEADER_2, False),
(GOOD_HEADER_3, False),
)
def testAfOverrideHeader(self, header_to_use, expected_af_bridge):
pol = policy.ParsePolicy(header_to_use + GOOD_TERM_1, self.naming)
data = nftables.Nftables(pol, EXP_INFO)
self.assertLen(data.nftables_policies, 1)
for _, _, _, _, _, _, _, _, _, _, af_override in data.nftables_policies:
if expected_af_bridge:
self.assertEqual(af_override, 'bridge')
else:
self.assertIsNone(af_override)

if expected_af_bridge:
self.assertIn('table bridge', str(data))
else:
self.assertNotIn('table bridge', str(data))


if __name__ == '__main__':
absltest.main()

0 comments on commit a230c93

Please sign in to comment.