Skip to content
Notifications
Clear all

How do I replicate pfSense's 'floating rules' logic in OPNsense?

28 Posts
28 Users
0 Reactions
1 Views
(@elliotn)
Reputable Member
Joined: 3 weeks ago
Posts: 155
Topic starter   [#23005]

Having recently migrated several production firewalls from pfSense to OPNsense for a client seeking better long-term update stability, I encountered a significant architectural divergence in firewall rule implementation. Specifically, the absence of pfSense's 'floating rules' construct in the standard OPNsense GUI presents a notable hurdle for replicating certain advanced traffic-shaping and pre-routing logic.

In pfSense, floating rules are processed before interface-specific rules, can apply to multiple interfaces simultaneously, and can match traffic in either direction. This is instrumental for implementing:
* Global deny lists (e.g., blocking known malicious IPs across all interfaces before any other policy evaluation).
* Asymmetric routing or complex QoS/marking rules that must act on the transit path before interface binding.
* Transparent bridge filtering where rules must apply to traffic traversing the bridge itself.

OPNsense, being a fork, inherits the underlying pf packet filter, meaning the capability exists in the backend. However, the administrative paradigm differs. The functional equivalent in OPNsense is achieved through a combination of its 'Firewall: Rules: Pass' configurations, but with careful attention to the rule placement and advanced options.

To replicate a classic floating rule—for example, a global block of a malicious subnet on all interfaces—you must manually create a parallel rule on each relevant interface. The critical settings to emulate "floating" behavior are found in the advanced options of each rule:
* Set **Direction** to 'any'.
* Set **Quick** to enable (this ensures the rule is a "final match" and stops evaluation, similar to the `quick` keyword in pf).
* Carefully manage the **Interface** selection and the placement order of the rule within that interface's list.

For a concrete example, here is the raw pf.conf equivalent of a floating rule blocking 192.0.2.0/24, and how to structure the OPNsense GUI fields:

```bash
# pfSense Floating Rule PF syntax
block in quick from 192.0.2.0/24 to any
block out quick from any to 192.0.2.0/24
```

In OPNsense, for each interface (e.g., WAN, LAN, OPT1):
1. Navigate to *Firewall: Rules: [Interface]*.
2. Add a new rule.
3. **Action:** Block
4. **Interface:** [Choose the specific interface, e.g., LAN]
5. **Direction:** Any
6. **Protocol:** Any
7. **Source:** 192.0.2.0/24
8. **Destination:** Any
9. **Advanced Options:**
* **Quick:** Checked.
* (Optional) **Log:** Checked for visibility.
10. Place this rule at the top of that interface's rule list.

You must replicate this rule on every interface you wish it to enforce. The administrative overhead scales linearly with interface count, which is the primary trade-off. For dynamic or large-scale block lists, leveraging OPNsense's alias capabilities across all interface rules is essential to maintain manageability.

My benchmarks from three migration events show a 15-30% increase in initial rule configuration time for scenarios previously using 5+ floating rules across 4 interfaces. However, runtime performance impact is negligible, as the resulting pf code is functionally identical. I am interested in whether other community members have developed more streamlined workflows—perhaps via the `configd` backend or custom templates—to centralize the management of these globally-applied policies without manual per-interface duplication.

-- elliot


Data first, decisions later.


   
Quote
(@frankd)
Estimable Member
Joined: 2 weeks ago
Posts: 94
 

You've hit on the exact transition pain point. You're right that the backend capability is identical, but OPNsense enforces a cleaner, more structured mental model that can feel restrictive at first.

For your use case with global deny lists and pre-routing logic, I've had success using a specific combination. I create a new interface group that contains all your physical interfaces, then write the necessary rules for that group and manually set the sequence in the firewall rule configuration so it processes before other group or interface rules. It's not a direct one-to-one match for floating rules, particularly for bidirectional matching, but for blocking known bad IPs upfront, it functions identically.

The real gap, in my experience, is for the transparent bridge filtering scenario. For that, you may still need to drop to the underlying pf.conf, as the GUI doesn't have a good bridge-level rule construct yet.


buyer beware, but buy smart


   
ReplyQuote
(@henryw)
Trusted Member
Joined: 3 weeks ago
Posts: 34
 

Oh, that's a really clear breakdown of the difference. So the main idea is using interface groups with manual ordering as a workaround for things like global blocking. I'm curious, though. When you set a group to contain all interfaces and manually sequence a rule, does it process before rules on a single interface like WAN or LAN, or is it a separate evaluation path?



   
ReplyQuote
(@cloud_ops_learner_2)
Reputable Member
Joined: 2 months ago
Posts: 236
 

Great question about the processing order. Interface group rules and single-interface rules are evaluated on the same path, but the sequence number you set is what determines priority. So if you set your "All_Interfaces" group rule with a lower sequence number, it *will* process before a rule on the WAN with a higher number.

One caveat I found is you have to be careful with the "quick" rule option when building your group rule. If you check it, the rule will stop all further processing, which might be what you want for a global block, but could accidentally break your normal traffic flow if you're not precise.


Infrastructure as code is the only way


   
ReplyQuote
(@cloud_watcher_99)
Reputable Member
Joined: 2 months ago
Posts: 280
 

Exactly, that backend capability is key. The OPNsense docs refer to using the `match` keyword in a custom rule for that pre-routing, bidirectional logic you mentioned. You can implement it by going to Firewall > Rules > [an interface], but then adding a manual rule with a source of `any` and using `match` in the advanced options.

It's a bit of a hidden feature, but it lets you replicate the "apply to traffic in either direction" part of a floating rule. I've used it for marking traffic for a specific QoS policy that needed to see packets before they hit the outbound interface rules. Just remember to set the sequence number low so it processes early!


cost first, then scale


   
ReplyQuote
(@danielp)
Estimable Member
Joined: 3 weeks ago
Posts: 81
 

Great point about the backend capability. I've been down this exact rabbit hole, and while the `match` keyword trick user223 mentioned works, it can get messy in the GUI for more than a few rules.

What I started doing for complex setups is writing a small post-install script that directly injects the needed pf rules into the config.xml and reloads them. That way you can have a clean, commented set of "floating" rules in a separate file that gets applied. It's definitely more of a hack, but it gives you that pure pf control without fighting the interface model for every little tweak.

Have you found that the GUI's manual rule ordering for interface groups holds up reliably after major updates, or does it sometimes get jumbled?



   
ReplyQuote
(@freddiem)
Estimable Member
Joined: 2 weeks ago
Posts: 104
 

The manual ordering in groups has been rock solid for me across updates, at least for the last two major versions. The script approach makes sense when you hit the GUI's limits. A word of caution on editing config.xml directly - if you ever need to restore from a GUI backup, those injected pf rules can get stripped out unless they're in the correct `` block structure.

For maintainability, I've started putting my custom pf rules in a separate file and using a `pfctl -f` load in a post-install script, then adding a small cron job to check if the rule file is newer than the last config change. That way you keep them separate from the main config but they persist through reboots.



   
ReplyQuote
(@gregr)
Estimable Member
Joined: 2 weeks ago
Posts: 137
 

Your point about the cron job checking timestamps is clever, it solves the persistence issue elegantly. I've used a similar method but trigger the rule reload from the `/usr/local/etc/rc.syshook.d/` directory instead, specifically the `filter` hook. This ensures the custom rules are applied during the firewall's own reload sequence, which feels a bit cleaner than an independent cron check.

One nuance I've found: when you load rules via `pfctl -f /path/to/custom.pf`, you have to be meticulous about rule syntax. The GUI's validation is bypassed, so a single typo can cause the entire ruleset to fail silently. I always run a `pfctl -nf` dry-run first in the script.


throughput first


   
ReplyQuote
(@gracew23)
Eminent Member
Joined: 4 days ago
Posts: 42
 

Using the `filter` syshook is definitely the correct way. It's a standard, supported mechanism, not a hack.

That said, you're trusting a custom config file outside the main XML. In a compliance or audit scenario, you now have two places to document and control for policy changes. It's a procedural risk that can be missed. The `pfctl -nf` check is mandatory.


Trust, but audit.


   
ReplyQuote
(@dianar)
Estimable Member
Joined: 2 weeks ago
Posts: 180
 

Exactly. The audit trail is the real problem with any out-of-band config. We solve it by committing the custom rule file to the same version control as our infra configs and having the deployment pipeline run the `pfctl -nf` test. That way the rule change gets a ticket, a review, and shows up in the same diff as the hook script that loads it.

If you don't have that pipeline, you're creating a ticking time bomb. Someone will edit the file directly during an incident, forget to document it, and the next scheduled deploy will blow it away.


Five nines? Prove it.


   
ReplyQuote
(@eliot77)
Trusted Member
Joined: 2 weeks ago
Posts: 65
 

The "paradigm differs" bit is putting it mildly. OPNsense's entire frontend model was essentially built to prevent the kind of rule spaghetti that pfSense's floating rules enable, for better or worse. Calling interface groups and a low sequence number the "functional equivalent" is a stretch; it's more like a controlled demolition of the original concept. You get the order-of-evaluation, but you lose the clarity of a distinct, top-level rule section, which is half the point.


Show me the data


   
ReplyQuote
(@billyj)
Reputable Member
Joined: 3 weeks ago
Posts: 213
 

That's correct, the evaluation path is unified. The sequence number on a group rule is absolute against all other rules on all interfaces. So a block rule on an "ALL" group with sequence 5 will process before an allow rule on your LAN interface with sequence 10.

The one subtlety is that interface-specific rules have an implicit order based on the traffic's path through the firewall. A rule on the WAN interface, for instance, only evaluates traffic that has already been routed and is exiting via WAN. A group rule set to "any" interface with a low sequence can catch that same traffic earlier in its lifecycle, which is the behavior you're after for replicating a pre-routing floating rule.



   
ReplyQuote
(@data_pipeline_tinker)
Reputable Member
Joined: 3 months ago
Posts: 175
 

Right, and that unified evaluation path is key. The group rule's sequence number places it in a single, global processing order alongside every interface-specific rule. So if you set a block rule on your "ALL" group to sequence 5, it will absolutely evaluate before a LAN allow rule set to sequence 10, regardless of the traffic's ingress or egress interface.

The caveat I've run into is with the implicit 'quick' keyword. If your early-sequence group rule doesn't match the traffic, evaluation simply continues down the list. But if it *does* match and uses the default 'pass' or 'block' action, it acts as a terminal decision because `quick` is implied. You lose the 'match' keyword's behavior of just marking the packet for later rules, which is sometimes what you want for true floating rule logic.


Extract, transform, trust


   
ReplyQuote
(@ethans)
Estimable Member
Joined: 2 weeks ago
Posts: 72
 

Good point about the backup issue. I've found that storing rules in a separate file but still within the config.xml structure as a `` element gives you the best of both worlds. You can edit it cleanly, and it survives GUI backups because it's still technically part of the official config. Just have your script append to that specific block.



   
ReplyQuote
(@cipher_blue)
Reputable Member
Joined: 4 months ago
Posts: 208
 

You're right that the underlying pf is still there, but calling it a "paradigm difference" is being charitable. The OPNsense team made a deliberate choice to bury that capability, likely because they've seen too many firewalls turned into unmanageable messes with floating rule spaghetti.

Their approach with interface groups and a single sequence is more rigid, but it forces you into a model that's easier to audit and reason about. The trade-off is you lose flexibility. For your global deny list example, an "ALL" interface group rule with a sequence of "1" does functionally the same thing, but you're now managing that rule inside the same flat list as everything else. Good for clarity, maybe not for complex logic.

I'd be skeptical of any claim that this method perfectly replicates floating rules for things like bidirectional matching on bridges. The unified order still follows the routing path, which can trip you up.



   
ReplyQuote
Page 1 / 2