Skip to content

Commit 72b542e

Browse files
TheLinuxGuyCapirca Team
authored andcommitted
Nftables ensure no duplicate rules inside an individual chain can exist. Skip IPv4 ICMP loop on mixed family on ICMPv6 only terms.
PiperOrigin-RevId: 504633020
1 parent ae1b95b commit 72b542e

File tree

2 files changed

+68
-3
lines changed

2 files changed

+68
-3
lines changed

capirca/lib/nftables.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ def MapICMPtypes(self, af, term_icmp_types):
133133
Function is used inside PortsAndProtocols.
134134
135135
Args:
136+
af: address family.
136137
term_icmp_types: ICMP types keywords.
137138
138139
Returns:
@@ -462,6 +463,7 @@ def RulesetGenerator(self, term):
462463
list of strings. Representing a ruleset for later formatting.
463464
"""
464465
term_ruleset = []
466+
unique_term_ruleset = []
465467
comment = 'comment '
466468

467469
# COMMENT handling.
@@ -488,11 +490,19 @@ def RulesetGenerator(self, term):
488490
self.term.destination_port,
489491
self.term.icmp_type)
490492

493+
# Do not render ICMP types if IP family mismatch.
494+
if ((address_family == 'ip6' and 'icmp' in self.term.protocol) or
495+
(address_family == 'ip' and ('icmpv6' in self.term.protocol)
496+
or 'icmp6' in self.term.protocol)):
497+
continue
491498
# TODO: If verdict is not supported, drop nftable_rule for it.
492499
nftable_rule = self.GroupExpressions(address_list, proto_and_ports, opt,
493500
verdict, comment)
494501
term_ruleset.extend(nftable_rule)
495-
return term_ruleset
502+
# Ensure that chain statements contain no duplicates rules.
503+
unique_term_ruleset = [
504+
i for n, i in enumerate(term_ruleset) if i not in term_ruleset[:n]]
505+
return unique_term_ruleset
496506

497507
def _AddressClassifier(self, address_to_classify):
498508
"""Organizes network addresses according to IP family in a dict.
@@ -771,7 +781,7 @@ def _ConfigurationDictionary(self, nft_pol):
771781
for (header, base_chain_name, nf_af, nf_hook, nf_priority,
772782
filter_policy_default_action, verbose, child_chains) in nft_pol:
773783
base_chain_comment = ''
774-
# Add max character checking on header.comment later if needed.
784+
# TODO: If child_chain ruleset is empty don't store term.
775785
if verbose:
776786
base_chain_comment = header.comment
777787
nftables[nf_af][base_chain_name] = {

tests/lib/nftables_test.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"""Unittest for Nftables rendering module."""
1515

1616
import datetime
17+
import re
1718
from unittest import mock
1819
from absl import logging
1920
from absl.testing import absltest
@@ -129,7 +130,24 @@ def __init__(self, in_dict: dict):
129130
}
130131
"""
131132

132-
# TODO(gfm): Noverbose testing once Term handling is added.
133+
HEADER_MIXED_AF = """
134+
header {
135+
target:: nftables mixed output
136+
}
137+
"""
138+
139+
HEADER_IPV4_AF = """
140+
header {
141+
target:: nftables inet output
142+
}
143+
"""
144+
145+
HEADER_IPV6_AF = """
146+
header {
147+
target:: nftables inet6 output
148+
}
149+
"""
150+
133151
HEADER_NOVERBOSE = """
134152
header {
135153
target:: nftables mixed output noverbose
@@ -154,6 +172,13 @@ def __init__(self, in_dict: dict):
154172
}
155173
"""
156174

175+
DENY_TERM = """
176+
term deny-term {
177+
comment:: "Dual-stack IPv4/v6 deny all"
178+
action:: deny
179+
}
180+
"""
181+
157182
ESTABLISHED_OPTION_TERM = """
158183
term established-term {
159184
protocol:: udp
@@ -541,6 +566,36 @@ def testSkippedTerm(self, termdata, messagetxt):
541566
record = ctx.records[1]
542567
self.assertEqual(record.message, messagetxt)
543568

569+
@parameterized.parameters(
570+
(HEADER_MIXED_AF + ICMPV6_TERM, 'ip protocol icmp'),
571+
(HEADER_IPV4_AF + ICMPV6_TERM, 'meta l4proto icmpv6'),
572+
(HEADER_IPV6_AF + ICMP_TERM, 'ip protocol icmp'),
573+
)
574+
def testRulesetGeneratorICMPmismatch(self, pol_data, doesnotcontain):
575+
# This test ensures that ICMPv6 only term isn't rendered in a mixed header.
576+
nftables.Nftables(
577+
policy.ParsePolicy(pol_data, self.naming), EXP_INFO)
578+
nft = str(
579+
nftables.Nftables(
580+
policy.ParsePolicy(pol_data, self.naming), EXP_INFO))
581+
self.assertNotRegex(nft, doesnotcontain)
582+
583+
def testRulesetGeneratorUniqueChain(self):
584+
# This test is intended to verify that on mixed address family rulesets
585+
# no duplicate instance of a simple deny is rendered within a mixed chain.
586+
expected_term_rule = 'drop comment "Dual-stack IPv4/v6 deny all"'
587+
count = 0
588+
nftables.Nftables(
589+
policy.ParsePolicy(HEADER_MIXED_AF + DENY_TERM, self.naming), EXP_INFO)
590+
nft = str(
591+
nftables.Nftables(
592+
policy.ParsePolicy(
593+
HEADER_MIXED_AF + DENY_TERM, self.naming), EXP_INFO))
594+
matching_lines = re.findall(expected_term_rule, nft)
595+
for match in matching_lines:
596+
count += 1
597+
self.assertEqual(count, 1)
598+
544599
@parameterized.parameters(
545600
(GOOD_HEADER_1 + GOOD_TERM_2, 'inet6'),
546601
(GOOD_HEADER_1 + ICMPV6_TERM, 'inet6'),

0 commit comments

Comments
 (0)