OPNsense / pfSense config.xml Data Structure Research#
Research conducted Feb 2025 by analyzing upstream OPNsense and pfSense source code, config.xml samples, and the Go schema package. This document informs parsing accuracy and identifies gaps in our data model.
| Source | URL / Path | Purpose |
|---|---|---|
| OPNsense config.xml.sample | src/etc/config.xml.sample (GitHub) | Default configuration template |
| OPNsense FilterRule.php | src/opnsense/mvc/app/models/OPNsense/Firewall/FilterRule.php | Rule processing logic |
| OPNsense Rule.php | src/opnsense/mvc/app/models/OPNsense/Firewall/Rule.php | MVC model definitions |
| pfSense filter.inc | src/etc/inc/filter.inc | pf rule generation from config.xml |
| pfSense firewall_rules_edit.php | src/usr/local/www/firewall_rules_edit.php | Web UI address handling |
| pfSense Bug #6893 | Redmine issue tracker | Self-closing tag inconsistency fix |
| Go schema package | pkg/schema/opnsense/*.go | Our current data model |
1. XML Boolean Patterns#
OPNsense/pfSense uses two distinct patterns for boolean-like values. Understanding these is critical for correct Go type selection.
1a. Presence-Based Booleans (Self-Closing Tags)#
Element existing = true, absent = false. Content is irrelevant.
Upstream PHP pattern: isset($rule['disabled']) or !empty($rule['disabled'])
Go type: BoolFlag (custom type in pkg/schema/opnsense/common.go)
| Element | Parent Context | Upstream Evidence |
|---|---|---|
<any/> | <source>, <destination> | isset($this->rule['source']['any']) in FilterRule.php |
<not/> | <source>, <destination> | isset($adr['not']) in filter.inc |
<disabled/> | <rule> (filter, NAT) | isset($rule['disabled']) in filter.inc |
<log/> | <rule> (filter) | isset($rule['log']) sets $filterent['log'] = true |
<quick/> | <rule> (filter) | isset($rule['quick']) in OPNsense |
<disableconsolemenu/> | <system> | config.xml.sample: self-closing |
<enable/> | <rrd> | config.xml.sample: self-closing |
<tcpflags_any/> | <rule> (filter) | isset($rule['tcpflags_any']) |
<nopfsync/> | <rule> (filter) | isset($rule['nopfsync']) |
<allowopts/> | <rule> (filter) | $filterent['allowopts'] = true |
<disablereplyto/> | <rule> (filter) | $filterent['disablereplyto'] = true |
<nosync/> | <rule> (filter, NAT) | $natent['nosync'] = true |
<nordr/> | <rule> (NAT inbound) | isset($natent['nordr']) |
<staticnatport/> | <rule> (NAT outbound) | Presence-checked in OPNsense |
<nonat/> | <rule> (NAT outbound) | Presence-checked |
<interfacenot/> | <rule> (filter) | !empty($rule['interfacenot']) |
<nottagged/> | <rule> (filter) | Match packets NOT tagged |
<trigger_initial_wizard/> | <opnsense> (root) | First-boot wizard trigger |
pfSense Bug #6893 note: Prior to pfSense 2.3.3, some code produced <tag/> while other code produced <tag></tag>. Both forms are valid XML and our *string / BoolFlag types handle both correctly via Go's encoding/xml.
1b. Value-Based Booleans#
Element contains 1, yes, or a specific value. Absent or empty = false.
Upstream PHP pattern: $config['system']['ipv6allow'] == "1"
Go type: string with value check, or potentially a custom type
| Element | Parent | Values | Notes |
|---|---|---|---|
<enable> | <interfaces><wan> | 1 | Interface enable/disable |
<blockpriv> | <interfaces><wan> | 1 | Block private networks |
<blockbogons> | <interfaces><wan> | 1 | Block bogon networks |
<dnsallowoverride> | <system> | 1 | Allow DNS override |
<ipv6allow> | <system> | 1 | IPv6 enabled |
<usevirtualterminal> | <system> | 1 | Virtual terminal |
<pf_share_forward> | <system> | 1 | Shared forwarding |
<lb_use_sticky> | <system> | 1 | Sticky load balancing |
<disablenatreflection> | <system> | yes | NAT reflection disabled |
<enable> | various OPNsense modules | 1 | Service/feature enabled |
1c. Design Rationale#
The distinction is not arbitrary:
- Presence-based = flags typically absent (disabled, negation, special modes)
- Value-based = feature toggles typically enabled (with explicit
1)
2. Source/Destination Structure#
The <source> and <destination> elements are the most complex sub-structures in firewall rules. Understanding their design is critical.
2a. XML Structure Examples#
Match any address:
<source>
<any />
</source>
Match interface subnet:
<source>
<network>lan</network>
</source>
Match specific IP/CIDR:
<source>
<address>192.168.1.0/24</address>
</source>
Match alias:
<source>
<address>MyAlias</address>
</source>
Negated match:
<source>
<not />
<network>lan</network>
</source>
With port (TCP/UDP only):
<destination>
<network>wan</network>
<port>443</port>
</destination>
Port range:
<destination>
<any />
<port>8000-9000</port>
</destination>
2b. Mutual Exclusivity#
<any>, <network>, and <address> are mutually exclusive. Resolution priority (from OPNsense legacyMoveAddressFields):
<network>(highest priority)<address><any>/ implicit any (when none present)
2c. Valid <network> Values#
- Interface names:
lan,wan,opt1,opt2, ... - Interface IP:
lanip,wanip,opt1ip - Special:
(self)(all local IPs) - Interface group names
- VIP names
2d. Port Range Delimiters#
- In config.xml: hyphen (
80-443) - In pf rules: colon (
80:443) - OPNsense/pfSense code handles the conversion
3. Filter Rule Fields Reference#
3a. Currently Modeled (in Rule struct)#
| Field | XML Element | Go Type | Status | Phase |
|---|---|---|---|---|
| Type | <type> | string | Correct | Initial |
| Descr | <descr> | string | Correct | Initial |
| Interface | <interface> | InterfaceList | Correct | Initial |
| IPProtocol | <ipprotocol> | string | Correct | Initial |
| Protocol | <protocol> | string | Correct | Initial |
| Source | <source> | Source | Complete | Phase 1 |
| Destination | <destination> | Destination | Complete | Phase 1 |
| Target | <target> | string | Correct | Initial |
| SourcePort | <sourceport> | string | Correct | Initial |
| Disabled | <disabled> | BoolFlag | Converted | Phase 2 |
| Quick | <quick> | BoolFlag | Converted | Phase 2 |
| Log | <log> | BoolFlag | Added | Phase 2 |
| Floating | <floating> | string | Added | Phase 2 |
| Gateway | <gateway> | string | Added | Phase 2 |
| Direction | <direction> | string | Added | Phase 2 |
| Tracker | <tracker> | string | Added | Phase 2 |
| StateType | <statetype> | string | Added | Phase 2 |
| Updated | <updated> | *Updated | Correct | Initial |
| Created | <created> | *Created | Correct | Initial |
| UUID | uuid attr | string | Correct | Initial |
3b. Rate-Limiting and Advanced Fields (Added in Phase 3)#
| Field | XML Element | Go Type | Phase |
|---|---|---|---|
| MaxSrcNodes | <max-src-nodes> | string | Phase 3 |
| MaxSrcConn | <max-src-conn> | string | Phase 3 |
| MaxSrcConnRate | <max-src-conn-rate> | string | Phase 3 |
| MaxSrcConnRates | <max-src-conn-rates> | string | Phase 3 |
| TCPFlags1 | <tcpflags1> | string | Phase 3 |
| TCPFlags2 | <tcpflags2> | string | Phase 3 |
| TCPFlagsAny | <tcpflags_any> | BoolFlag | Phase 3 |
| ICMPType | <icmptype> | string | Phase 3 |
| ICMP6Type | <icmp6-type> | string | Phase 3 |
| StateTimeout | <statetimeout> | string | Phase 3 |
| AllowOpts | <allowopts> | BoolFlag | Phase 3 |
| DisableReplyTo | <disablereplyto> | BoolFlag | Phase 3 |
| NoPfSync | <nopfsync> | BoolFlag | Phase 3 |
| NoSync | <nosync> | BoolFlag | Phase 3 |
3c. Remaining Missing Fields (LOW importance)#
| Field | XML Element | Recommended Go Type |
|---|---|---|
| Tag | <tag> | string |
| Tagged | <tagged> | string |
| OS | <os> | string |
| DSCP | <dscp> | string |
| DNPipe | <dnpipe> | string |
| PDNPipe | <pdnpipe> | string |
| DefaultQueue | <defaultqueue> | string |
| AckQueue | <ackqueue> | string |
| Max | <max> | string |
| MaxSrcStates | <max-src-states> | string |
| VLANPrio | <vlanprio> | string |
| VLANPrioSet | <vlanprioset> | string |
| SetPrio | <set-prio> | string |
| SetPrioLow | <set-prio-low> | string |
| InterfaceNot | <interfacenot> | BoolFlag |
| NotTagged | <nottagged> | BoolFlag |
4. Schema Gaps: Source and Destination#
RESOLVED (Phase 1): All missing fields added directly to Source/Destination structs. Fields were added directly rather than via
RuleLocationembedding to preserve the*stringAny field and maintain backward compatibility with existing consumers.
4a. Current Implementation (Post-Phase 1)#
// security.go
type Source struct {
Any *string `xml:"any,omitempty"`
Network string `xml:"network,omitempty"`
Address string `xml:"address,omitempty"`
Port string `xml:"port,omitempty"`
Not BoolFlag `xml:"not,omitempty"`
}
type Destination struct {
Any *string `xml:"any,omitempty"`
Network string `xml:"network,omitempty"`
Address string `xml:"address,omitempty"`
Port string `xml:"port,omitempty"`
Not BoolFlag `xml:"not,omitempty"`
}
4b. Previously Missing Fields (Now Resolved)#
| Field | XML Element | Impact | Resolution |
|---|---|---|---|
Address | <address> | CRITICAL - IP/CIDR/alias data | Phase 1 |
Not | <not> | HIGH - Negated rule semantics | Phase 1 |
Port (Source only) | <port> | MEDIUM - Source port matching | Phase 1 |
Helper methods IsAny(), EffectiveAddress(), and Equal() were added to both types for safe comparison and address resolution following OPNsense priority rules.
4c. Existing RuleLocation Type#
pkg/schema/opnsense/common.go defines RuleLocation with similar fields. It remains available for other use cases but was not embedded into Source/Destination to avoid complexity with the *string Any field and XML marshaling behavior.
5. Type Mismatches: string vs BoolFlag#
Resolution Status#
Converted to BoolFlag (presence-based — isset() in PHP):
- Rule: Disabled, Quick, Log (security.go) — Phase 2
- NATRule: Disabled, Log (security.go) — Phase 2
- InboundRule: Disabled, Log (security.go) — Phase 2
- System: DisableConsoleMenu (system.go) — Phase 3
- Firmware: Type, Subscription, Reboot (system.go) — Phase 3
- User: Expires, AuthorizedKeys, IPSecPSK, OTPSeed (system.go) — Phase 3
- System.RRD: Enable (system.go) — Phase 3
- Rrd: Enable (services.go) — Phase 3
- OpnSenseDocument: TriggerInitialWizard (opnsense.go) — Phase 3
Kept as string (value-based — == "1" in PHP):
The following fields use OPNsense MVC value-based semantics where <field>0</field> is valid and distinct from absent. BoolFlag would incorrectly treat <field>0</field> as true (element present), breaking the == "1" / == "0" distinction. These remain string:
5a. Security (security.go) — value-based, kept as string#
| Struct | Field | Type | Rationale |
|---|---|---|---|
| IDS.General | Enabled | string | MVC field, uses == "1" with helper method |
| IDS.General | Ips | string | MVC field, uses == "1" with helper method |
| IDS.General | Promisc | string | MVC field, uses == "1" with helper method |
| IDS.EveLog.HTTP | Enable | string | MVC field, value-based |
| IDS.EveLog.HTTP | Extended | string | MVC field, value-based |
| IDS.EveLog.HTTP | DumpAllHeaders | string | MVC field, value-based |
| IDS.EveLog.TLS | Enable | string | MVC field, value-based |
| IDS.EveLog.TLS | Extended | string | MVC field, value-based |
| IDS.EveLog.TLS | SessionResumption | string | MVC field, value-based |
| IPsec.General | Enabled | string | MVC field, uses FormatBoolean() |
| IPsec.General | Disablevpnrules | string | MVC field, uses FormatBoolean() |
5b. Services (services.go) — value-based, kept as string#
| Struct | Field | Type | Rationale |
|---|---|---|---|
| Unbound | Enable | string | Heavily used with == "1" across 10+ files |
| Monit.General | Enabled | string | MVC field, value-based |
| Monit.General | Ssl | string | MVC field, value-based |
| Monit.General | Sslverify | string | MVC field, value-based |
| Monit.General | HttpdEnabled | string | MVC field, value-based |
| Monit.Alert | Enabled | string | MVC field, value-based |
| MonitService | Enabled | string | MVC field, value-based |
5c. OPNsense module (opnsense.go) — value-based, kept as string#
| Struct | Field | Type | Rationale |
|---|---|---|---|
| Kea.Dhcp4.General | Enabled | string | MVC field, value-based |
| Kea.Dhcp4.HighAvailability | Enabled | string | MVC field, value-based |
| UnboundPlus.General | Enabled | string | MVC field, value-based |
| UnboundPlus.General | Stats | string | MVC field, value-based |
| UnboundPlus.General | Dnssec | string | MVC field, value-based |
| UnboundPlus.General | DNS64 | string | MVC field, value-based |
| UnboundPlus.General | RegisterDHCP* (x3) | string | MVC field, value-based |
| UnboundPlus.General | No* fields (x2) | string | MVC field, value-based |
| UnboundPlus.General | Txtsupport | string | MVC field, value-based |
| UnboundPlus.General | Cacheflush | string | MVC field, value-based |
| UnboundPlus.General | EnableWpad | string | MVC field, value-based |
| UnboundPlus.Dnsbl | Enabled | string | MVC field, value-based |
| UnboundPlus.Dnsbl | Safesearch | string | MVC field, value-based |
| UnboundPlus.Forwarding | Enabled | string | MVC field, value-based |
| SyslogInternal.General | Enabled | string | MVC field, value-based |
| Netflow.Capture | EgressOnly | string | MVC field, value-based |
| Netflow.Collect | Enable | string | MVC field, value-based |
5d. DHCP (dhcp.go) — value-based, kept as string#
| Struct | Field | Type | Rationale |
|---|---|---|---|
| DhcpdInterface | Enable | string | Value-based (<enable>1</enable>), heavy use |
Note on value-based fields: These use OPNsense MVC pattern <field>1</field> / <field>0</field>. Converting to BoolFlag would break semantics because BoolFlag.UnmarshalXML treats any present element as true, regardless of content — so <enabled>0</enabled> would incorrectly become true.
6. NAT Structure Issues#
RESOLVED (Phase 4): All missing NAT outbound and inbound fields have been added.
6a. NAT XML Path#
In OPNsense config.xml, inbound NAT (port forward) rules are at <nat><rule>, not <nat><inbound><rule>. Our schema uses xml:"inbound>rule" path mapping.
6b. NAT Outbound Rule Fields (Added in Phase 4)#
| Field | XML Element | Type | Status |
|---|---|---|---|
| StaticNatPort | <staticnatport> | BoolFlag | Added |
| NoNat | <nonat> | BoolFlag | Added |
| NatPort | <natport> | string | Added |
| PoolOptsSrcHashKey | <poolopts_sourcehashkey> | string | Added |
6c. NAT Inbound Rule Fields (Added in Phase 4)#
| Field | XML Element | Type | Status |
|---|---|---|---|
| NATReflection | <natreflection> | string | Added |
| AssociatedRuleID | <associated-rule-id> | string | Added |
| NoRDR | <nordr> | BoolFlag | Added |
| NoSync | <nosync> | BoolFlag | Added |
| LocalPort | <local-port> | string | Added |
6d. Outbound NAT Mode Values#
| Value | Meaning |
|---|---|
automatic | Automatic outbound NAT (default) |
hybrid | Hybrid: auto + manual rules |
advanced | Manual outbound NAT only |
disabled | Disable outbound NAT |
7. Key XML Design Decisions#
7a. Dynamic Interface Keys#
The <interfaces> section uses dynamic element names (<wan>, <lan>, <opt0>) rather than repeated <interface name="wan">. This requires custom unmarshaling in Go. Our map-based approach in Interfaces and Dhcpd types is correct.
7b. Comma-Separated Lists#
Several fields pack multiple values into a single element:
<interface>wan,lan,opt1</interface>(floating rules)<icmptype>3,11,0</icmptype>(ICMP type list)- Space-separated:
<timeservers>0.ntp.org 1.ntp.org</timeservers>
Our InterfaceList custom type correctly handles the comma-separated case.
7c. UUID vs Tracker#
- OPNsense:
uuidattribute on rule elements (<rule uuid="...">) - pfSense:
<tracker>element (integer, auto-generated frommicrotime) - Both may be present in migrated configs
7d. Legacy vs MVC Model Split#
OPNsense maintains two parallel systems:
- Legacy: Rules in
<filter><rule>and<nat><rule>(pfSense-compatible) - MVC/New-style: Rules in
<OPNsense><Firewall><Filter>with<rules>,<snatrules>, etc.
Both are loaded by pf_firewall(). Our schema currently only models the legacy format.
8. Correctly Implemented Patterns#
8a. Source.Any / Destination.Any as *string#
Go's encoding/xml produces "" for both <any/> and absent elements when using plain string. Using *string correctly distinguishes presence (non-nil) from absence (nil). Validated against FilterRule.php's isset() pattern.
8b. BoolFlag Custom Type#
Correctly implements presence-based boolean semantics: UnmarshalXML sets true on any element presence, regardless of content.
8c. InterfaceList Custom Type#
Correctly handles comma-separated interface lists for floating rules.
8d. Interfaces and Dhcpd Map Types#
Correctly handle dynamic interface element names via custom UnmarshalXML/MarshalXML.
8e. RuleLocation Struct#
Already has all needed fields for complete source/destination modeling. Just needs to be connected to the actual Source/Destination types.
9. Action Items (Priority Order)#
CRITICAL: Add— COMPLETEAddressfield toSourceandDestinationstructsHIGH: Add— COMPLETENotfield (BoolFlag) toSourceandDestinationHIGH: Add— COMPLETEPortfield toSourcestructHIGH: Add— COMPLETELogfield (BoolFlag) toRulestructHIGH: Convert— COMPLETERule.Disabled,NATRule.Disabled,InboundRule.DisabledfromstringtoBoolFlagHIGH: Add— COMPLETEFloating,Gateway,Directionfields toRuleMEDIUM: Convert— COMPLETEstruct{}fields in system.go toBoolFlagMEDIUM: Add rate-limiting fields to— COMPLETE (Phase 3: 14 fields added — rate-limiting, TCP/ICMP, state/advanced)Rule(max-src-nodes, etc.)- MEDIUM: Convert IDS/IPsec Enabled fields from
stringtoBoolFlag - LOW: Add remaining missing filter rule fields (tag, tagged, DSCP, etc.)
LOW: Add missing NAT rule fields— COMPLETE (Phase 4: NATRule +4 fields, InboundRule +5 fields)
10. Remaining Work#
10a. Medium Priority#
| Item | Description | Rationale |
|---|---|---|
| #9 | Convert IDS/IPsec Enabled fields from string to BoolFlag | These use MVC value-based semantics (== "1") which BoolFlag cannot represent correctly. Requires a new MVCBool type or keeping as string with helper methods. Current helper methods (IsEnabled(), IsIPSMode()) work correctly with string. |
10b. Low Priority#
| Item | Description | Fields |
|---|---|---|
| #10 | Add remaining filter rule fields | tag, tagged, os, dscp, dnpipe, pdnpipe, defaultqueue, ackqueue, max, max-src-states, vlanprio, vlanprioset, set-prio, set-prio-low, interfacenot, nottagged |
10c. Phase Summary#
| Phase | Scope | Fields Added | Status |
|---|---|---|---|
| 1 | Source/Destination gaps | Address, Port (Source), Not | Complete |
| 2 | High-priority Rule fields | Log, Disabled→BoolFlag, Quick→BoolFlag, Floating, Gateway, Direction, Tracker, StateType | Complete |
| 3 | Rate-limiting and advanced fields | 14 fields (max-src-*, TCP/ICMP, state/advanced) | Complete |
| 4 | NAT rule enhancements | NATRule +4 fields, InboundRule +5 fields | Complete |
| 5 | Documentation and validation | Research doc updates, field reference, validator enhancements | Complete |
11. Kea DHCP4 XML Structure#
This section documents the full <Kea><dhcp4> XML structure in OPNsense config.xml as parsed by the Go schema package (pkg/schema/opnsense/kea.go).
11a. <dhcp4> Top-Level Structure#
<dhcp4 version="1.0.4">
<general>
<enabled>1</enabled>
<interfaces>lan</interfaces>
<fwrules>1</fwrules>
<valid_lifetime>4000</valid_lifetime>
</general>
<ha>
<enabled>0</enabled>
<this_server_name />
<max_unacked_clients>2</max_unacked_clients>
</ha>
<subnets>
<subnet4 uuid="..."> ... </subnet4>
</subnets>
<reservations>
<reservation uuid="..."> ... </reservation>
</reservations>
<ha_peers />
</dhcp4>
Go type mapping:
| XML Element | Go Field | Go Type | Notes |
|---|---|---|---|
<dhcp4> | OPNsense.Kea.Dhcp4 | schema.KeaDhcp4 | Defined in pkg/schema/opnsense/kea.go |
<general> | KeaDhcp4.General | inline struct | value-based booleans ("0" / "1") |
<ha> | KeaDhcp4.HighAvailability | inline struct | value-based booleans |
<subnets> | KeaDhcp4.Subnets | []KeaSubnet | MVC ArrayField; elements named <subnet4> |
<reservations> | KeaDhcp4.Reservations | []KeaReservation | Each element references parent subnet by UUID |
<ha_peers> | KeaDhcp4.HAPeers | string |
11b. KeaSubnet (<subnet4> element)#
Each <subnet4> element carries a uuid attribute and the following child elements:
| XML Element | Go Field | Go Type | Notes |
|---|---|---|---|
uuid (attr) | KeaSubnet.UUID | string | Used by reservations to reference the parent subnet |
<subnet> | KeaSubnet.Subnet | string | CIDR notation (e.g., 192.168.1.0/24) |
<option_data_autocollect> | KeaSubnet.OptionDataAutocollect | string | "0" or "1", value-based |
<option_data> | KeaSubnet.OptionData | KeaOptionData | See §11c |
<pools> | KeaSubnet.Pools | string | Newline-separated pool ranges ("start-end" or CIDR notation) |
<next_server> | KeaSubnet.NextServer | string | |
<description> | KeaSubnet.Description | string | Human-readable label |
Pools format note: The <pools> field stores entries from OPNsense's KeaPoolsField. Each newline-separated entry is either a range (192.168.1.100-192.168.1.200) or CIDR notation. The converter uses the first entry as DHCPScope.Range; additional entries emit a SeverityInfo warning.
11c. KeaOptionData (<option_data> element)#
Used on both <subnet4> and <reservation> elements.
| XML Element | Go Field | Go Type | Notes |
|---|---|---|---|
<domain_name_servers> | KeaOptionData.DomainNameServers | string | Comma-separated IPs |
<domain_search> | KeaOptionData.DomainSearch | string | Comma-separated domain names |
<routers> | KeaOptionData.Routers | string | Gateway; comma-separated IPs |
<domain_name> | KeaOptionData.DomainName | string | |
<ntp_servers> | KeaOptionData.NTPServers | string | Comma-separated IPs |
<tftp_server_name> | KeaOptionData.TFTPServerName | string | |
<boot_file_name> | KeaOptionData.BootFileName | string |
11d. KeaReservation (<reservation> element)#
| XML Element | Go Field | Go Type | Notes |
|---|---|---|---|
uuid (attr) | KeaReservation.UUID | string | |
<subnet> | KeaReservation.Subnet | string | UUID of the parent <subnet4> (not the CIDR) |
<ip_address> | KeaReservation.IPAddress | string | |
<hw_address> | KeaReservation.HWAddress | string | MAC address |
<hostname> | KeaReservation.Hostname | string | |
<description> | KeaReservation.Description | string | |
<option_data> | KeaReservation.OptionData | KeaOptionData | See §11c |
11e. Converter Behavior (convertKeaDHCPScopes)#
convertKeaDHCPScopes() in pkg/parser/opnsense/converter_subsystems.go converts Kea subnets into []common.DHCPScope entries and appends them to CommonDevice.DHCP alongside ISC scopes (which carry Source: "isc"). Key behaviors:
- Produces no scopes when
<subnets>is empty. Scopes are emitted even when Kea is disabled (Enabledreflects the server state). - Each
DHCPScopehasSource: "kea"andEnabledset from the Kea general enabled flag. - Option data is mapped:
Routers→Gateway,DomainNameServers→DNSServer,NTPServers→NTPServer. For comma-separated values, only the first entry is used. - Reservations are grouped by their
<subnet>UUID and attached asStaticLeaseson the matching scope. Reservations referencing a nonexistent subnet UUID are not attached and emit a conversion warning. - ISC DHCP scopes (OPNsense and pfSense) carry
Source: "isc". An emptySourceis treated as"isc"for backward compatibility.