One should clearly understand what Pf's "lists" are. They aren't a part of ruleset that gets loaded into kernel in fact, but macros instead. It means they're expanded during preprocessing phase of rules loading — contrary to tables. Keeping that in mind saves one from "shooting in own foot".
Let's now see what you're trying to do:
block out quick proto { tcp, udp } from any to ! <my_table> port != { 66 80 }
- Block immediately If
- it's TCP or it's UDP
- AND it's destined to
- IPs that aren't in
my_table
- AND ports, that aren't in the list
As I've told you lists' items would get expanded into a separate rule each. And this would break the logic I've just explained: only the first port of the list would be treated right, if you block immediately (quick
) it obviously means no second checking — "not allowed port, ok, blocking it".
Mastering firewall ruleset you'd better keep things as simple as possible. Well, actually it's not only for firewalls — it's general and programming common sense. Double negations, exception of exceptions aren't that simple to follow.
So what are your options then? — You can take a look at the different angle: what and when do you want to pass?
# If it's destined to IP in <my_table> -- pass:
pass out quick proto { tcp, udp } from any to <my_table>
# ElseIf it's to allowed ports -- pass:
pass out quick proto { tcp, udp } to port { 66 80 }
# And this point would be reached only if it wasn't to <my_table>
# and to some ports other than allowed ones:
block out quick proto { tcp, udp }
This ruleset is way more readable indeed. Moreover — macro expansion doesn't spoil its logic.
Of course, Pf has some other means for solving this very task but describing them all properly would make this answer way too lengthy.
!= { 66, 80 }
(it is a suggestion based on deduction, I never did it) – Prado Aug 29 '19 at 18:39