Documents
XML Addressing Semantics
XML Addressing Semantics
Type
Topic
Status
Published
Created
Feb 27, 2026
Updated
Apr 18, 2026
Created by
Dosu Bot
Updated by
Dosu Bot

XML Addressing Semantics#

XML Addressing Semantics in OPNsense firewall configurations defines a structured approach for specifying source and destination addresses in firewall rules. The system implements mutually exclusive addressing fields (<any>, <network>, and <address>) with a strict resolution priority (Network > Address > Any) that determines which field takes precedence when multiple values are present during configuration parsing.

The addressing semantics extend beyond simple field selection to include presence-based boolean negation through the <not/> element, port specifications for protocol-specific matching, and floating rule direction requirements. These patterns derive from OPNsense's native PHP implementation and have been formally documented in the opnDossier schema implementation.

Understanding these semantics is critical for configuration parsing, rule validation, and automated firewall management tools, as incorrect interpretation of field priorities or boolean semantics can result in security policy violations or unexpected rule behavior.

Mutually Exclusive Address Fields#

Field Types#

OPNsense firewall rules use three mutually exclusive address fields in source and destination blocks:

  • <any/>: Presence-based element indicating "match any address"
  • <network>: String value specifying an interface name (e.g., lan, wan, opt1, lanip, (self))
  • <address>: String value containing IP/CIDR notation or alias name

Per OPNsense semantics, only one of these fields should be present in a valid configuration. The Any field uses pointer type *string to distinguish XML element presence (non-nil) from absence (nil), since Go's encoding/xml package treats both empty elements and absent elements as empty strings when using plain string types.

XML Examples#

<!-- Any address (matches everything) -->
<source>
  <any/>
</source>

<!-- Network-based (matches interface subnet) -->
<source>
  <network>lan</network>
</source>

<!-- Address-based (matches specific IP/CIDR or alias) -->
<source>
  <address>192.168.1.0/24</address>
  <port>1024-65535</port>
</source>

Examples from testdata files demonstrate these patterns in production OPNsense configurations.

Field Resolution Priority#

Priority Order#

When resolving which address field to use, the EffectiveAddress() method implements a strict priority order:

  1. Network (highest priority)
  2. Address
  3. Any (lowest priority)
  4. Empty string (when no fields are set)

This priority matches OPNsense's legacyMoveAddressFields behavior as documented in configuration-reference.

Implementation#

The Source struct implements address resolution through the EffectiveAddress() method:

func (s Source) EffectiveAddress() string {
    if s.Network != "" {
        return s.Network
    }
    if s.Address != "" {
        return s.Address
    }
    if s.IsAny() {
        return constants.NetworkAny
    }
    return ""
}

The Destination struct implements identical logic, ensuring consistent address resolution across source and destination endpoints.

Helper Methods#

The IsAny() method checks for the presence of the <any> element:

func (s Source) IsAny() bool {
    return s.Any != nil
}

The Equal() method provides semantic equality comparison:

func (s Source) Equal(other Source) bool {
    if (s.Any != nil) != (other.Any != nil) {
        return false
    }
    return s.Network == other.Network &&
        s.Address == other.Address &&
        s.Port == other.Port &&
        s.Not == other.Not
}

This method compares the presence (not value) of the Any field, then performs exact string comparison for all other fields.

Negation Semantics#

Presence-Based Boolean#

Address negation uses the presence-based <not/> element to implement NOT logic:

<!-- Match all addresses EXCEPT the lan subnet -->
<source>
  <not/>
  <network>lan</network>
</source>

<!-- Match all addresses EXCEPT 10.0.0.0/8 with port 22 -->
<source>
  <not/>
  <address>10.0.0.0/8</address>
  <port>22</port>
</source>

Test examples demonstrate negation with both network and address patterns.

BoolFlag Implementation#

The <not/> field uses the BoolFlag custom type for presence-based boolean semantics:

type BoolFlag bool

func (bf *BoolFlag) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
    var content string
    if err := d.DecodeElement(&content, &start); err != nil {
        return err
    }

    if strings.TrimSpace(content) == "" {
        *bf = true // Empty body: presence = true
        return nil
    }

    *bf = BoolFlag(shared.IsValueTrue(content)) // Non-empty: delegate to liberal parser

    return nil
}

The UnmarshalXML implementation layers presence semantics over value-based parsing: absent element means false (Go zero value), empty body (<not/>) means true (presence convention), non-empty body (<not>on</not>) delegates to shared.IsValueTrue which recognizes "on", "yes", "1", "true", "enabled" (and their case variants) as truthy.

Port Specifications#

Port Field#

Both source and destination blocks support optional port specifications that apply only to TCP and UDP protocols:

<!-- Single port -->
<destination>
  <network>wan</network>
  <port>443</port>
</destination>

<!-- Port range (hyphen-delimited in XML) -->
<destination>
  <any/>
  <port>8000-9000</port>
</destination>

Per documentation, port ranges in config.xml use hyphen delimiters (80-443), which are converted to colon delimiters (80:443) when generating pf firewall rules.

Port Handling Differences#

Configuration-reference notes mention that Source.Port (nested <source><port>) and Rule.SourcePort (top-level <sourceport>) represent different XML elements in legacy configurations, with preference given to the nested Source.Port field when both are present.

Floating Rules and Direction#

Direction Requirement#

Floating rules require a direction field specifying traffic flow:

<rule>
  <floating>yes</floating>
  <direction>in</direction>
  <interface>lan,wan</interface>
  <source>
    <network>lan</network>
  </source>
  <destination>
    <any/>
  </destination>
</rule>

Valid direction values: in, out, any

Test examples demonstrate all three direction types with floating rules.

Interface Lists#

Floating rules support comma-separated interface lists (<interface>wan,lan,opt1</interface>), allowing a single rule to apply across multiple interfaces simultaneously.

Boolean Field Patterns#

Two Distinct Patterns#

OPNsense XML uses two distinct boolean patterns that must not be conflated:

Presence-based (element present = true, absent = false):

  • Examples: <disabled/>, <log/>, <quick/>, <not/>, <any/>, <staticnatport/>
  • Go type: BoolFlag
  • PHP pattern: isset($rule['field'])

Value-based (contains 1 = true, 0 or absent = false):

  • Examples: <enable>1</enable>, <blockpriv>1</blockpriv>
  • Go type: string or FlexBool
  • PHP pattern: $rule['field'] == "1"

The BoolFlag implementation correctly handles both empty-element presence (<not/> → true) and liberal value parsing (<not>on</not> → true, <not>0</not> → false) by delegating non-empty content to shared.IsValueTrue. For fields that are always emitted with body content, use FlexBool directly instead of BoolFlag.

Liberal Boolean Vocabulary#

The pkg/schema/shared/ package provides canonical liberal boolean parsing:

  • shared.IsValueTrue(s): Recognizes "1", "on", "yes", "true", "enable", "enabled" (case-insensitive)
  • shared.IsValueFalse(s): Recognizes "0", "off", "no", "false", "disable", "disabled", "" (case-insensitive)
  • FlexBool: Value-level liberal boolean type for fields always emitted with content
  • FlexInt: Liberal integer that coerces truthy/falsy strings to 1/0 while preserving numeric inputs

This upgrade fixes OPNsense 26.1 compatibility where fields previously typed as Go int receive "on" values, causing strconv.ParseInt failures. Ten opnsense.System fields (DNSAllowOverride, UseVirtualTerminal, hardware offloading toggles, etc.) migrated from int to BoolFlag to handle this vocabulary.

NAT Reflection Modes#

Inbound NAT rules support reflection configuration:

<rule>
  <natreflection>enable</natreflection>
  <associated-rule-id>5f1234567890abcd</associated-rule-id>
</rule>

Valid modes: enable, disable, purenat

State Types#

State tracking configuration:

<rule>
  <statetype>keep state</statetype>
  <statetimeout>3600</statetimeout>
</rule>

Valid state types: keep state, sloppy state, synproxy state, none

Rate Limiting#

Connection rate limiting format:

<rule>
  <max-src-nodes>100</max-src-nodes>
  <max-src-conn>10</max-src-conn>
  <max-src-conn-rate>15/5</max-src-conn-rate>
</rule>

The max-src-conn-rate field uses format connections/seconds (e.g., 15/5 = 15 connections per 5 seconds).

Relevant Code Files#

FileDescriptionLines
internal/schema/security.goSource and Destination struct definitions, EffectiveAddress() implementation200-287
internal/schema/common.goBoolFlag type and UnmarshalXML implementation14-55
internal/schema/security_test.goComprehensive test coverage for XML addressing patterns650-820, 1074-1113
testdata/sample.config.*.xmlProduction OPNsense configuration examplesVarious
  • Firewall Rule Schemas: Complete OPNsense rule structure including protocols, actions, and scheduling
  • XML Unmarshaling Patterns: Custom Go types for handling presence-based vs value-based XML semantics
  • OPNsense Configuration Management: Tools and libraries for parsing, validating, and generating firewall configurations
  • Packet Filter (pf) Syntax: Translation from OPNsense XML to BSD pf rule syntax
XML Addressing Semantics | Dosu