NetBSD-Users archive

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index][Old Index]

Re: npf on a router: configuration issues



On Tue, Apr 08, 2025 at 07:51:30PM -0400, Greg Troxel wrote:
> Christoph Badura <bad%bsd.de@localhost> writes:
> > On Tue, Apr 01, 2025 at 08:57:38AM -0400, Greg Troxel wrote:
> >>    b) Is it really meant that "if a packet does not match any defined
> >>       group, then -- and only then -- will it be processed by the special
> >>       group default (which is default NOT in quotes, as a keyword not a
> >>       name)"?

> I have been reading code, and finding it difficult.  I now think (from a
> combination of code and docs)
> 
>   groups are processed in order, with a group skipped if direction or
>   interface of the packet mismatches the group definition
> 
>   If a rule in a group matches the packet, even if not a "final" rule or
>   not, then when the end of the group is reached, that is the rule that
>   controls the result, and the rest of the groups are not evaluated.
>   Same for a "final" rule, except that it skips the rest of the group
>   rules, as always for final.
>   
>   If after running the rules in a group, none of them matched
>   (implicitly, because they were all too specific and there was no
>   default, or there were no rules, like a group with pass out but no in
>   rules), then processing continues with the next group.

Or, shorter, if a packet passes through a group without a pass/block
verdict it is processed by the next group.

> >>    b) Assuming so, and I want to
> >>         - block packets heading to the host to most ports, except for a few
> >>         - allow outbound transit packets without regard to blocked ports
> >>       how do I do this?  It looks like I have to have my block rules
> >>       narrowed by $ifaddrs and run on each interface, making groups
> >>       awkward.  Surely there must be a better way, as I don't think my
> >>       intent is unusual.
> >
> > You don't actually define what the "host" is.  It seems you mean the
> > router's addresses.  You are correct in that you have to narrow the rules
> > down to ifaddrs(iface) for all interfaces (except lo0 which you can
> > short-circuit).
> 
> I have a mental model where the router part of the system forwards
> packets but does not receive or transmit them.  And that there is a host
> attached to the router, which can receive/transmit packets, accept ssh
> connections, etc.  I am thinking of being able to filter on "is this
> packet coming from the router part to the host part".
> 
> 
>                             host
>                             |
>                   pass up   | pass down
>                             |
>   wm0 ---- pass in -------router---- pass out ------- wm1

Linux/nftables has that formalized as:

                     application layer
                            ^ |
                     input  | |  output
                            | v
  wm0 ---- ingress ------ ip layer ---- egress ------- wm1
			 forwarding

And you explicitly attach rules for the "input", "output", and "forwarding"
hooks.

It's actually quite a bit more complicated, but this catches the gist of
it for our functionality.

https://wiki.nftables.org/wiki-nftables/index.php/Netfilter_hooks

But NPF doesn't work that way.  It is more tied to interfaces.

https://rmind.github.io/npf/intro.html#processing explicitly states that
NPF doesn't hook the forwarding path.

> What I want as an example is
> 
>    For pass up, block all, and allow a few ports.  Just like you would
>    want on a host which is not a router.
> 
>    For packets that come in wm0 and go out wm1, or in wm1 and out wm0,
>    don't filter.

I think you can't do that because packets are filtered before a routing
descision has been made.

>    Probably, keep state on pass up for incoming ssh and keep state on
>    pass down to allow replies to connections from the host.
> 
> A more likely/complicated is to replace the second point with
> 
>    For packets that come in wm0 and go out wm1, don't filter, but keep
>    state on wm1 output.  For packets that come in wm1, default block
>    unless state.

Except for the "come in on wm0 and go out wm1" part this is easily done.
As documented, state processing is done before rule inspection.
So a "pass stateful out on wm1" should do the trick.

>    (Plus, packets from host going out wm1 keep state and allow matching.)

Easily done in nftables but with NPF I believe you have to be explicit
with ifaddrs(iface) for all relevant interfaces.

> I think you're suggesting accepting that host is mixed in and
> 
>    pass in on wm0
>    block in on wm0 to ifaddrs(iface0)
>    pass in on wm0 to ifaddrs(iface0) port $allowed_ports
> 
> so that all packets can be forward, but the 'pass up' leg is default block.

Yes.  Except for the case where you have more than 2 interfaces and you
are dealing with applications that bind to all interfaces addresses (e.g.
ntpd and bind).

> I think one should be able to specify different rules for "connect to
> the router" and "what the router will pass".

Yes, sometimes that makes writing certain rules simpler.  But sometimes
the "interfaces based approach" makes writing certain rules simpler, too.
Also, sometimes it's nice to match on topology (external interface,
internal interface(s)), sometimes it's nice to match on IP addresses.

E.g. on nftables I have:

table inet filter {
	chain input {
		type filter hook input priority 0; policy drop;
		...
		iifname ppp0 goto external-traffic-common
		...
	}
	chain forward {
		type filter hook forward priority 0; policy drop;
		...
		iifname ppp0 goto external-traffic-common
		...
	}
	chain external-traffic-common {
		icmp type echo-request counter accept
		icmpv6 type echo-request counter accept
		...
	}
}

There, at least, I can factor out common rules into sub-chains.

I'm not saying that nftables is the gold standard.  It has a lot of ugly
warts IMNSHO.  But it does get a bunch of things right.

> >>    Why would I want to or not want to use stateful processing for
> >>    inbound UDP?  Inbound ICMP echo?
> >
> > For inbound UDP so that e.g. PORT UNREACHABLE from the hosts can be
> > returned, if you find that useful (e.g. for traceroute).
> 
> That makes sense, if the unreachable is otherwise blocked.

It's not a strong case, IMHO.  I meant to write in the earlier mail that I
mainly use outgoing state for UDP.  E.g. it is nice for outgoing NTP to
pool servers where it's unknown what the destination addresses are.  And,
after syncing up there's only one exchange of packets every 20 minutes or
so.  Handling that in the state table is nice.

> >> 5) The NAT examples almost all suggest a group with "pass stateful out
> >>    final".
> >> 
> >>    Is there any reason there needs to be such a group/rules if the rules
> >>    that exist anyway on the outbound interface have "pass stateful out"?
> >
> > Well, certain types of NAT require state, but
> > https://rmind.github.io/npf/nat.html says it does stateful filtering
> > automatically if required.  That would imply that "pass stateful" is not
> > required at all.  I haven't experimented with that, however.  However,
> > adding "stateful" to a rule that also is affected by NAT can make it
> > explicit that you expect state to be maintained and does no harm.
> 
> This remains unclear, and I think we need to experiment, read code, and
> document.

Frankly, I think it's absolutely bonkers to use a paket filter with gaping
holes in the documented semantics.

I think we need to go back and
a) clearly and unambigously document the existing behaviour.

b) go back to the drawing board and design a system that we actually
want to use.  That includes looking at the other packet filters commonly
in use and researching the strong and week points and come up with a
desing that takes as many of the strong points and as little of the weak
points as possible.

But that is another discussion.

--chris


Home | Main Index | Thread Index | Old Index