Skip to content

Commit

Permalink
nftables: Support for base-chain-name, table-name, and `as-regula…
Browse files Browse the repository at this point in the history
…r-chain`.

* `base-chain-name`: 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`: 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.

While these options were previously added, they used a single argument in header options with `=` as the splitting character. This is an unsupported token. Symptom would be something resembling `Illegal character '=' on line 251`, but successful exit by `aclgen.py`.

Some tests have been added and some cleanup was performed as well.

PiperOrigin-RevId: 675308936
  • Loading branch information
ivucica authored and Capirca Team committed Sep 16, 2024
1 parent 89bf38f commit 40b3cbb
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 12 deletions.
44 changes: 36 additions & 8 deletions capirca/lib/nftables.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions doc/generators/nftables.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
40 changes: 40 additions & 0 deletions policies/pol/sample_nftables.pol
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
65 changes: 61 additions & 4 deletions tests/lib/nftables_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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()

0 comments on commit 40b3cbb

Please sign in to comment.