<html>
    <head>
      <base href="https://bugzilla.netfilter.org/" />
    </head>
    <body><table border="1" cellspacing="0" cellpadding="8">
        <tr>
          <th>Bug ID</th>
          <td><a class="bz_bug_link 
          bz_status_NEW "
   title="NEW - Design flaw in chain traversal"
   href="https://bugzilla.netfilter.org/show_bug.cgi?id=1758">1758</a>
          </td>
        </tr>

        <tr>
          <th>Summary</th>
          <td>Design flaw in chain traversal
          </td>
        </tr>

        <tr>
          <th>Product</th>
          <td>nftables
          </td>
        </tr>

        <tr>
          <th>Version</th>
          <td>unspecified
          </td>
        </tr>

        <tr>
          <th>Hardware</th>
          <td>x86_64
          </td>
        </tr>

        <tr>
          <th>OS</th>
          <td>Ubuntu
          </td>
        </tr>

        <tr>
          <th>Status</th>
          <td>NEW
          </td>
        </tr>

        <tr>
          <th>Severity</th>
          <td>major
          </td>
        </tr>

        <tr>
          <th>Priority</th>
          <td>P5
          </td>
        </tr>

        <tr>
          <th>Component</th>
          <td>kernel
          </td>
        </tr>

        <tr>
          <th>Assignee</th>
          <td>pablo@netfilter.org
          </td>
        </tr>

        <tr>
          <th>Reporter</th>
          <td>hadmut@danisch.de
          </td>
        </tr></table>
      <p>
        <div>
        <pre>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
<a href="https://wiki.nftables.org/wiki-nftables/index.php/Configuring_chains">https://wiki.nftables.org/wiki-nftables/index.php/Configuring_chains</a> 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.</pre>
        </div>
      </p>
      <hr>
      <span>You are receiving this mail because:</span>
      
      <ul>
          <li>You are watching all bug changes.</li>
      </ul>
    </body>
</html>