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 isadmin - OPNsense:
<passwd>element; default administrative user isroot
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:
| Value | Meaning |
|---|---|
automatic | Automatic outbound NAT (default) |
hybrid | Hybrid: automatic + manual rules |
advanced | Manual outbound NAT only |
disabled | Disable 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
BoolFlagtype that setstrueon 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
stringwith value check (legacy), orshared.FlexBoolfor 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
uuidattribute on the element:<rule uuid="..."> - pfSense: Rules use a
<tracker>child element (integer, auto-generated frommicrotime()):<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 Element | Go Type | Notes |
|---|---|---|
<type> | string → enum | pass, block, reject |
<interface> | InterfaceList (custom) | Comma-separated for floating rules |
<ipprotocol> | string → enum | inet, inet6 |
<protocol> | string | tcp, udp, etc. |
<disabled> | BoolFlag | Presence-based |
<log> | BoolFlag | Presence-based |
<quick> | BoolFlag | Presence-based |
<floating> | string | OPNsense-specific |
<direction> | string → enum | in, out, any |
<tracker> | string | pfSense identity field |
uuid attr | string | OPNsense 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:
| Field | Separator | Context |
|---|---|---|
<interface>wan,lan,opt1</interface> | Comma | Floating firewall rules |
<icmptype>3,11,0</icmptype> | Comma | ICMP type list |
<timeservers>0.ntp.org 1.ntp.org</timeservers> | Space | NTP servers |
Bridge <members> | Comma | Bridge member interfaces |
Interface group <members> | Space | Note: 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 bestringnotBoolFlag<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 →
BoolFlagor*string
3. Does PHP upstream use == "1" or value comparison?
- YES →
string(presence-based type will break<field>0</field>semantics)
4. Is the element an address that may be <any/>, <network>, or <address>?
- Use
*stringforAny, plainstringforNetworkandAddress
5. Does the section use dynamic element names (e.g., <wan>, <lan>, <opt0>)?
- YES → Implement custom
UnmarshalXML/MarshalXMLwith map
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:
| # | Category | pfSense | OPNsense | Parser Impact |
|---|---|---|---|---|
| 1 | Root element | <pfsense> | <opnsense> | Auto-detection mechanism |
| 2 | Password field | <bcrypt-hash> | <passwd> | Field mapping |
| 3 | Default admin user | admin | root | Authentication logic |
| 4 | NAT inbound nesting | Direct <nat><rule> | Flat <nat><rule> (no <inbound>) | XPath must not assume wrapper |
| 5 | NAT target field | <target> | <internalip> | Field name mapping |
| 6 | NAT outbound mode | Similar structure | automatic, hybrid, advanced, disabled | Typed enum handling |
| 7 | Rule identity | <tracker> element (integer) | uuid attribute | Must parse both patterns |
| 8 | Boolean patterns | Two incompatible patterns | Presence-based + value-based | Custom BoolFlag type required |
| 9 | MVC rule system | Not present | <OPNsense><Firewall><Filter> | OPNsense parser must handle both |
| 10 | Kea DHCP | Not present | <OPNsense><Kea><Dhcp4> | OPNsense-only feature detection |
| 11 | DNS resolver enable | Value-based | Value-based (<enable>1</enable>) | Must NOT use BoolFlag |
| 12 | Traffic shaping fields | ALTQ model | <dnpipe>, <pdnpipe>, queues | Platform-specific fields |
| 13 | Config version | <version> element | <trigger_initial_wizard/> flag | Different versioning schemes |
| 14 | Interface keys | Dynamic names (<wan>, <lan>) | Same pattern | Custom XML marshaling required |
| 15 | List separators | Comma/space context-dependent | Same patterns | Custom unmarshaling for lists |
| 16 | Self-closing tags | Both <tag/> and <tag></tag> | Same compatibility issue | Parser must handle both forms |
| 17 | Address priority | <network> > <address> > <any> | Same priority order | Mutually 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]
Related Topics#
- XML Schema Design: Strategies for handling presence-based vs value-based booleans in XML
- Parser Factory Pattern: Self-registration systems using
init()functions (similar todatabase/sqldriver 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/MarshalXMLimplementations for complex XML patterns - Typed Enums in Go: String-based typed constants with validation patterns
Relevant Code Files#
| File | Description | Key Features |
|---|---|---|
pkg/schema/opnsense/opnsense.go | Root OPNsense XML schema | OpnSenseDocument struct with xml:"opnsense" root element; 30+ configuration sections |
pkg/schema/opnsense/common.go | Shared types and BoolFlag | Custom BoolFlag type with presence+value semantics; delegates non-empty bodies to shared.IsValueTrue |
pkg/schema/shared/bool.go | Canonical truthy parsers | IsValueTrue / IsValueFalse supporting `1 |
pkg/schema/shared/flex_bool.go | Value-level boolean wrapper | FlexBool for XML fields where element body (not presence) carries the boolean signal |
pkg/schema/shared/flex_int.go | Liberal integer wrapper | FlexInt coerces truthy/falsy strings to 1/0, passes numerics through unchanged |
pkg/schema/opnsense/security.go | Firewall and NAT schemas | FilterRule, NATRule, Source/Destination with priority resolution; EffectiveAddress() method |
pkg/schema/opnsense/interfaces.go | Interface map marshaling | Custom UnmarshalXML/MarshalXML for dynamic interface keys (<wan>, <lan>, etc.) |
pkg/schema/pfsense/document.go | Root pfSense XML schema | Document struct with xml:"pfsense" root element; reuses 25+ OPNsense types |
pkg/schema/pfsense/system.go | pfSense system configuration | Forked System, User, Group structs with []string DNS servers, <bcrypt-hash>, and []string privileges |
pkg/schema/pfsense/security.go | pfSense firewall and NAT | Forked InboundRule with <target>, FilterRule with pfSense-specific fields |
pkg/schema/pfsense/network.go | pfSense network configuration | DHCPv6 with pfSense-specific <ramode> and <rapriority> fields |
pkg/schema/pfsense/services.go | pfSense services | SyslogConfig, Unbound, Cron, Widgets, Diag structs |
pkg/parser/factory.go | Parser factory and root detection | peekRootElementBounded() with 10MB limit; context-aware streaming detection |
pkg/parser/xmlutil.go | Shared XML security utilities | NewSecureXMLDecoder() with XXE protection, charset handling (Windows-1252, ISO-8859-1) |
pkg/parser/registry.go | Device parser registry | database/sql-style registration pattern; panic on duplicate registration |
pkg/parser/opnsense/parser.go | OPNsense parser implementation | init() self-registration; blank import requirement |
pkg/parser/opnsense/converter.go | XML to CommonDevice converter | Two-stage transformation; typed enum casting; warning accumulation |
pkg/parser/pfsense/parser.go | pfSense parser implementation | init() self-registration; manages own XML decoding pipeline |
pkg/parser/pfsense/converter.go | pfSense to CommonDevice converter | Four-stage transformation (system, network, security, services) with validation |
pkg/model/firewall.go | Platform-agnostic domain model | Typed enums for FirewallRuleType, NATOutboundMode, IPProtocol with validation |
internal/cfgparser/xml.go | Low-level XML parser | Switch-dispatch pattern for top-level elements; manual slice append for <ca>, <cert> |
internal/validator/pfsense.go | pfSense semantic validation | Reuses shared IP/CIDR/MTU validation helpers; pfSense-specific rule validation |
docs/development/xml-structure-research.md | Research documentation | Boolean patterns, address priority rules, PHP upstream patterns |
docs/development/architecture.md | Architecture overview | Two-stage parsing pipeline; extensibility patterns |
pkg/schema/pfsense/README.md | pfSense schema reference | Comprehensive 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:
Systemstruct: Forks to support multiple<dnsserver>elements as[]stringinstead of a single space-separated stringGroupstruct: Forks to support multiple<priv>elements as[]string(pfSense allows per-group privileges as repeating elements)Userstruct: Forks to use<bcrypt-hash>field instead of<passwd>InboundRulestruct: Forks to use<target>field instead of<internalip>for NAT redirect destinationFilterRulestruct: Adds pfSense-specific fields like<id>,<tag>,<tagged>,<os>,<associated-rule-id>DHCPv6Interfacestruct: pfSense-specific with<ramode>and<rapriority>fieldsSyslogConfigstruct: 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.Document → CommonDevice with validation and warning accumulation.
[Implementation: pkg/schema/pfsense/, pkg/parser/pfsense/]