Rich Rule Priorities
Recently firewalld gained support for a priority field in the rich rule syntax. It allows fine grained control over rich rules and their execution order. This enables using rich rules in ways not possible before.
Why is it needed?
One issue with current rich rules is that they are organized based on their rule action. Log always occurs before deny. Deny always occurs before allow. This has led to confusion from users as this implicitly reorders rules. It also made it impossible to add a catch-all rich rule to deny traffic.
More information on this can be found in the firewalld.richlanguage man page in the section “Information about logging and actions”.
What does it look like?
The syntax modifications add a new priority field. This can be any number between -32768 and 32767, where lower numbers have higher precedence. This range is large enough to allow automatic rule generation from scripts or other entities.
Example:
# firewall-cmd --add-rich-rule='rule priority=1234 service name="mdns" allow'
Based on the priority rules are organized into different chains.
- If priority < 0, the rule goes into a chain with the suffix _pre.
- If priority > 0, the rule goes into a chain with the suffix _post.
- If priority == 0, the rule goes into a chain ( _log, _deny, _allow ) based on their action. This is the same behavior as rich rules before priority support.
Inside these sub-chains rules are sorted according to their priority value. If they have the same priority value, then it’s undefined in what order they will be executed.
Putting it all together a zone’s set of chains now looks like below:
# nft list chain inet firewalld filter_IN_public
table inet firewalld {
chain filter_IN_public {
jump filter_IN_public_pre
jump filter_IN_public_log
jump filter_IN_public_deny
jump filter_IN_public_allow
jump filter_IN_public_post
meta l4proto { icmp, ipv6-icmp } accept
}
}
A couple key points from this layout:
- _pre can occur before normal log rules.
- _post execution always occurs after firewalld’s other primitives (services, ports, etc). This makes it a good place for catch-all type rules.
- _pre and _post chains may contain rich rules with any type of action (accept, deny, log, audit, etc)
Examples (use cases)
Below are some examples, but they don’t even scratch the surface of what’s possible now that rich rules support arbitrary ordering.
Log all traffic not caught by other rules
Using a very low precedence rich rule you can log all traffic that has not yet been denied or accepted. This is useful to flag any unexpected traffic. It can also be a way to implement the zone level equivalent to –log-denied.
# firewall-cmd --add-rich-rule='rule priority=32767 log prefix="UNEXPECTED: " limit value="5/m"'
This results in the following:
# nft list chain inet firewalld filter_IN_public_post
table inet firewalld {
chain filter_IN_public_post {
log prefix "UNEXPECTED: " limit rate 5/minute
}
}
Special policy for subset of traffic
To mimic a policy for only a subset of source addresses you can use a low precedence rule.
# firewall-cmd --add-rich-rule='rule family="ipv4" priority=32767 source address="10.1.1.0/24" reject'
This results in the following:
# nft list chain inet firewalld filter_IN_public_post
table inet firewalld {
chain filter_IN_public_post {
ip saddr 10.1.1.0/24 reject
}
}
Allow a service for a subset of sources
This example allows a service for a subset of sources, then logs and denies it for everyone else.
# firewall-cmd --add-rich-rule='rule family="ipv4" priority=-100 source address="10.1.1.0/24" service name="ssh" accept'
# firewall-cmd --add-rich-rule='rule priority=-99 service name="ssh" log'
# firewall-cmd --add-rich-rule='rule priority=-98 service name="ssh" reject'
This results in the following:
# nft list chain inet firewalld filter_IN_public_pre
table inet firewalld {
chain filter_IN_public_pre {
ip saddr 10.1.1.0/24 tcp dport 22 ct state new,untracked accept
tcp dport 22 ct state new,untracked log
tcp dport 22 ct state new,untracked reject
}
}
Compatibility
To maintain compatibility rich rules that have a priority == 0 or an absent priority will behave as they’ve done in the past. They’ll be sorted into the _log, _deny, and _allow chains based on their action.
When will they be available?
Rich rules with priority support will be available in the next minor firewalld release, which will most likely be v0.7.0. However, the feature may be backported to distributions that do that sort of thing.
firewalld 0.6.3 release
A new release of firewalld, version 0.6.3, is available.
This is a bug fix only release.
- nftables: fix reject statement in “block” zone
- shell-completion: bash: don’t check firewalld state
- firewalld: fix –runtime-to-permanent if NM not in use.
- firewall-cmd: sort –list-protocols output
- firewall-cmd: sort –list-services output
- tests/regression/icmp_block_in_forward_chain: fix for newer nftables version
- command: sort services/protocols in –list-all output
- services: add audit
- nftables: fix rich rule log/audit being added to wrong chain
- tests/firewall-cmd: rich rule coverage for simple source/dest match
- nftables: fix destination checks not allowing masks
- firewall/core/io/*.py: Let SAX handle the encoding of XML files (#395)
- fw_zone: expose _ipset_match_flags()
- tests/firewall-cmd: exercise multiple interfaces and zones
- fw_transaction: On clear zone transaction, must clear fw and other zones
- Fix translating labels (#392)
- tests/functions: fix macro to dump ipset
Source available here:
- Tarball: firewalld-0.6.3.tar.gz
- SHA256: 039ad56ea6d6553aadf33243ea5b39802d73519e46a89c80c648b2bd1ec78aeb
- Complete changelog on github: 0.6.2 to 0.6.3
firewalld 0.6.2 release
A new release of firewalld, version 0.6.2, is available.
This is a bug fix only release.
- nftables: fix log-denied with values other than “all” or “off”
- fw_ipset: raise FirewallError if backend command fails
- ipset: only use “-exist” on restore
- fw_ipset: fix duplicate add of ipset entries
- *tables: For opened ports/protocols/etc match ct state new,untracked
- nftables: fix rich rules ports/protocols/source ports not considering ct state
- ports: allow querying a single port added by range
- fw_zone: fix services with multiple destination IP versions
- fw_zone: consider destination for protocols
- firewall/core/fw_nm: nm_get_zone_of_connection should return None or empty string instead of False
- nftables: fix rich rule audit log
- fw: if failure occurs during startup set state to FAILED
- services/high-availability: open all 8 ports used knetd/corosync
Source available here:
- Tarball: firewalld-0.6.2.tar.gz
- SHA256: 76ef7ed41caf67204dc80e1f2640176a481c72cadc30488492b22e45b3757c54
- Complete changelog on github: 0.6.1 to 0.6.2
firewalld 0.5.5 release
A new release of firewalld, version 0.5.5, is available.
This is a bug fix only release.
- fw: if startup fails on reload, reapply non-perm config that survives reload
- fw: If direct rules fail to apply add a “Direct” label to error msg
- firewall/core/fw_nm: nm_get_zone_of_connection should return None or empty string instead of False
- update translations
Source available here:
- Tarball: firewalld-0.5.5.tar.gz
- SHA256: 0b04d4d13f8b5ea6f971247f253c7620d0078fd8df465fcea0f8e44bd91c73ac
- Complete changelog on github: 0.5.4 to 0.5.5
Testsuite Primer
Over the past two major releases firewalld has seen vast improvements to its testsuite. This post will discuss how to run the testsuite, how to debug a failure, and finally we’ll go through an exercise of adding a new test case. The main target is for current and future contributors to firewalld. However, since firewalld’s testsuite utilizes autotest some of the knowledge gained here may also carry over to other projects.
Running the testsuite
Running the testsuite is very simple. To start build the code just as you would if installing from source.
$ git clone https://github.com/firewalld/firewalld
$ cd firewalld
$ ./autogen.sh
$ ./configure
$ make
Then run the testsuite using the check
make target. This needs to be run as
root
.
# make check
These tests are non-destructive to the host running them and there is no need to stop the host’s running firewalld instance. They are run inside of network namespaces (containers) which allows numerous benefits; reliability, non-destructive to the host, and they can be run in parallel.
At the time of writing firewalld has 112 test groups - most of which include multiple tests. As such, the testsuite takes a long time to run. The good news is you can speed things up by running them in parallel.
To run test groups in parallel pass -j4
to autotest via the TESTSUITEFLAGS
variable.
# make check TESTSUITEFLAGS="-j4"
Running the testsuite against the host installed firewalld
Firewalld’s testsuite also provides an installcheck
make target. This is
useful for running tests against the hosts installed version of firewalld.
The check
target runs against the executables built in the source tree.
This is very important for development as it allows you to run a newer
testsuite against an older version of firewalld. As we’ll find later on in this
post, this can be used to leverage test driven development.
To run the testsuite against the installed firewalld
# make installcheck
Debugging a failed test case
Inevitably a test will fail. Whether it’s a bug in firewalld, a permission issue, or a compatibility issue, you’ll have to debug the problem. There are a couple of ways to inspect a failure. The most straight forward way is to view the testsuite log.
To view the testsuite log for failed test number 42
# vi ./src/tests/testsuite.dir/042/testsuite.log
Alternatively, you can enable the verbose flag to cause the testsuite to dump to standard output.
# make check TESTSUITEFLAGS="42 -v"
To enable firewalld’s debug output, you can use the -d
flag.
# make check TESTSUITEFLAGS="42 -v -d"
The test numbers passed to TESTSUITEFLAGS
are very flexible. It will accept
individual numbers or ranges.
For example this is also valid
# make check TESTSUITEFLAGS="42 1-5 110 13-14 17"
Writing a new test case
In this example we’ll follow a test driven development style to fix a real
world firewalld bug. We’ll be using Red Hat bugzilla 1404076 for this
exercise. This bug occurs when a port range is opened using the --add-port
option. --query-port
fails to return the expected result if querying a
single port within the range.
Basic test layout
All of the tests follow the same basic layout. Since this is a new test case in
response to an existing bug we’ll add it to the set of regression tests in
src/tests/regression
.
At the absolute minimum your new test should look like this
FWD_START_TEST([test description])
...
FWD_CHECK([some command])
...
FWD_END_TEST
The testsuite makes heavy use of m4 macros (FWD_START_TEST
, FWD_CHECK
,
etc.) to simplify test creation. m4 is a macro language which autotest uses to
generate the testsuite script.
A consequence of the macro magic is most tests will be run multiple times. Once for each firewall backend; nftables and iptables. This ensures that both backends are tested equally.
The new test case
We start by creating a new test that reproduces the issue from the bugzilla report.
Here is our new test case
src/tests/regression/rhbz1404076.at
:
FWD_START_TEST([query single port added with range])
dnl add a set of ports by range, then query a specific port inside that range.
FWD_CHECK([-q --add-port=8080-8090/tcp])
FWD_CHECK([-q --query-port=8085/tcp])
FWD_CHECK([-q --query-port=webcache/tcp]) dnl named port
FWD_CHECK([-q --query-port=8091/tcp], 1) dnl negative test
FWD_CHECK([-q --query-port=8085/udp], 1) dnl negative test
dnl same thing, but for permanent configuration.
FWD_CHECK([-q --permanent --add-port=8080-8090/tcp])
FWD_CHECK([-q --permanent --query-port=8085/tcp])
FWD_CHECK([-q --permanent --query-port=webcache/tcp]) dnl named port
FWD_CHECK([-q --permanent --query-port=8091/tcp], 1) dnl negative test
FWD_CHECK([-q --permanent --query-port=8085/udp], 1) dnl negative test
FWD_END_TEST
Note: dnl
is m4’s way of starting a comment.
This test is fairly exhaustive by including negative tests and named ports. This gives us even more confidence in our code changes.
To attach the new test case to the testsuite we need to append the new test to
src/tests/regression.at
.
$ vi src/tests/regression.at
[...]
m4_include([regression/rhbz1404076.at])
This new test case can be found upstream on github.
The commit is 3fb707228ced ("tests/regression: add coverage for rhbz 1404076")
.
Verifying the new test reproduces the issue
Now that the test has been added you can verify it reproduces the issue by
running it against the host’s firewalld using the installcheck
make target.
# make installcheck
[...]
regression (nftables)
[...]
67: query single port added with range FAILED (rhbz1404076.at:5)
[...]
regression (iptables)
[..]
110: query single port added with range FAILED (rhbz1404076.at:5)
Here we see the test case failed as expected for both firewall backends.
Fix the bug
The next step is to make code changes to fix the bug. This is out of scope for
this post, but for those interested the fix can be found on github.
The commit is 2925de324443 ("ports: allow querying a single added by range")
.
Verify the fix against the in-tree source code
Now that the test case has been written and code changes have been made we can
run the test case again, but this time against the source tree version. This
will verify our code changes fix the bug. Be sure to use the check
make
target this time.
# make check TESTSUITEFLAGS="67 110"
Once your new test case passes you should run the whole testsuite again to verify you didn’t introduce a regression.
# make check
If that was successful, then you’re ready to submit your changes upstream with a pull request! Once your pull request is submitted the testsuite will automatically be run again by travis-ci against multiple versions of python.
Conclusions
This post has shown how easy it is to get started with the firewalld testsuite. We hope it will encourage contributions from others while simultaneously improving firewalld’s quality. Additionally we hope it turns some users into active contributors!