[Bug 1758] New: Design flaw in chain traversal
bugzilla-daemon at netfilter.org
bugzilla-daemon at netfilter.org
Sat Jul 13 14:21:58 CEST 2024
https://bugzilla.netfilter.org/show_bug.cgi?id=1758
Bug ID: 1758
Summary: Design flaw in chain traversal
Product: nftables
Version: unspecified
Hardware: x86_64
OS: Ubuntu
Status: NEW
Severity: major
Priority: P5
Component: kernel
Assignee: pablo at netfilter.org
Reporter: hadmut at danisch.de
Hi,
when trying to migrate my old iptables/ufw rules to nftables I ran into a
subtile change in semantics which I do consider as a major design flaw. Let me
cite your documentation
https://wiki.nftables.org/wiki-nftables/index.php/Configuring_chains which says
NOTE: If a packet is accepted and there is another chain, bearing the same hook
type and with a later priority, then the packet will subsequently traverse this
other chain. Hence, an accept verdict – be it by way of a rule or the default
chain policy – isn’t necessarily final. However, the same is not true of
packets that are subjected to a drop verdict. Instead, drops take immediate
effect, with no further rules or chains being evaluated.
which is a design flaw.
Formerly, with iptables, there was one INPUT, OUTPUT, FORWARD chain, and
different tasks for different applications were implemented with subchains,
that were jumped into one after the other. All three final decisions accept,
reject, and drop were final and terminated processing, i.e. were reliable.
But since nftables invented the hooks where chains are registered in order to
keeps different things apart, the logic is broken.
Let me explain this.
I followed common examples to write a simple firewall ruleset to protect a
machine to get into the nftables style (and not just copying my old rules), and
(a shortened excerpt just do demonstrate the problem) was something like
table inet firewall {
set allowed_interfaces {
type ifname
elements = { "lo" }
}
set allowed_protocols {
type inet_proto
elements = { icmp, icmpv6 }
}
set allowed_tcp_dports {
type inet_service
elements = { ssh }
}
chain allow {
ct state established,related accept
meta l4proto @allowed_protocols accept
iifname @allowed_interfaces accept
tcp dport @allowed_tcp_dports accept
}
chain input {
type filter hook input priority filter + 10;
policy accept
jump allow
reject with icmpx type port-unreachable
}
}
which, at a first glance, worked like expected. But then I noticed that the
virtual guest machine in LXD virtualization cannot resolve DNS queries anymore.
The reason: LXD itself installs a ruleset like (again, just an excerpt to show
the problem)
table inet lxd {
chain in.lxdbr0 {
type filter hook input priority filter; policy accept;
iifname "lxdbr0" tcp dport 53 accept
iifname "lxdbr0" udp dport 53 accept
iifname "lxdbr0" icmp type { destination-unreachable,
time-exceeded, parameter-problem } accept
iifname "lxdbr0" udp dport 67 accept
iifname "lxdbr0" icmpv6 type { destination-unreachable,
packet-too-big, time-exceeded, parameter-problem, nd-router-solicit,
nd-neighbor-solicit, nd-neighbor-advert, mld2-listener-report } accept
iifname "lxdbr0" udp dport 547 accept
}
}
which becomes effectively useless and effectless, once my firewall ruleset is
loaded, since the accepted packages are then sent through my firewall ruleset
and thus rejected by the reject command. There is no way for me to cleanly
respect LXD tables, I would have to repeat all statements in my ruleset, and
thus continuously monitor LXD rulesets to copy its rules into mine.
Rules like
iifname "lxdbr0" udp dport 53 accept
are completely useless here, since the have the very same effect as if they
just would not exist: The packet is accepted within the chain, and then sent
through the next chain (i.e. my firewall ruleset).
As a consequence, any accept statement becomes completely useless and has no
effect in a ruleset (i.e. chains in a table) without a reject/drop, since an
accept only can override a reject/drop in (oder under) the very same chain, but
not against other chains.
As a result, I do not see a clean way to both have my machine protected and
have LXD running correctly while keeping the rules apart. Under iptables, this
was working.
My impression is, that this construction was made with constructive rules in
midn only, i.e. allow each program to manage its own tables for NAT, mangling,
tunneling, and things like that, but not with security and the final "reject
anything that has not been permitted yet" rule.
That's a major design flaw.
A clean solution (which is common in other applications with cascaded black-,
and whitelists) would be to have a fourth terminal action besides accept,
reject, drop, i.e. "proceed", probably as a packet destination and as a chain
policy, and as a default value of the policy. "proceed" would mean "go on with
the next chain/table", while accept, reject, drop have to be final and strictly
terminal. That's a common practice.
Therefore, the current practice to send an already accepted packet to the next
chain the same way as if it hadn't been accepted is design flaw and sets the
security on risk, since people who do not deeply understand the problem and are
not willing/able to copy all rulesets into the last one feel urged to omit
security reject rules in order to keep things up and running.
--
You are receiving this mail because:
You are watching all bug changes.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.netfilter.org/pipermail/netfilter-buglog/attachments/20240713/d93a5d31/attachment.html>
More information about the netfilter-buglog
mailing list