Skip to content

Commit ae1b95b

Browse files
TheLinuxGuyCapirca Team
authored andcommitted
Nftables ensure comments are attached to expected position in NFT syntax (after verdict or counter rather than standalone line).
PiperOrigin-RevId: 504134275
1 parent 3641027 commit ae1b95b

File tree

2 files changed

+51
-27
lines changed

2 files changed

+51
-27
lines changed

capirca/lib/nftables.py

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ def _OptionsHandler(self, term):
349349
else:
350350
return ''
351351

352-
def GroupExpressions(self, address_expr, pp_expr, options, verdict):
352+
def GroupExpressions(self, address_expr, pp_expr, options, verdict, comment):
353353
"""Combines all expressions with a verdict (decision).
354354
355355
The inputs are already pre-sanitized by RulesetGenerator. NFTables processes
@@ -362,11 +362,13 @@ def GroupExpressions(self, address_expr, pp_expr, options, verdict):
362362
pp_expr: pre-processed list of nftables protocols and ports.
363363
options: string value to append before verdict for NFT special options.
364364
verdict: action to take on resulting final statement (allow/deny).
365+
comment: term.comment string adhering to NFT limits.
365366
366367
Returns:
367368
list of strings representing valid nftables statements.
368369
"""
369370
statement = []
371+
statement_with_comment = []
370372
if address_expr:
371373
for addr in address_expr:
372374
if pp_expr:
@@ -388,8 +390,13 @@ def GroupExpressions(self, address_expr, pp_expr, options, verdict):
388390
statement.append(pstat + Add(options) + Add(verdict))
389391
else:
390392
# If no addresses or ports & protocol. Verdict only statement.
391-
statement.append((Add(options) + verdict))
392-
return statement
393+
statement.append((Add(options) + Add(verdict)))
394+
# Handling of comments should always be done after verdict statement.
395+
if comment != 'comment ':
396+
statement_with_comment.append(statement[0] + Add(comment))
397+
return statement_with_comment
398+
else:
399+
return statement
393400

394401
def _AddrStatement(self, address_family, src_addr, dst_addr):
395402
"""Builds an NFTables address statement.
@@ -455,12 +462,12 @@ def RulesetGenerator(self, term):
455462
list of strings. Representing a ruleset for later formatting.
456463
"""
457464
term_ruleset = []
465+
comment = 'comment '
458466

459467
# COMMENT handling.
460468
if self.verbose:
461-
term_ruleset.append(
462-
'comment ' + aclgenerator.TruncateWords(
463-
self.term.comment, Nftables.COMMENT_CHAR_LIMIT))
469+
comment += aclgenerator.TruncateWords(
470+
self.term.comment, Nftables.COMMENT_CHAR_LIMIT)
464471
# OPTIONS / LOGGING / COUNTERS
465472
opt = self._OptionsHandler(term)
466473
# STATEMENT VERDICT / ACTION.
@@ -483,7 +490,7 @@ def RulesetGenerator(self, term):
483490

484491
# TODO: If verdict is not supported, drop nftable_rule for it.
485492
nftable_rule = self.GroupExpressions(address_list, proto_and_ports, opt,
486-
verdict)
493+
verdict, comment)
487494
term_ruleset.extend(nftable_rule)
488495
return term_ruleset
489496

@@ -795,20 +802,22 @@ def __str__(self):
795802
# First time we comment it out so .nft file is human-readable.
796803
nft_config.append(
797804
TabSpacer(8, '#' + ' '.join(base_chain_dict[item]['comment'])))
798-
# Second time so 'nft list ruleset' keeps the comment in memory.
799-
nft_config.append(
800-
TabSpacer(
801-
8, 'comment ' +
802-
aclgenerator.TruncateWords(
803-
base_chain_dict[item]['comment'], self.COMMENT_CHAR_LIMIT)))
804805
nft_config.append(
805806
TabSpacer(
806807
8, 'type filter hook %s priority %s; policy %s;' %
807808
(base_chain_dict[item]['hook'],
808809
base_chain_dict[item]['priority'],
809810
base_chain_dict[item]['policy'])))
810-
# stateful firewall: allow reply traffic.
811-
nft_config.append(TabSpacer(8, 'ct state established,related accept'))
811+
# Add policy header comment after stateful firewall rule.
812+
if base_chain_dict[item]['comment']:
813+
nft_config.append(TabSpacer(8, 'ct state established,related accept'
814+
+ Add('comment') +
815+
Add(aclgenerator.TruncateWords(
816+
base_chain_dict[item]['comment'],
817+
self.COMMENT_CHAR_LIMIT))))
818+
else:
819+
# stateful firewall: allows reply traffic.
820+
nft_config.append(TabSpacer(8, 'ct state established,related accept'))
812821
# Reference the child chains with jump.
813822
for child_chain in base_chain_dict[item]['rules'][item].keys():
814823
nft_config.append(TabSpacer(8, 'jump %s' % child_chain))

tests/lib/nftables_test.py

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2022 Google Inc. All Rights Reserved.
1+
# Copyright 2023 Google Inc. All Rights Reserved.
22
#
33
# Licensed under the Apache License, Version 2.0 (the "License");
44
# you may not use this file except in compliance with the License.
@@ -193,6 +193,21 @@ def __init__(self, in_dict: dict):
193193
}
194194
"""
195195

196+
COMMENT_TERM = """
197+
term good-icmpv6-type {
198+
comment:: "This term has a comment"
199+
protocol:: tcp
200+
action:: accept
201+
}
202+
"""
203+
204+
NOCOMMENT_TERM = """
205+
term good-icmpv6-type {
206+
protocol:: tcp
207+
action:: accept
208+
}
209+
"""
210+
196211
GOOD_TERM_1 = """
197212
term good-term-1 {
198213
action:: accept
@@ -294,17 +309,16 @@ def testCreateAnonymousSet(self, input_data, expected):
294309
self.assertEqual(result, expected)
295310

296311
@parameterized.parameters(
297-
([
298-
'ip6 saddr 2606:4700:4700::1111/128 ip6 daddr { 2001:4860:4860::8844/128, 2001:4860:4860::8888/128'
299-
], ['tcp sport 80 tcp dport 80'],
300-
'ct state { ESTABLISHED, RELATED } log prefix "combo_cnt_log_established" counter',
301-
'accept', [
302-
'ip6 saddr 2606:4700:4700::1111/128 ip6 daddr { 2001:4860:4860::8844/128, 2001:4860:4860::8888/128 tcp sport 80 tcp dport 80 ct state { ESTABLISHED, RELATED } log prefix "combo_cnt_log_established" counter accept'
303-
]),)
304-
def testGroupExpressions(self, address_expr, porst_proto_expr, opt, verdict,
305-
expected_output):
312+
(['ip6 saddr 2606:4700:4700::1111/128 ip6 daddr { 2001:4860:4860::8844/128, 2001:4860:4860::8888/128 }'], ['tcp sport 80 tcp dport 80'],'ct state { ESTABLISHED, RELATED } log prefix "combo_cnt_log_established" counter',
313+
'accept', 'comment ', ['ip6 saddr 2606:4700:4700::1111/128 ip6 daddr { 2001:4860:4860::8844/128, 2001:4860:4860::8888/128 } tcp sport 80 tcp dport 80 ct state { ESTABLISHED, RELATED } log prefix "combo_cnt_log_established" counter accept'
314+
]),
315+
(['ip daddr 8.8.8.8/32'], ['tcp sport 53 tcp dport 53'],'ct state new','accept', 'comment "this is a term with a comment"', ['ip daddr 8.8.8.8/32 tcp sport 53 tcp dport 53 ct state new accept comment "this is a term with a comment"'])
316+
)
317+
def testGroupExpressions(self, address_expr, porst_proto_expr, opt,
318+
verdict, comment, expected_output):
306319
result = self.dummyterm.GroupExpressions(address_expr, porst_proto_expr,
307-
opt, verdict)
320+
opt, verdict, comment)
321+
308322
self.assertEqual(result, expected_output)
309323

310324
def testDuplicateTerm(self):
@@ -530,11 +544,12 @@ def testSkippedTerm(self, termdata, messagetxt):
530544
@parameterized.parameters(
531545
(GOOD_HEADER_1 + GOOD_TERM_2, 'inet6'),
532546
(GOOD_HEADER_1 + ICMPV6_TERM, 'inet6'),
547+
(GOOD_HEADER_1 + COMMENT_TERM, 'mixed'),
533548
(GOOD_HEADER_2 + GOOD_TERM_2, 'mixed'),
534549
(GOOD_HEADER_3 + GOOD_TERM_2, 'inet'),
535550
(GOOD_HEADER_3 + ICMP_TERM, 'inet'),
536551
)
537-
def testRulesetGenerator(self, policy_data: str, expected_inet: str):
552+
def testRulesetGeneratorAF(self, policy_data: str, expected_inet: str):
538553
self.naming.GetNetAddr.return_value = TEST_IPS
539554
self.naming.GetServiceByProto.return_value = ['22']
540555

0 commit comments

Comments
 (0)