Documents
PfSense vs OPNsense XML Structural Differences
PfSense vs OPNsense XML Structural Differences
Type
Topic
Status
Published
Created
Mar 22, 2026
Updated
Apr 18, 2026
Created by
Dosu Bot
Updated by
Dosu Bot

pfSense vs OPNsense XML Structural Differences#

pfSense and OPNsense are two widely-deployed open-source firewall and routing platforms that share a common lineage—OPNsense forked from pfSense in 2015. Both systems store their complete configuration state in XML files (config.xml), and while these formats appear superficially similar, they contain numerous critical structural divergences that directly impact parser implementation, data migration, and cross-platform tooling. This article provides a comprehensive technical reference cataloging 17+ structural differences between pfSense and OPNsense configuration XML schemas, based on research and implementation work from the opnDossier project.

Understanding these structural differences is essential for software engineers developing cross-platform firewall management systems, configuration migration utilities, backup analysis tools, or multi-vendor network automation platforms. The divergences span fundamental XML structure (root element naming for device auto-detection), data type semantics (boolean representation patterns with incompatible conventions), network address translation structures (NAT rule nesting and field naming), authentication systems (password field names and privilege models), and feature-specific subsystems (Kea DHCP, MVC architecture, traffic shaping). Parsers that do not explicitly handle both platforms risk silent data loss, incorrect semantic interpretation, or runtime failures.

This document serves as a definitive technical reference for these differences, organized by functional subsystem, with implementation guidance, decision trees for parser design, security considerations, and code examples drawn from production Go implementations in the opnDossier parser library.

Structural Differences by Category#

Root Element and Device Detection#

The most fundamental difference between the two platforms is the XML root element:

  • pfSense: <pfsense> as the document root
  • OPNsense: <opnsense> as the document root

This difference is used for auto-detection of device type in parser factory patterns. A parser registry should key registration on the root element string to enable automatic platform identification without requiring explicit configuration.

Source: Project Milestones And Release Roadmap

User Authentication and Authorization#

Password Field Names#

The platforms use different XML elements for storing user password hashes:

  • pfSense: <bcrypt-hash> element; default administrative user is admin
  • OPNsense: <passwd> element; default administrative user is root

Source: Project Milestones And Release Roadmap

User and Group Structure#

Both platforms nest users and groups under the <system> element: <system><user>...</user><group>...</group></system>. Each user may contain nested <apikeys> elements for API authentication.

Group privileges: pfSense allows multiple <priv> elements per group (stored as []string in the schema), while OPNsense uses a single string field. This divergence requires a forked Group struct in the pfSense schema.

Network Address Translation (NAT)#

Inbound Rule Nesting#

A critical path difference exists for inbound NAT (port forwarding) rules:

  • OPNsense: Rules are at <nat><rule>...</rule></nat> (flat, no <inbound> wrapper)
  • pfSense: Rules are also direct children at <nat><rule>...</rule></nat>, but use <target> instead of <internalip> for the redirect destination

The opnDossier Go schema uses xml:"inbound>rule" as a path mapping trick for unmarshaling OPNsense configs, but the actual XML does not contain an <inbound> wrapper element. The pfSense schema directly uses xml:"rule" without the path mapping. Parsers that assume nested <inbound> elements will silently fail to parse NAT rules.

Source: xml-structure-research

Outbound Mode Values#

OPNsense outbound NAT mode is stored as a string field with these valid values:

ValueMeaning
automaticAutomatic outbound NAT (default)
hybridHybrid: automatic + manual rules
advancedManual outbound NAT only
disabledDisable outbound NAT

These must be handled as typed constants in a parser, not as presence-based booleans.

Source: xml-structure-research

Platform-Specific NAT Rule Fields#

OPNsense includes additional NAT rule fields that pfSense may handle differently or not support:

Outbound NAT rule fields:

  • <staticnatport> (BoolFlag): Preserve source port
  • <nonat> (BoolFlag): Disable NAT for this rule
  • <natport> (string): NAT destination port
  • <poolopts_sourcehashkey> (string): Pool options hash key

Inbound NAT rule fields:

  • <natreflection> (string): NAT reflection mode
  • <associated-rule-id> (string): Linked filter rule ID
  • <nordr> (BoolFlag): No redirect
  • <nosync> (BoolFlag): Disable sync
  • <local-port> (string): Internal port after translation

Source: xml-structure-research

Boolean Representation Semantics#

One of the most parser-critical divergences is that both platforms use two incompatible boolean patterns within the same XML document. Using the wrong type mapping silently breaks semantics.

Presence-Based Booleans#

Element presence = true; absence = false; element content is irrelevant.

  • PHP upstream pattern: isset($rule['disabled'])
  • Go type: Custom BoolFlag type that sets true on any element presence, ignoring content
  • Examples: <disabled/>, <log/>, <quick/>, <not/>, <nosync/>, <nordr/>, <staticnatport/>, <nonat/>

Value-Based Booleans#

Element contains "1", "yes", or a specific value; absent or empty = false.

  • PHP upstream pattern: $config['system']['enable'] == "1"
  • Go type: Plain string with value check (legacy), or shared.FlexBool for explicit boolean semantics
  • Examples: <enable>1</enable>, <blockpriv>1</blockpriv>, <ipv6allow>1</ipv6allow>, <disablenatreflection>yes</disablenatreflection>

Both OPNsense and pfSense emit a liberal truthy vocabulary (1|on|yes|true|enable|enabled, case-insensitive). The new pkg/schema/shared/ package provides FlexBool (value-only boolean), FlexInt (liberal int with truthy/falsy fallback), and the canonical truthy parsers IsValueTrue/IsValueFalse that handle this vocabulary uniformly across both platforms.

Source: xml-structure-research, XML Presence Detection

Rule Identity and Tracking#

The platforms use different mechanisms for rule identity:

  • OPNsense: Rules carry a uuid attribute on the element: <rule uuid="...">
  • pfSense: Rules use a <tracker> child element (integer, auto-generated from microtime()): <rule><tracker>1234567890</tracker></rule>

Both may coexist in configs migrated from pfSense to OPNsense. Parsers must handle both patterns to ensure robust migration support.

Source: xml-structure-research

OPNsense Legacy vs MVC Architecture#

OPNsense maintains two parallel rule systems that pfSense does not have:

  • Legacy format: Rules in <filter><rule> and <nat><rule> (pfSense-compatible format)
  • MVC/New-style: Rules in <OPNsense><Firewall><Filter> with <rules>, <snatrules>, etc.

Both are loaded and processed by OPNsense. Current Go schemas only model the legacy format. A pfSense parser does not need to handle the MVC tree; an OPNsense parser may need to handle both for complete configuration coverage.

Source: xml-structure-research

Firewall Rules and Packet Filtering#

Core Filter Rule Fields#

Core fields shared by both platforms:

XML ElementGo TypeNotes
<type>string → enumpass, block, reject
<interface>InterfaceList (custom)Comma-separated for floating rules
<ipprotocol>string → enuminet, inet6
<protocol>stringtcp, udp, etc.
<disabled>BoolFlagPresence-based
<log>BoolFlagPresence-based
<quick>BoolFlagPresence-based
<floating>stringOPNsense-specific
<direction>string → enumin, out, any
<tracker>stringpfSense identity field
uuid attrstringOPNsense identity attribute

Advanced Filter Rule Fields#

Additional fields for rate limiting and advanced options: <max-src-nodes>, <max-src-conn>, <max-src-conn-rate>, <max-src-conn-rates>, <tcpflags1>, <tcpflags2>, <tcpflags_any> (BoolFlag), <icmptype>, <icmp6-type>, <statetimeout>, <allowopts> (BoolFlag), <disablereplyto> (BoolFlag), <nopfsync> (BoolFlag), <nosync> (BoolFlag).

Source: xml-structure-research

Source and Destination Address Structure#

Both platforms share the nested <source>/<destination> pattern, but the fields are mutually exclusive and follow strict priority:

<!-- Match any address -->
<source><any/></source>

<!-- Match interface subnet (highest priority) -->
<source><network>lan</network></source>

<!-- Match specific IP/CIDR or alias -->
<source><address>192.168.1.0/24</address></source>

<!-- Negated match -->
<source><not/><network>lan</network></source>

<!-- With port -->
<destination><any/><port>443</port></destination>

Priority: <network> > <address> > <any> (implicit)

Parser note: <any> must use *string (pointer), NOT plain string, because Go's encoding/xml produces "" for both <any/> (present) and absent elements when using plain string—losing the semantic distinction.

Port range delimiter: In config.xml, hyphens (80-443); in pf rules, colons (80:443)—the conversion is handled by OPNsense/pfSense code.

Source: xml-structure-research, XML Presence Detection

Network Configuration#

Dynamic Interface Keys#

The <interfaces> section uses dynamic element names rather than repeated fixed elements:

<interfaces>
  <wan>...</wan>
  <lan>...</lan>
  <opt0>...</opt0>
</interfaces>

This requires custom UnmarshalXML/MarshalXML with a map type—standard Go struct tags cannot handle dynamic element names. The same pattern applies to <dhcpd> (DHCP scopes keyed by interface name).

Source: xml-structure-research

Comma-Separated and Space-Separated Lists#

Several fields pack multiple values into one element and require custom unmarshaling:

FieldSeparatorContext
<interface>wan,lan,opt1</interface>CommaFloating firewall rules
<icmptype>3,11,0</icmptype>CommaICMP type list
<timeservers>0.ntp.org 1.ntp.org</timeservers>SpaceNTP servers
Bridge <members>CommaBridge member interfaces
Interface group <members>SpaceNote: space, not comma

Source: xml-structure-research, Schema Parser Synchronization

DNS Configuration and Unbound#

OPNsense: DNS resolver uses a <unbound> struct. The Enable field is value-based (<enable>1</enable>), NOT presence-based—it must remain a string type, not BoolFlag. Converting to BoolFlag would break <enable>0</enable> semantics.

pfSense: DNS servers are stored as repeating <dnsserver> elements (parsed as []string in the schema), while OPNsense typically uses a single space-separated string field. <timeservers> uses space-separated NTP server addresses.

Source: xml-structure-research

Aliases and Firewall Objects#

Alias references appear in <source><address> and <destination><address> fields—an alias name (e.g., MyAlias) is syntactically identical to an IP address in the XML. The parser cannot distinguish them purely from structure; context and a separate alias table lookup are required.

Source: xml-structure-research

Platform-Specific Features#

DHCPv6 Server Configuration#

pfSense: Uses a map-based structure (<dhcpdv6><lan>...</lan><wan>...</wan></dhcpdv6>) with interface-specific fields including <ramode> (Router Advertisement mode) and <rapriority> (RA priority). These fields control IPv6 router advertisements and are unique to pfSense.

OPNsense: Has a different DHCPv6 implementation that does not use these pfSense-specific fields.

Syslog Configuration Differences#

pfSense: The <syslog> section includes facility-specific remote logging toggles (auth, routing, ntpd, ppp, vpn, dhcp, resolver, etc.), log rotation settings, and structured notification fields that differ from OPNsense's implementation. Currently only minimal syslog fields are modeled in the pfSense schema.

OPNsense: Uses a simpler syslog configuration structure.

Traffic Shaping Models#

OPNsense-specific traffic shaping fields on filter rules:

  • <dnpipe> — Download pipe assignment
  • <pdnpipe> — Parent download pipe
  • <defaultqueue> — Default queue assignment
  • <ackqueue> — ACK queue assignment

These are used by OPNsense's traffic shaping subsystem and may not have direct equivalents in pfSense's ALTQ-based shaping or may use different element names.

Source: xml-structure-research

Kea DHCP Server (OPNsense-Only)#

OPNsense introduced Kea DHCP as a replacement for ISC DHCP. Kea configuration lives under:

  • <OPNsense><Kea><Dhcp4> (MVC-style path)
  • <Kea.Dhcp4.General.Enabled> — value-based boolean ("1"/"0"), must be string not BoolFlag
  • <Kea.HighAvailability.Enabled> — also value-based

pfSense does not have Kea DHCP—it uses the legacy ISC <dhcpd> map structure exclusively. A parser must check for the presence of the <Kea> subsection to determine which DHCP model is active.

Source: xml-structure-research

Certificate Revocation Lists and PKI#

Top-level repeating elements like <ca> (Certificate Authority) and <cert> require manual append in the parser because Go's encoding/xml does not automatically populate slices for sibling elements at the document root without a container element. The parser must use a switch-dispatch pattern.

CRL entries follow the same pattern. Both platforms share this XML structure challenge.

Source: Schema Parser Synchronization

Config Versioning and First-Boot Detection#

  • OPNsense: <trigger_initial_wizard/> at the root element is a presence-based boolean (BoolFlag) that signals first-boot wizard state
  • pfSense: Uses a <version> child element for config schema version tracking (integer-based, auto-incremented)

Config version numbers between the two platforms are not interchangeable—they represent divergent schema evolution histories.

Source: xml-structure-research

Self-Closing Tag Compatibility#

Prior to pfSense 2.3.3, some code produced <tag/> while other code produced <tag></tag>. Both forms are valid XML and must be handled identically by any parser. Presence-detection types (BoolFlag, *string) handle both forms correctly—this is a known compatibility requirement for parsing older config files from both platforms.

Source: xml-structure-research, XML Presence Detection

Parser Implementation Guidance#

When mapping XML fields to Go types, follow this decision tree based on opnDossier's implementation patterns:

1. Does element presence (vs absence) convey meaning?

  • YES → Use BoolFlag (boolean) or *string (needs value)
  • NO → Use plain string

2. Does PHP upstream use isset() or !empty()?

  • YES → BoolFlag or *string

3. Does PHP upstream use == "1" or value comparison?

4. Is the element an address that may be <any/>, <network>, or <address>?

  • Use *string for Any, plain string for Network and Address

5. Does the section use dynamic element names (e.g., <wan>, <lan>, <opt0>)?

BoolFlag Implementation Example#

The BoolFlag type implements presence-based boolean semantics with delegation to shared.IsValueTrue for non-empty bodies:

type BoolFlag bool

// UnmarshalXML implements presence+value semantics:
// - Absent element → false (zero value)
// - <tag/> or <tag></tag> → true (presence means enabled)
// - <tag>body</tag> → shared.IsValueTrue(body) handles "on", "yes", "1", etc.
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
        return nil
    }

    *bf = BoolFlag(shared.IsValueTrue(content))
    return nil
}

// MarshalXML emits empty element for true, omits for false
func (bf *BoolFlag) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
    if *bf {
        return e.EncodeElement("", start) // <disabled/>
    }
    return nil // Element omitted
}

Source: opnDossier schema implementation

Summary Table of Key Differences#

The following table summarizes the 15+ critical structural differences between pfSense and OPNsense config.xml files:

#CategorypfSenseOPNsenseParser Impact
1Root element<pfsense><opnsense>Auto-detection mechanism
2Password field<bcrypt-hash><passwd>Field mapping
3Default admin useradminrootAuthentication logic
4NAT inbound nestingDirect <nat><rule>Flat <nat><rule> (no <inbound>)XPath must not assume wrapper
5NAT target field<target><internalip>Field name mapping
6NAT outbound modeSimilar structureautomatic, hybrid, advanced, disabledTyped enum handling
7Rule identity<tracker> element (integer)uuid attributeMust parse both patterns
8Boolean patternsTwo incompatible patternsPresence-based + value-basedCustom BoolFlag type required
9MVC rule systemNot present<OPNsense><Firewall><Filter>OPNsense parser must handle both
10Kea DHCPNot present<OPNsense><Kea><Dhcp4>OPNsense-only feature detection
11DNS resolver enableValue-basedValue-based (<enable>1</enable>)Must NOT use BoolFlag
12Traffic shaping fieldsALTQ model<dnpipe>, <pdnpipe>, queuesPlatform-specific fields
13Config version<version> element<trigger_initial_wizard/> flagDifferent versioning schemes
14Interface keysDynamic names (<wan>, <lan>)Same patternCustom XML marshaling required
15List separatorsComma/space context-dependentSame patternsCustom unmarshaling for lists
16Self-closing tagsBoth <tag/> and <tag></tag>Same compatibility issueParser must handle both forms
17Address priority<network> > <address> > <any>Same priority orderMutually exclusive field resolution

Sources: xml-structure-research, Project Milestones, XML Presence Detection

Security Considerations for Parsers#

When implementing parsers for pfSense and OPNsense config.xml files, the following security measures are critical:

XML Bomb Protection#

The opnDossier parser implements bounded input reading with a 10MB limit (DefaultMaxInputSize) to prevent XML entity expansion attacks (billion laughs attack):

limited := io.LimitReader(newCtxReader(ctx, r), DefaultMaxInputSize)

XXE Prevention#

External entity processing must be disabled in the XML decoder to prevent XXE (XML External Entity) attacks. Standard Go encoding/xml does not process external entities by default, but parsers should explicitly verify this.

Charset Handling#

Older pfSense configs may use ISO-8859-1 or Windows-1252 encoding rather than UTF-8. The parser must support these legacy charsets to avoid data corruption during migration.

Streaming Token-Based Parsing#

The opnDossier implementation uses streaming token-based parsing for memory efficiency on large configs:

dec := xml.NewDecoder(tee)
for {
    tok, err := dec.Token()
    if se, ok := tok.(xml.StartElement); ok {
        return se.Name.Local // Root element detection
    }
}

Secret Handling#

Sensitive data should be excluded at the converter layer, not redacted post-parse:

  • Never map to domain model: OpenVPN TLS keys, IPsec pre-shared keys, WireGuard private keys
  • Password hashes: May be mapped for authentication logic but should never be exported in plain output formats

[Sources: Project Milestones, CommonDevice Export Model, Factory implementation]

Usage Examples#

Example 1: Detecting Device Type from Root Element#

The parser factory automatically detects the device type:

import (
    "context"
    "os"

    "github.com/EvilBit-Labs/opnDossier/pkg/parser"
    _ "github.com/EvilBit-Labs/opnDossier/pkg/parser/opnsense" // Registers OPNsense parser
    _ "github.com/EvilBit-Labs/opnDossier/pkg/parser/pfsense" // Registers pfSense parser
)

func main() {
    file, _ := os.Open("config.xml")
    defer file.Close()

    factory := parser.NewFactory(cfgparser.NewXMLParser())

    // Auto-detects based on root element (<opnsense> or <pfsense>)
    device, warnings, err := factory.CreateDevice(context.Background(), file, "", parser.ValidateWarn)

    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Device type: %s\n", device.DeviceType) // "opnsense" or "pfsense"
    fmt.Printf("Warnings: %d\n", len(warnings))
}

Example 2: Handling Boolean Semantic Differences#

Incorrectly using BoolFlag for value-based booleans causes silent failures:

// WRONG (historical bug before issue #558 fix): Using BoolFlag for value-based boolean
type Unbound struct {
    Enable BoolFlag `xml:"enable"` // ⚠️ BoolFlag now correctly handles <enable>0</enable> → false via shared.IsValueTrue delegation
}

// CORRECT: Using string for value-based boolean
type Unbound struct {
    Enable string `xml:"enable"` // ✓ Check with: Enable == "1"
}

// CORRECT: Using BoolFlag for presence-based boolean
type FilterRule struct {
    Disabled BoolFlag `xml:"disabled"` // ✓ Presence = true, absence = false
    Log BoolFlag `xml:"log"`
}

Example 3: Resolving Mutually Exclusive Address Fields#

The EffectiveAddress() method implements priority resolution:

type Source struct {
    Network string `xml:"network,omitempty"`
    Address string `xml:"address,omitempty"`
    Any *string `xml:"any,omitempty"` // Pointer to detect presence
    Not BoolFlag `xml:"not"`
}

func (s Source) EffectiveAddress() string {
    if s.Network != "" { return s.Network } // Highest priority
    if s.Address != "" { return s.Address }
    if s.Any != nil { return "any" } // Must use pointer to detect <any/>
    return ""
}

// Usage
rule := FilterRule{
    Source: Source{
        Network: "lan",
        Address: "192.168.1.0/24", // Ignored because Network takes priority
    },
}

fmt.Println(rule.Source.EffectiveAddress()) // Output: "lan"

Example 4: Handling Dynamic Interface Keys#

Custom XML marshaling for map-based interface representation:

type Interfaces struct {
    Items map[string]Interface `xml:",any"`
}

func (i *Interfaces) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
    i.Items = make(map[string]Interface)

    for {
        tok, err := d.Token()
        if err == io.EOF {
            break
        }

        if se, ok := tok.(xml.StartElement); ok {
            var iface Interface
            if err := d.DecodeElement(&iface, &se); err != nil {
                return err
            }
            i.Items[se.Name.Local] = iface // "wan", "lan", "opt0", etc.
        }
    }

    return nil
}

// Result: map["wan"]{...} map["lan"]{...} map["opt0"]{...}

Example 5: Converter Typed Enum Casting#

The converter transforms XML strings to typed enums:

func (c *converter) convertFirewallRules(doc *schema.OpnSenseDocument) []common.FirewallRule {
    var rules []common.FirewallRule

    for i, rule := range doc.Filter.Rules.Rule {
        commonRule := common.FirewallRule{
            ID: rule.UUID,
            Type: common.FirewallRuleType(rule.Type), // "pass" → RuleTypePass
            Direction: common.FirewallDirection(rule.Direction), // "in" → DirectionIn
            IPProtocol: common.IPProtocol(rule.IPProtocol), // "inet" → IPProtocolInet
            Protocol: rule.Protocol,
            Disabled: bool(rule.Disabled), // BoolFlag → bool
        }

        // Validation with warning accumulation
        if !commonRule.Type.IsValid() {
            c.addWarning(
                fmt.Sprintf("FirewallRules[%d].Type", i),
                rule.Type,
                "invalid firewall rule type",
                common.SeverityHigh,
            )
        }

        rules = append(rules, commonRule)
    }

    return rules
}

Example 6: Handling Both Rule Identity Patterns#

Supporting both pfSense <tracker> and OPNsense uuid attribute:

type FilterRule struct {
    UUID string `xml:"uuid,attr,omitempty"` // OPNsense: attribute
    Tracker string `xml:"tracker,omitempty"` // pfSense: child element
}

func (r FilterRule) GetID() string {
    if r.UUID != "" {
        return r.UUID
    }
    return r.Tracker
}

[Sources: opnDossier code examples, xml-structure-research]

  • XML Schema Design: Strategies for handling presence-based vs value-based booleans in XML
  • Parser Factory Pattern: Self-registration systems using init() functions (similar to database/sql driver pattern)
  • Firewall Configuration Management: Cross-platform firewall configuration tools and migration utilities
  • OPNsense MVC Architecture: OPNsense's dual legacy + MVC rule system architecture
  • Go XML Marshaling: Custom UnmarshalXML/MarshalXML implementations for complex XML patterns
  • Typed Enums in Go: String-based typed constants with validation patterns

Relevant Code Files#

FileDescriptionKey Features
pkg/schema/opnsense/opnsense.goRoot OPNsense XML schemaOpnSenseDocument struct with xml:"opnsense" root element; 30+ configuration sections
pkg/schema/opnsense/common.goShared types and BoolFlagCustom BoolFlag type with presence+value semantics; delegates non-empty bodies to shared.IsValueTrue
pkg/schema/shared/bool.goCanonical truthy parsersIsValueTrue / IsValueFalse supporting `1
pkg/schema/shared/flex_bool.goValue-level boolean wrapperFlexBool for XML fields where element body (not presence) carries the boolean signal
pkg/schema/shared/flex_int.goLiberal integer wrapperFlexInt coerces truthy/falsy strings to 1/0, passes numerics through unchanged
pkg/schema/opnsense/security.goFirewall and NAT schemasFilterRule, NATRule, Source/Destination with priority resolution; EffectiveAddress() method
pkg/schema/opnsense/interfaces.goInterface map marshalingCustom UnmarshalXML/MarshalXML for dynamic interface keys (<wan>, <lan>, etc.)
pkg/schema/pfsense/document.goRoot pfSense XML schemaDocument struct with xml:"pfsense" root element; reuses 25+ OPNsense types
pkg/schema/pfsense/system.gopfSense system configurationForked System, User, Group structs with []string DNS servers, <bcrypt-hash>, and []string privileges
pkg/schema/pfsense/security.gopfSense firewall and NATForked InboundRule with <target>, FilterRule with pfSense-specific fields
pkg/schema/pfsense/network.gopfSense network configurationDHCPv6 with pfSense-specific <ramode> and <rapriority> fields
pkg/schema/pfsense/services.gopfSense servicesSyslogConfig, Unbound, Cron, Widgets, Diag structs
pkg/parser/factory.goParser factory and root detectionpeekRootElementBounded() with 10MB limit; context-aware streaming detection
pkg/parser/xmlutil.goShared XML security utilitiesNewSecureXMLDecoder() with XXE protection, charset handling (Windows-1252, ISO-8859-1)
pkg/parser/registry.goDevice parser registrydatabase/sql-style registration pattern; panic on duplicate registration
pkg/parser/opnsense/parser.goOPNsense parser implementationinit() self-registration; blank import requirement
pkg/parser/opnsense/converter.goXML to CommonDevice converterTwo-stage transformation; typed enum casting; warning accumulation
pkg/parser/pfsense/parser.gopfSense parser implementationinit() self-registration; manages own XML decoding pipeline
pkg/parser/pfsense/converter.gopfSense to CommonDevice converterFour-stage transformation (system, network, security, services) with validation
pkg/model/firewall.goPlatform-agnostic domain modelTyped enums for FirewallRuleType, NATOutboundMode, IPProtocol with validation
internal/cfgparser/xml.goLow-level XML parserSwitch-dispatch pattern for top-level elements; manual slice append for <ca>, <cert>
internal/validator/pfsense.gopfSense semantic validationReuses shared IP/CIDR/MTU validation helpers; pfSense-specific rule validation
docs/development/xml-structure-research.mdResearch documentationBoolean patterns, address priority rules, PHP upstream patterns
docs/development/architecture.mdArchitecture overviewTwo-stage parsing pipeline; extensibility patterns
pkg/schema/pfsense/README.mdpfSense schema referenceComprehensive field inventory, listtags, version mapping, OPNsense divergences

pfSense Schema Implementation#

The pfSense schema is implemented in pkg/schema/pfsense/ and registered with the parser factory for automatic <pfsense> root element detection. It follows a copy-on-write architecture that maximizes code reuse:

Reused OPNsense types (25+): Where pfSense and OPNsense XML structures are identical, the pfSense schema imports and reuses OPNsense types directly—including BoolFlag, InterfaceList, Source, Destination, SSHConfig, Outbound, DHCP (ISC DHCP), OpenVPN, StaticRoutes, Gateways, VLANs, Certificate, CertificateAuthority, and more.

Forked types at divergence points:

  • System struct: Forks to support multiple <dnsserver> elements as []string instead of a single space-separated string
  • Group struct: Forks to support multiple <priv> elements as []string (pfSense allows per-group privileges as repeating elements)
  • User struct: Forks to use <bcrypt-hash> field instead of <passwd>
  • InboundRule struct: Forks to use <target> field instead of <internalip> for NAT redirect destination
  • FilterRule struct: Adds pfSense-specific fields like <id>, <tag>, <tagged>, <os>, <associated-rule-id>
  • DHCPv6Interface struct: pfSense-specific with <ramode> and <rapriority> fields
  • SyslogConfig struct: Currently minimal, pfSense has facility-specific remote logging toggles not present in OPNsense

The parser registers via init() and auto-detects <pfsense> root elements. The converter pipeline transforms pfsense.DocumentCommonDevice with validation and warning accumulation.

[Implementation: pkg/schema/pfsense/, pkg/parser/pfsense/]

PfSense vs OPNsense XML Structural Differences | Dosu