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:


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:


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:


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!