Summary
Add comprehensive support for parsing pfSense config.xml configuration files using the existing multi-device abstraction layer. The device-agnostic CommonDevice model, parser factory, and --device-type CLI flag are already in place — what remains is the pfSense-specific schema, parser, and validator, plus updating the markdown converter title and adding test coverage.
Background
pfSense and OPNsense share a common heritage (OPNsense forked from pfSense in 2014), resulting in similar but distinct XML configuration formats. Both store firewall state in a single config.xml file, but the root structure and several field names differ enough to require explicit support.
Key structural differences:
| Feature |
OPNsense |
pfSense |
| Root element |
<opnsense> |
<pfsense> |
| Password field |
<passwd> |
<bcrypt-hash> |
| Default admin user |
root |
admin |
| Version field |
<version> |
<version> |
| Interface naming |
Minor variations |
Minor variations |
Dependencies
Blocked by: #196 (Model restructuring for multi-device support)
Note: As of the current branch, Phase 1 and most of Phase 3 from the original plan have already been implemented as part of #196. This issue now covers the pfSense-specific work only.
Current Architecture (Already Implemented)
The multi-device abstraction layer is complete. The design uses a flat CommonDevice struct (not an interface) with an embedded DeviceType field, and a registry-based parser factory pattern.
Platform-Agnostic Model (pkg/model/device.go) ✅ DONE
type DeviceType string
const (
DeviceTypeOPNsense DeviceType = "opnsense"
DeviceTypePfSense DeviceType = "pfsense"
DeviceTypeUnknown DeviceType = ""
)
// CommonDevice is the platform-agnostic root struct — all parsers produce this.
type CommonDevice struct {
DeviceType DeviceType `json:"device_type" yaml:"device_type"`
Version string
System System
// ... all configuration fields
}
Parser Factory (pkg/parser/factory.go) ✅ DONE
// Factory auto-detects device type from the XML root element and delegates
// to the appropriate registered DeviceParser.
func (f *Factory) CreateDevice(
ctx context.Context,
r io.Reader,
deviceTypeOverride common.DeviceType,
validateMode bool,
) (*common.CommonDevice, []common.ConversionWarning, error)
Parser Registry (pkg/parser/registry.go) ✅ DONE
// DeviceParser implementations register themselves via init() + blank import.
type DeviceParser interface {
Parse(ctx context.Context, r io.Reader) (*common.CommonDevice, []common.ConversionWarning, error)
ParseAndValidate(ctx context.Context, r io.Reader) (*common.CommonDevice, []common.ConversionWarning, error)
}
// Register adds a constructor for a device type (called from init()).
func Register(deviceType string, fn ConstructorFunc)
CLI --device-type Flag (cmd/shared_flags.go) ✅ DONE
The --device-type flag, validateDeviceType(), and resolveDeviceType() are already wired into the CLI.
Proposed Solution — What Remains
Proposed Package Layout
pkg/
schema/
pfsense/
document.go # PfSenseDocument XML structs (xml:"pfsense" root) (NEW)
parser/
pfsense/
parser.go # pfSense DeviceParser + init() registration (NEW)
parser_test.go # Unit tests (NEW)
internal/
validator/
pfsense.go # ValidatePfSenseDocument() (NEW)
converter/
markdown.go # Update title to use device_type (not hardcoded) (MODIFY)
testdata/
pfsense/
config-2.6.x.xml # pfSense 2.6.x fixture (NEW)
config-2.7.x.xml # pfSense 2.7.x fixture (NEW)
pfSense Schema Root (pkg/schema/pfsense/document.go)
type PfSenseDocument struct {
XMLName xml.Name `xml:"pfsense"`
Version string `xml:"version"`
System PfSenseSystem `xml:"system"`
// ...
}
pfSense Parser (pkg/parser/pfsense/parser.go)
// Parser implements DeviceParser for pfSense configuration files.
type Parser struct {
decoder parser.XMLDecoder
}
// init registers this parser with the global registry.
func init() {
parser.Register("pfsense", func(d parser.XMLDecoder) parser.DeviceParser {
return NewParser(d)
})
}
func (p *Parser) Parse(ctx context.Context, r io.Reader) (*common.CommonDevice, []common.ConversionWarning, error)
func (p *Parser) ParseAndValidate(ctx context.Context, r io.Reader) (*common.CommonDevice, []common.ConversionWarning, error)
Implementation Phases
Phase 1: Multi-Device Abstraction Layer ✅ COMPLETE
pkg/model/device.go — DeviceType constants, CommonDevice struct with DeviceType field
pkg/parser/factory.go — Factory.CreateDevice() with auto-detection via root-element peek
pkg/parser/registry.go — DeviceParserRegistry, DefaultRegistry(), Register()
pkg/parser/opnsense/parser.go — OPNsense DeviceParser, registered in init()
internal/cfgparser/ — low-level XML streaming parser (unchanged)
Phase 2: pfSense Parser Implementation ⬜ TODO
- Define pfSense schema structs with
xml:"pfsense" root tag in pkg/schema/pfsense/
- Implement streaming token-based
DeviceParser in pkg/parser/pfsense/parser.go, registered via init()
- Add
ValidatePfSenseDocument() in internal/validator/pfsense.go, reusing existing IP/CIDR/MTU helpers from internal/validator/
- Apply same security hardening: XXE prevention, XML bomb protection, charset normalization (via shared
internal/cfgparser reader)
Phase 3: CLI and Converter Integration ⬜ PARTIALLY DONE
- Converters (
markdown.go, json.go, yaml.go) already accept *common.CommonDevice — device_type field is already in JSON/YAML output ✅
--device-type flag already wired in cmd/shared_flags.go ✅
- Remaining: Update the markdown output title in
internal/converter/markdown.go — currently hardcoded as "OPNsense Configuration", should be device-type-aware (e.g. "pfSense Configuration")
- Remaining: Optionally add a typed
ErrUnsupportedConfigType sentinel to internal/cfgparser/errors.go (currently the factory returns inline fmt.Errorf)
Phase 4: Testing and Documentation ⬜ TODO
- Add pfSense 2.6.x and 2.7.x test fixtures in
testdata/pfsense/
- Unit tests in
pkg/parser/pfsense/parser_test.go
- Integration tests for
Factory.CreateDevice() auto-detection with pfSense configs
- Update
README.md to announce pfSense support
- Update
docs/dev-guide/architecture.md to describe the registry-based parser pattern
Research & Analysis
Acceptance Criteria
Functional Requirements:
Code Quality:
Security:
Documentation:
Technical Notes
How to add a new device type (based on existing OPNsense implementation):
- Create schema structs in
pkg/schema/<device>/
- Create a converter that maps schema →
*common.CommonDevice (set DeviceType field)
- Create a
DeviceParser in pkg/parser/<device>/parser.go that calls the converter
- Register in
init(): parser.Register("<root-element>", func(d parser.XMLDecoder) parser.DeviceParser { ... })
- Blank-import the parser package from
main.go to trigger init()
Security — reuse existing infrastructure:
- Input size limiting: use
io.LimitReader wrapping the incoming reader (mirrors internal/cfgparser.DefaultMaxInputSize)
- Charset handling: delegate to
internal/cfgparser.charsetReader or replicate its logic
- Entity map: set
dec.Entity = map[string]string{} on the XML decoder
Backward Compatibility:
- All existing OPNsense tests must continue to pass without modification
- The low-level
internal/cfgparser.XMLParser is unchanged; pfSense uses its own XML decoder
Related Issues & PRs
Summary
Add comprehensive support for parsing pfSense
config.xmlconfiguration files using the existing multi-device abstraction layer. The device-agnosticCommonDevicemodel, parser factory, and--device-typeCLI flag are already in place — what remains is the pfSense-specific schema, parser, and validator, plus updating the markdown converter title and adding test coverage.Background
pfSense and OPNsense share a common heritage (OPNsense forked from pfSense in 2014), resulting in similar but distinct XML configuration formats. Both store firewall state in a single
config.xmlfile, but the root structure and several field names differ enough to require explicit support.Key structural differences:
<opnsense><pfsense><passwd><bcrypt-hash>rootadmin<version><version>Dependencies
Note: As of the current branch, Phase 1 and most of Phase 3 from the original plan have already been implemented as part of #196. This issue now covers the pfSense-specific work only.
Current Architecture (Already Implemented)
The multi-device abstraction layer is complete. The design uses a flat
CommonDevicestruct (not an interface) with an embeddedDeviceTypefield, and a registry-based parser factory pattern.Platform-Agnostic Model (
pkg/model/device.go) ✅ DONEParser Factory (
pkg/parser/factory.go) ✅ DONEParser Registry (
pkg/parser/registry.go) ✅ DONECLI
--device-typeFlag (cmd/shared_flags.go) ✅ DONEThe
--device-typeflag,validateDeviceType(), andresolveDeviceType()are already wired into the CLI.Proposed Solution — What Remains
Proposed Package Layout
pfSense Schema Root (
pkg/schema/pfsense/document.go)pfSense Parser (
pkg/parser/pfsense/parser.go)Implementation Phases
Phase 1: Multi-Device Abstraction Layer ✅ COMPLETE
pkg/model/device.go—DeviceTypeconstants,CommonDevicestruct withDeviceTypefieldpkg/parser/factory.go—Factory.CreateDevice()with auto-detection via root-element peekpkg/parser/registry.go—DeviceParserRegistry,DefaultRegistry(),Register()pkg/parser/opnsense/parser.go— OPNsenseDeviceParser, registered ininit()internal/cfgparser/— low-level XML streaming parser (unchanged)Phase 2: pfSense Parser Implementation ⬜ TODO
xml:"pfsense"root tag inpkg/schema/pfsense/DeviceParserinpkg/parser/pfsense/parser.go, registered viainit()ValidatePfSenseDocument()ininternal/validator/pfsense.go, reusing existing IP/CIDR/MTU helpers frominternal/validator/internal/cfgparserreader)Phase 3: CLI and Converter Integration ⬜ PARTIALLY DONE
markdown.go,json.go,yaml.go) already accept*common.CommonDevice—device_typefield is already in JSON/YAML output ✅--device-typeflag already wired incmd/shared_flags.go✅internal/converter/markdown.go— currently hardcoded as"OPNsense Configuration", should be device-type-aware (e.g."pfSense Configuration")ErrUnsupportedConfigTypesentinel tointernal/cfgparser/errors.go(currently the factory returns inlinefmt.Errorf)Phase 4: Testing and Documentation ⬜ TODO
testdata/pfsense/pkg/parser/pfsense/parser_test.goFactory.CreateDevice()auto-detection with pfSense configsREADME.mdto announce pfSense supportdocs/dev-guide/architecture.mdto describe the registry-based parser patternResearch & Analysis
Acceptance Criteria
Functional Requirements:
config.xmlfiles parse successfully for versions 2.6.x and 2.7.x<pfsense>)device_type: pfsenseappears in JSON/YAML output--device-type pfsenseflag allows manual overrideCode Quality:
DeviceParserinterface aspkg/parser/opnsense/parser.goinit()inpkg/parser/pfsense/parser.gointernal/validator/helpersSecurity:
xml.Decoder+ explicitdec.Entity = map[string]string{})io.LimitReaderwithDefaultMaxInputSize)internal/cfgparser.charsetReaderDocumentation:
DeviceParserRegistryand factory patternTechnical Notes
How to add a new device type (based on existing OPNsense implementation):
pkg/schema/<device>/*common.CommonDevice(setDeviceTypefield)DeviceParserinpkg/parser/<device>/parser.gothat calls the converterinit():parser.Register("<root-element>", func(d parser.XMLDecoder) parser.DeviceParser { ... })main.goto triggerinit()Security — reuse existing infrastructure:
io.LimitReaderwrapping the incoming reader (mirrorsinternal/cfgparser.DefaultMaxInputSize)internal/cfgparser.charsetReaderor replicate its logicdec.Entity = map[string]string{}on the XML decoderBackward Compatibility:
internal/cfgparser.XMLParseris unchanged; pfSense uses its own XML decoderRelated Issues & PRs