[firewall] opt-in in-process nftables backend via libnftnl/libmnl - #3325
[firewall] opt-in in-process nftables backend via libnftnl/libmnl#3325LorbusChris wants to merge 5 commits into
Conversation
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
There was a problem hiding this comment.
Code Review
This pull request introduces an in-process firewall management system using nftables (via libnftnl and libmnl), replacing the legacy shell-script-based iptables and ipset configuration. It adds a new FirewallManager class to handle ingress filtering, NAT44 masquerading, and ND-proxy redirects directly within otbr-agent. The changes include build system updates, Dockerfile modifications, and integration into the Application and BackboneAgent components. Unit tests for the new firewall logic are also provided. I have no feedback to provide.
The posix platform firewall maintains the ingress allow/deny prefix sets consumed by the OTBR forward-ingress filter. Until now it drove ipset (`otbr-ingress-*`) via the `ipset` binary, which pairs with the legacy ip6tables/ipset shell firewall. openthread/ot-br-posix#3325 proposes moving the OTBR firewall in-process onto nftables (libnftnl/libmnl), where the ingress chain and its named sets live in an `inet` table owned by otbr-agent. ipset and nftables named sets are different kernel subsystems and cannot be shared, so with such a backend the ingress-prefix producer must target nftables directly. Add OPENTHREAD_POSIX_CONFIG_FIREWALL_NFTABLES_ENABLE (default 0). When set, UpdateIpSets repopulates the `ingress_deny_src` / `ingress_allow_dst` sets of the inet table OPENTHREAD_POSIX_CONFIG_NFT_TABLE ("otbr") using a single atomic `nft -f` transaction (flush both sets + re-add), which provides the same atomicity the ipset path got from its '-swap' swap. The table and sets must already exist; this producer only updates their contents. The legacy ipset path is unchanged and remains the default. NOTE: draft — compiled-by-inspection only; needs an on-device smoke test (nft transaction syntax, set element formatting, and startup ordering vs. the in-process table creation). Assisted-By: Claude Fable 5
The posix platform firewall maintains the ingress allow/deny prefix sets consumed by the OTBR forward-ingress filter. Until now it drove ipset (`otbr-ingress-*`) via the `ipset` binary, which pairs with the legacy ip6tables/ipset shell firewall. openthread/ot-br-posix#3325 proposes moving the OTBR firewall in-process onto nftables (libnftnl/libmnl), where the ingress chain and its named sets live in an `inet` table owned by otbr-agent. ipset and nftables named sets are different kernel subsystems and cannot be shared, so with such a backend the ingress-prefix producer must target nftables directly. Add OPENTHREAD_POSIX_CONFIG_FIREWALL_NFTABLES_ENABLE (default 0). When set, UpdateIpSets repopulates the `ingress_deny_src` / `ingress_allow_dst` sets of the inet table OPENTHREAD_POSIX_CONFIG_NFT_TABLE ("otbr") using a single atomic `nft -f` transaction (flush both sets + re-add), which provides the same atomicity the ipset path got from its '-swap' swap. The table and sets must already exist; this producer only updates their contents. The legacy ipset path is unchanged and remains the default. NOTE: draft — compiled-by-inspection only; needs an on-device smoke test (nft transaction syntax, set element formatting, and startup ordering vs. the in-process table creation). Assisted-By: Claude Fable 5
cff8a27 to
b3248cd
Compare
d63e109 to
6fe5a24
Compare
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces an in-process nftables firewall backend for the OpenThread Border Router (otbr-agent), replacing the legacy shell-based ipset/ip6tables setup. It adds a FirewallManager and an INftables interface with a concrete implementation using libnftnl and libmnl. The review feedback recommends adding an AbortBatch() method to the INftables interface and its implementation to safely discard half-built batches and prevent transaction leaks. Additionally, the feedback highlights a state desynchronization bug in EnableNdProxyRedirect where mNdRuleHandle is cleared before a successful commit, and suggests replacing an unsafe memset on Ip6Prefix with C++ value-initialization.
|
Thanks — the batch-leak finding is correct and is now fixed, with a couple of adjustments. What was actually broken. Every One correction:
Tests. Added two regression tests and checked they actually fail without the fix — on a tree with only the behavioural changes reverted: 17 passed / 2 failed; with them: 19/19.
CI. Also added a All fixes are folded into the commits that introduced the code, so the history stays bisectable. |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces an in-process nftables firewall backend (OTBR_NFTABLES) as an opt-in alternative to the legacy shell-based ipset/ip6tables firewall. It adds a new FirewallManager and Nftables wrapper using libnftnl and libmnl to manage ingress filtering, NAT44 masquerading, and ND-proxy redirects in-process. Feedback on the changes highlights several robustness issues: ignoring the return value of mnl_nlmsg_batch_next could lead to heap corruption on batch overflows, a potential null-pointer dereference crash exists in Application::UpdateIngressPrefixes if the OpenThread instance is uninitialized, and EnableNat44Masquerade lacks a check to prevent invalid rules when called with an empty upstream interface name.
|
Thanks — all three are addressed.
Rather than repeat the check ten times, added a small VerifyOrExit(mnl_nlmsg_batch_next(mBatch), errno = ENOBUFS, error = OTBR_ERROR_ERRNO);I deliberately did not auto-flush and reset on overflow, which is the other way libmnl supports: that would split one logical change across two kernel transactions and lose the atomicity the batch exists to provide. Failing the whole operation — and, with the abort path added earlier, discarding the batch — keeps it all-or-nothing.
Empty upstream interface name. Added Firewall suite is 20/20 with the nftables backend enabled, and all changes are folded into the commits that introduced the code. |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces an opt-in, in-process nftables firewall backend (OTBR_NFTABLES) for the OpenThread Border Router, replacing the legacy shell-based ipset/ip6tables firewall with a robust C++ implementation using libnftnl and libmnl. The review feedback highlights several critical improvements for robustness and portability, including: preventing potential null pointer dereferences during expression allocation, ensuring proper 4-byte alignment of stack-allocated Netlink buffers to avoid bus errors on alignment-sensitive architectures like MIPS, using the IfNameToWire helper for consistency, and adopting pkg_check_modules in CMake to better support cross-compilation environments.
|
All six addressed. Unchecked expression allocation (high). Correct. The eleven otbrError AddExpr(struct nftnl_rule *aRule, struct nftnl_expr *aExpr)
{
VerifyOrExit(aExpr != nullptr, errno = ENOMEM, error = OTBR_ERROR_ERRNO);
nftnl_rule_add_expr(aRule, aExpr);
...Combined with the abort path added earlier, a failed allocation now unwinds the whole operation and discards the batch instead of building a partial rule. Netlink buffer alignment. Correct, and worth taking seriously here: OpenWrt targets MIPS, which is exactly where an under-aligned buffer cast to
One deviation: the suggestion's natural companion, $<$<BOOL:${LIBNFTNL_LIBRARY_DIRS}>:-L$<JOIN:${LIBNFTNL_LIBRARY_DIRS}," -L">>Verified with |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces an opt-in, Linux-only in-process nftables firewall backend (OTBR_NFTABLES) for the OpenThread Border Router, replacing the legacy ipset/ip6tables shell scripts. It adds a new FirewallManager and Nftables implementation using libnftnl and libmnl, integrates them into the main application, and updates build configurations, Dockerfiles, and OpenWrt packages. A high-severity memory leak was identified in Nftables::BeginBatch where a failure in AdvanceBatch() leaks the allocated mnl_nlmsg_batch structure because mnl_nlmsg_batch_stop is not called during cleanup.
|
Correct, and thanks — that leak was one I introduced in the previous round. Before Fixing it surfaced a second problem on the same exit path, which is the more serious of the two: Both are fixed by cleaning up only what the call itself allocated: exit:
// Only clean up what this call allocated. When the failure is that a batch
// is already open, mInBatch is still set and the buffer belongs to that
// batch, so it must be left alone.
if (error != OTBR_ERROR_NONE && !mInBatch)
{
AbortBatch();
}
return error;
One caveat: this is not covered by the unit tests. They drive Folded into the commit that introduces the backend. |
|
/gemini review |
|
Both taken.
Rather than adding the missing line,
Worth saying plainly: that hole was avoidable. Firewall suite 22/22, full build clean, clang-format clean, folded into the commit that introduces the backend. CI on the previous head came back green for |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces an opt-in, Linux-only in-process nftables firewall backend (OTBR_NFTABLES) for the OpenThread Border Router, which manages ingress filtering, NAT44 masquerading, and ND-proxy redirection directly within otbr-agent using libnftnl and libmnl. This modern implementation replaces the legacy shell-script-based ipset/ip6tables setup. The review feedback highlights two safety improvements: explicitly zero-initializing the prefix structure in Application::UpdateIngressPrefixes to prevent uninitialized memory issues, and adding a bounds check on the length parameter in MakeBitwiseMask to avoid potential out-of-bounds reads.
|
One taken, one declined.
Those cannot all hold, and in fact none of the three states differ: class Ip6Prefix
{
public:
Ip6Prefix(void) { Clear(); }
...
void Clear(void) { memset(reinterpret_cast<void *>(this), 0, sizeof(*this)); }
Leaving it as Firewall suite 22/22, build clean, clang-format clean, folded into the commits that introduce the code. |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces an opt-in, in-process nftables firewall backend (OTBR_NFTABLES) for the OpenThread Border Router, replacing the legacy shell-based ipset/ip6tables firewall. This change adds libnftnl and libmnl as dependencies and implements a new FirewallManager and Nftables wrapper to manage ingress filtering and NAT44 masquerading directly within otbr-agent. Build scripts, Dockerfiles, OpenWrt configurations, and tests have been updated to support this new backend. Feedback on the implementation suggests reducing code duplication in src/firewall/nftables_impl.cpp by reusing the ApplyPrefixMask helper function within AttachPrefixInterval.
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces an opt-in, in-process nftables firewall backend (OTBR_NFTABLES) for the OpenThread Border Router, replacing the legacy shell-based ipset/ip6tables setup. The implementation adds FirewallManager and Nftables classes to manage firewall rules and NAT44 masquerading directly within otbr-agent, alongside corresponding build, Docker, and OpenWrt configuration updates. Feedback on the changes suggests utilizing target_link_directories instead of manually constructing -L linker flags with generator expressions in CMake to align with modern CMake best practices.
|
Folded a testability change into the first two commits. The netlink socket now sits behind an Six cases in
Each was checked by reverting the corresponding fix and confirming the matching test fails; all six do. Counts are 28 tests with |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces an in-process nftables firewall backend for the OpenThread Border Router (otbr-agent), replacing the legacy shell-script-based ipset/ip6tables setup. It adds a new FirewallManager and Nftables implementation using libnftnl and libmnl to manage ingress filtering, NAT44 masquerading, and ND-proxy redirection directly in-process, along with updates to the build configuration, bootstrap scripts, and unit tests. The review feedback suggests improving memory management by using MakeUnique instead of direct new allocation in Nftables constructors, and simplifying FirewallManager by removing unused handle variables where rule handles are not needed.
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces an opt-in, in-process nftables firewall backend (OTBR_NFTABLES) for the OpenThread Border Router, replacing the legacy shell-based ipset/ip6tables setup. It adds a new firewall module featuring FirewallManager and Nftables implementations, integrates them into the main Application lifecycle, and updates build configurations, Dockerfiles, and OpenWrt packages to support the new libmnl and libnftnl dependencies. The review feedback highlights a potential vulnerability where mSocket could be dereferenced when null in the Send, Recv, and Drain methods of MnlNetlinkSocket, and suggests adding appropriate null checks to prevent crashes.
Add an in-process nftables backend for the OTBR firewall: an INftables abstraction plus a concrete implementation that talks netlink directly via libnftnl/libmnl (no shelling out). It supports table/chain/set/rule management in atomic kernel batches: interval sets for IPv6 prefixes (two-element start/INTERVAL_END encoding), rule builders for the ingress filter and NAT44 masquerade (with meta nfproto guards so IPv4 and IPv6 rules never cross over in the inet table), ECHO-reply parsing to capture kernel-assigned rule handles, and set-element CRUD. The backend is behind a new opt-in build flag, OTBR_NFTABLES (default OFF): the legacy ipset/ip6tables shell firewall remains the default. Building with -DOTBR_NFTABLES=ON requires libnftnl and libmnl. The interval-set encoding and atomic flush-and-refill batches were validated against live kernels (6.8 and 7.1, nftables 1.0.9/1.1.6). Assisted-By: Claude Fable 5
FirewallManager owns the OTBR firewall policy on top of INftables: the `inet otbr` table lifecycle (Init tears down leftovers from a prior run; Deinit removes the table), the forward_ingress chain with the ingress_deny_src/ingress_allow_dst interval sets (same rule semantics as the legacy ip6tables chain), NAT44 masquerade via a fwmark, and the ingress-set CRUD, including ReplaceIngressPrefixes() which swaps the full contents of both sets in one atomic kernel batch so there is never a half-populated window. The gmock-based unit tests pin the chain/set/rule construction sequences and argument values against a mock backend. Assisted-By: Claude Fable 5
When built with OTBR_NFTABLES=ON, otbr-agent owns the OTBR firewall in-process: on RCP init it creates the `inet otbr` table, installs the ingress filter and NAT44 masquerade, and populates the ingress prefix sets from Thread network data, refreshing them on OT_CHANGED_THREAD_NETDATA (the in-process replacement for the OpenThread posix platform firewall's ipset producer). Because the agent produces the sets itself, OT_FIREWALL is forced OFF for nftables builds so the OT-core ipset producer does not also run. The table is torn down on shutdown. Firewall install failures are fatal (SuccessOrDie): a border router that cannot install its ingress filter must not silently forward unfiltered, matching the legacy scripts, which `die`d when the firewall/NAT services failed to start. Assisted-By: Claude Fable 5
Make the legacy ipset/ip6tables firewall and iptables NAT44 skip themselves when the in-process nftables backend is in use, so the two never install duplicate or conflicting rules: - Docker run/finish scripts honor an OTBR_NFTABLES env flag: with it set they skip the ipset/ip6tables ingress setup, the iptables NAT44 masquerade, and their teardown (the agent owns them). - OpenWrt: the otbr-firewall init script and the host script/setup firewall + otbr-nat44 services skip installation when OTBR_NFTABLES=ON / OTBR_NFTABLES=1. - bootstrap installs libnftnl/libmnl (build deps for the opt-in backend) alongside the still-default ipset. - Document the alternate OpenWrt dependency set (libmnl, libnftnl, kmod-nft-core, kmod-nft-nat) for OTBR_NFTABLES=ON builds. Legacy ipset/ip6tables remains the default in every path. Assisted-By: Claude Fable 5
Nothing in CI configured -DOTBR_NFTABLES=ON, so the libnftnl/libmnl backend was never compiled there. Only the FirewallManager policy layer and its tests were covered, because those targets are built unconditionally; nftables_impl.cpp was not. Add a check job that builds with the backend enabled and runs the test suite, so the netlink code is compiled and the firewall tests run in the configuration they describe. script/bootstrap already installs libnftnl/libmnl, so no bootstrap change is needed. Assisted-By: Claude Opus 4.8
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces an opt-in, in-process nftables firewall backend (OTBR_NFTABLES) for the OpenThread Border Router (otbr-agent), replacing the legacy shell-based ipset/ip6tables firewall setup. It implements a new FirewallManager and Nftables backend using libnftnl and libmnl to manage the Thread ingress filter, NAT44 masquerading, and ND-proxy redirects directly within the agent process. The changes also include updates to build configurations, Docker scripts, OpenWrt packages, and bootstrap scripts to support the new dependencies, alongside a comprehensive suite of unit tests using a scriptable fake netlink socket. No review comments were provided, so there is no additional feedback to address.
|
Friendly ping for a CI approval / first review pass. Two notes that may help planning:
The backend has been running in production on an OpenWrt (Turris Omnia) border router. |
Summary
Adds an opt-in in-process nftables backend for the OTBR firewall, speaking
netlink directly via libnftnl/libmnl. Addresses #1675, which suggested talking to
the kernel directly via libmnl/libnftnl rather than calling binaries from C++.
The legacy ipset/ip6tables shell firewall remains the default. Building with
-DOTBR_NFTABLES=ON(defaultOFF) switches the whole firewall in-process:otbr-agent owns a single
inet otbrtable, including producing the ingressprefix sets from Thread network data (refreshed on
OT_CHANGED_THREAD_NETDATA),replacing the OpenThread posix platform
firewall.cppipset producer(
OT_FIREWALLis forcedOFFfor such builds).Ingress rule semantics match the legacy ip6tables chain 1:1. Set updates are a
single atomic kernel batch (flush both sets + re-add), so there is never a
half-populated window. Firewall install failures are fatal, matching the legacy
scripts'
die-on-failure: a border router that cannot install its ingressfilter must not silently forward unfiltered.
Commits (independently reviewable)
add INftables interface and libnftnl/libmnl backendINftablesinterface + libnftnl/libmnl backend + theOTBR_NFTABLESflag (default OFF). Focus: netlink/batch correctness.add FirewallManager policy layer with unit testsFirewallManagerpolicy layer + gmock unit tests.Focus: table lifecycle, rule/set construction, atomic replace.
wire the nftables backend into otbr-agent (opt-in)otbr-agent wiring: install on init, produce ingress setsfrom netdata, tear down on shutdown;
OT_FIREWALL=OFFfor nftablesbuilds. Focus: producer parity with OT-core's ipset producer.
gate legacy firewall install on the nftables opt-inDeployment gating: Docker run/finish, OpenWrt init andhost
script/setupskip the legacy firewall/NAT44 when the in-processbackend is in use, so the two stacks never double-install. Focus: every
path defaults to legacy; opt-in is explicit everywhere.
build the nftables backend in CIAnftables-checkjob that configures-DOTBR_NFTABLES=ONand runs the test suite. Focus: the netlink backend isactually compiled and exercised in CI.
Opting in per platform
-DOTBR_NFTABLES=ON, andOTBR_NFTABLES=1in theenvironment for
script/setup(skips the legacyotbr-firewall/otbr-nat44services).--build-arg OTBR_OPTIONS="-DOTBR_NFTABLES=ON"and run with-e OTBR_NFTABLES=1(skips the legacy blocks in the s6 scripts).-DOTBR_NFTABLES=ONplus a manualDEPENDSswap inetc/openwrt/openthread-br/Makefile— drop+ip6tables +ipset +iptables-mod-extra, add+libmnl +libnftnl +kmod-nft-core +kmod-nft-nat.The Makefile carries a comment rather than a switch, since the package recipe
has no visibility of the CMake option. The
otbr-firewallinit script isdropped automatically by
src/openwrt/CMakeLists.txt.Validation
The interval-set element encoding (two-element start/
INTERVAL_END) and theatomic flush-and-refill batches were validated against live kernels twice
(6.8/nftables 1.0.9 in a VM, 7.1/nftables 1.1.6 in a container), including
verifying that
nftrenders the libnftnl-populated sets as the expectedprefixes. An end-to-end on-device test with a real RCP (join, ingress
filtering, NAT44) is still outstanding.
Notes for reviewers
nd_proxy.cpp([dua] remove DUA routing feature #3360/[build] remove all references to DUA #3377) while this PR was in flight. The firewall library still carries
the ND-proxy NFQUEUE redirect API (
EnableNdProxyRedirect/dua_prerouting), currently unconsumed — kept for when ND-proxy returns;happy to drop it if preferred.
OTBR_NFTABLES=OFFthe installed firewall/NAT behaviour is unchanged frommain— every new runtime path isOTBR_ENABLE_NFTABLES-gated. The builddoes differ slightly even when off:
script/bootstrapand the Dockerfileinstall the libnftnl/libmnl headers unconditionally, and the
otbr-firewallpolicy layer plus its gmock test binary are always compiled.
nftables_impl.cppand the libnftnl/libmnl link are what the option actually gates.
nat_prerouting/nat_postrouting/nat_forwardchains are installed onlywhen a backbone/infra interface is configured; that interface is the masquerade
upstream, matching the legacy
OT_INFRA_IF.nftables-checkCI job builds with-DOTBR_NFTABLES=ONand runs the testsuite, so
nftables_impl.cppis compiled and the firewall tests run in theconfiguration they describe. The
FirewallManagertests also run in the defaultbuild, since that target is unconditional.
🤖 Generated with Claude Code