Skip to content

pfSense: translate a missing <protocol> and source ports exactly (#80) - #367

Merged
ARSH871-bot merged 1 commit into
mainfrom
fix/80-ports-that-would-widen
Sep 28, 2026
Merged

ARSH871-bot merged 1 commit into
mainfrom
fix/80-ports-that-would-widen

Conversation

@ARSH871-bot

Copy link
Copy Markdown
Owner

Closes #80. Stacked on #302, so it merges after it; retarget to main before #302 lands.

@patelankeet2, this also answers your 5 September question on #80 about Option C. There's no silent default left to disclose: every absent-<protocol> case is now pfSense's own documented behaviour, cited in the code.

#80's question, answered from pfSense's source

Senaka hadn't replied in 25 days (Discussion #294), and the answer turned out to be in pfSense itself. From github.com/pfsense/pfsense at 480a1c0626:

Source What it does
firewall_rules_edit.php:1195 Saving a rule as "any" unsets <protocol>. Absence is "any".
firewall_rules_edit.php:619 The GUI zeroes every port unless the protocol is tcp/udp.
filter.inc:4497 No <protocol> but a port set gives proto tcp.
filter.inc:3930 A port is emitted only for tcp/udp, and a source port is enforced.

So ip is right for the client's four protocol-less rules, and refusing them (Option B) would have been wrong. Two cases next to it were silently widened, and they're what this PR fixes:

pfSense rule pfSense enforces Before Now
no <protocol> + a port (hand-edited XML only) tcp, any port ip tcp
tcp/udp + source port that source port dropped eq N after the source
explicit any + port all protocols, port ignored ip ip (unchanged, now pinned)
icmp + source port port ignored icmp icmp (unchanged, now pinned)

Why translate rather than refuse

I first wrote this as refusals and threw it away. Refusing skips the rule, and @SamikaPerera's finding on #302 applies: skipping a pass rule narrows the model, so a "must be denied" policy can come back clean while the real firewall permits the traffic. Translating exactly is the only option that is wrong in neither direction. A source port that isn't one valid number (a range or an alias) still refuses, exactly as destination ports already do.

Verified in Batfish, not only in unit tests

The same synthetic file, converted by #302's head and by this branch, checked with testFilters:

                                                   old      new      pfSense
udp -> host covered by a protocol-less block rule  DENY     PERMIT   PERMIT
tcp -> same host, port 80                           DENY     DENY     DENY
tcp :2000 -> a rule allowing only source :1024      PERMIT   DENY     DENY
tcp :1024 -> same rule                              PERMIT   PERMIT   PERMIT
fileParseStatus                                     PASSED   PASSED

The first row was the dangerous direction: the old model reported traffic as blocked that the real firewall lets through.

Tests

  • 21 new, in tests/test_pfsense_convert.py. test_port_is_not_emitted_for_protocol_any is untouched: pfSense's source agrees with it.
  • 7 of 7 mutations caught, run with Batfish stopped and with it running. Mutations included restoring each old bug, treating explicit any like an absent protocol, keeping the port on the inferred-TCP line, skipping source-port validation, and writing a source port for icmp.
  • The explicit-any mutation was caught only by a test added for it. Before that test, nothing distinguished the two cases.
pytest tests/          1572 passed, 10 skipped
ruff (CI rule set)     all checks passed

CLAUDE.md §7 gets one dated update; the earlier entries saying #80 was untouched are left as history.

Not in this PR

pfSense's own source answers #80: it saves "any" by unsetting
<protocol>, so `ip` is correct for protocol-less rules. Two cases were
not, and both widened silently:

  no <protocol> + a port   pfSense enforces tcp, any port   was: ip
  tcp/udp + source port    pfSense enforces the port         was: dropped

Both are now written exactly (`tcp`, and `eq N` after the source).
Refusing them would skip pass rules, which narrows the model and can
turn a real policy violation into a clean result (#302). A source port
that is not one valid number refuses, as destination ports already do.

Verified in Batfish: every flow tested now matches pfSense; the old
conversion denied UDP a block rule never covered, and permitted a
source port a rule never allowed.
@ARSH871-bot
ARSH871-bot changed the base branch from fix/78-rule-level-resilience to main September 28, 2026 01:53
@ARSH871-bot ARSH871-bot reopened this Sep 28, 2026
@ARSH871-bot
ARSH871-bot merged commit 5a995a3 into main Sep 28, 2026
2 of 4 checks passed
@ARSH871-bot
ARSH871-bot deleted the fix/80-ports-that-would-widen branch September 28, 2026 02:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A rule with no <protocol> silently widens to 'ip'; a rule with no <type> refuses

1 participant