Documents
Dual Validator Synchronization
Dual Validator Synchronization
Type
Topic
Status
Published
Created
Feb 27, 2026
Updated
Mar 19, 2026
Created by
Dosu Bot
Updated by
Dosu Bot

Dual Validator Synchronization#

Dual Validator Synchronization is a project-specific architectural pattern implemented in the opnDossier project to maintain consistency of validation logic across multiple validation enforcement points. The pattern addresses the challenge of preventing validation logic drift between processor-level validation and schema-level validation components by centralizing validation rules in a shared constants layer.

The pattern emerged from the need to validate OPNsense firewall configurations at two distinct stages: lightweight semantic validation during the processing pipeline and comprehensive authoritative validation of raw XML schemas. By maintaining validation whitelists as a "single source of truth" in shared constants, the architecture ensures that both validators enforce identical business rules for critical configuration values such as power management modes and system optimization settings.

The pattern complements the Stats Tracking Pattern, which separates validation check logic from metrics updates to prevent double-counting during fallback validation scenarios.

Architecture and Components#

Two-Layer Validation System#

The Dual Validator Synchronization pattern implements validation at two distinct architectural layers:

Schema-Level Validation operates on raw XML structures through the internal/validator package, performing comprehensive validation of schema.OpnSenseDocument objects. This authoritative validator validates XML structure, OPNsense-specific constraints, and device type-specific checks with deep granularity. The validation logic is organized across domain-specific files: validate_system.go for system configuration, validate_network.go for network interfaces and DHCP, and validate_security.go for firewall rules and NAT.

Pipeline-Level Validation operates through internal/processor/validate.go, performing best-effort semantic validation of normalized common.CommonDevice objects. This lightweight validator acts as a pipeline guard, catching obvious misconfigurations early without duplicating comprehensive schema-level checks.

Shared Constants Layer#

The internal/constants/constants.go file serves as the central repository for all validation whitelists and domain constants requiring synchronization between validators. The file contains 124 lines defining application-wide constants, validation maps, and configuration defaults.

The constants layer uses the map[string]struct{} pattern, a Go idiom for efficient set membership testing where the empty struct consumes zero bytes of memory, making it optimal for whitelist validation.

File Organization#

internal/
├── constants/
│ └── constants.go # Shared validation whitelists (single source of truth)
├── processor/
│ ├── validate.go # Pipeline validation (best-effort semantic checks)
│ └── validate_helpers.go # Helper functions for processor validation
└── validator/
    ├── opnsense.go # Main entry point and shared helper functions
    ├── validate_network.go # Network interface and DHCP validation
    ├── validate_security.go # Firewall filter and NAT validation
    └── validate_system.go # System-level configuration validation

Validation Whitelists#

Power Management Modes#

The ValidPowerdModes whitelist defines five allowed FreeBSD power management modes:

// ValidPowerdModes defines the allowed powerd power modes.
// Shared by processor and validator packages — single source of truth.
var ValidPowerdModes = map[string]struct{}{
    "hadp": {}, // High Adaptive
    "hiadp": {}, // Highest Adaptive
    "adaptive": {}, // Adaptive power management
    "minimum": {}, // Minimum power consumption
    "maximum": {}, // Maximum performance
}

The schema validator validates three powerd mode fields using this whitelist: PowerdACMode, PowerdBatteryMode, and PowerdNormalMode (implemented in validate_system.go). The processor validator performs identical validation on the normalized configuration model.

System Optimization Modes#

The ValidOptimizationModes whitelist defines four system optimization levels:

// ValidOptimizationModes defines the allowed system optimization modes.
// Shared by processor and validator packages — single source of truth.
var ValidOptimizationModes = map[string]struct{}{
    "normal": {}, // Standard optimization
    "high-latency": {}, // Optimized for high-latency networks
    "aggressive": {}, // Aggressive performance optimization
    "conservative": {}, // Conservative/safe optimization
}

Both the schema validator (in validate_system.go) and pipeline validator reference this whitelist to validate system optimization settings.

Network Validation Bounds#

The constants layer also defines network-related validation limits referenced by RFC standards:

  • MaxHostnameLength = 253 (RFC 1035 maximum hostname length)
  • MinPort = 1, MaxPort = 65535 (TCP/UDP port range)
  • MaxIPv4Subnet = 32, MaxIPv6Subnet = 128 (subnet prefix lengths)
  • MinMTU = 68 (RFC 791 minimum for IPv4), MaxMTU = 9000 (jumbo frames)

Implementation Pattern#

Validation Logic#

Both validators implement identical validation logic using map lookup against shared whitelists:

Schema Validator Implementation:

if system.Optimization != "" {
    if _, ok := constants.ValidOptimizationModes[system.Optimization]; !ok {
        errors = append(errors, ValidationError{
            Field: "system.optimization",
            Message: fmt.Sprintf("optimization '%s' must be one of: %v",
                system.Optimization,
                slices.Sorted(maps.Keys(constants.ValidOptimizationModes))),
        })
    }
}

Pipeline Validator Implementation:

if s.Optimization != "" {
    if _, ok := constants.ValidOptimizationModes[s.Optimization]; !ok {
        errors = append(errors,
            ValidationError{Field: "system.optimization", Message: "invalid optimization value"},
        )
    }
}

The pattern ensures consistency: both validators reject the same invalid values and accept the same valid values. The only difference is error message verbosity—the schema validator provides detailed feedback while the pipeline validator uses terse messages.

Anti-Pattern: Duplicated Validation Logic#

The Dual Validator Synchronization pattern explicitly avoids duplicating validation constants:

// ❌ ANTI-PATTERN: Duplicated validation logic
// In processor/validate.go
validModes := []string{"low", "high", "medium"}

// In validator/opnsense.go
validModes := []string{"low", "high", "medium"} // Risk: now out of sync!
// ✓ CORRECT: Centralized constants
// In constants/constants.go
var ValidModes = map[string]struct{}{"low": {}, "high": {}, "medium": {}}

// Both validators reference the same whitelist
if _, ok := constants.ValidModes[value]; !ok { ... }

Error Collection Pattern#

Both validators use structured error collection where validation errors are accumulated and returned together rather than failing fast:

type ValidationError struct {
    Field string
    Message string
}

func (e ValidationError) Error() string {
    return fmt.Sprintf("validation error for field '%s': %s", e.Field, e.Message)
}

This pattern allows users to see all validation failures in a single pass, improving the developer experience by eliminating iterative fix-and-retest cycles.

Relationship to Stats Tracking Pattern#

The Dual Validator Synchronization pattern complements the Stats Tracking Pattern documented in AGENTS.md, which mandates: "Separate check logic from stats updates. Never increment stats inside a function that may be called multiple times for fallback logic — check all candidates first, then update stats once based on the outcome."

The Stats Tracking Pattern applies to validation scenarios involving fallback logic. For example, the sanitizer component implements this pattern when checking redaction rules:

// Check if we should redact (try full path first, then element name)
// Only check - don't update stats yet
should, rule := s.engine.ShouldRedactValue(fullPath, content)
if !should {
    should, rule = s.engine.ShouldRedactValue(currentElement, content)
}

// Stats updated once after determining final outcome
if should {
    s.stats.RedactedFields++
    if rule != nil {
        s.stats.RedactionsByType[rule.Name]++
    }
} else {
    s.stats.SkippedFields++
}

The key principle is that check functions like ShouldRedactValue() are pure functions without side effects, enabling safe multiple calls during fallback logic without double-counting statistics.

While the validation code in internal/processor/validate.go and the internal/validator package does not currently track validation statistics, the Stats Tracking Pattern principle applies: validation functions are designed as pure checkers that return error collections without side effects, allowing them to be called multiple times in complex validation scenarios without corrupting metrics.

Practical Usage Examples#

Adding New Validation Rules#

When adding new validation rules, developers follow this workflow:

1. Define constant in internal/constants/constants.go:

// ValidNewModes defines the allowed new operational modes.
// Shared by processor and validator packages — single source of truth.
var ValidNewModes = map[string]struct{}{
    "option1": {},
    "option2": {},
    "option3": {},
}

2. Reference in schema validator (appropriate domain file in internal/validator/):

if system.NewMode != "" {
    if _, ok := constants.ValidNewModes[system.NewMode]; !ok {
        errors = append(errors, ValidationError{
            Field: "system.newMode",
            Message: fmt.Sprintf("mode '%s' must be one of: %v",
                system.NewMode,
                slices.Sorted(maps.Keys(constants.ValidNewModes))),
        })
    }
}

3. Reference in pipeline validator (internal/processor/validate.go):

if s.NewMode != "" {
    if _, ok := constants.ValidNewModes[s.NewMode]; !ok {
        errors = append(errors,
            ValidationError{Field: "system.newMode", Message: "invalid mode value"},
        )
    }
}

4. Add comprehensive tests meeting the >80% coverage requirement.

Updating Existing Whitelists#

When OPNsense introduces new valid values, updates require modifying only the constants file:

Before (old whitelist):

var ValidPowerdModes = map[string]struct{}{
    "hadp": {},
    "hiadp": {},
    "adaptive": {},
    "minimum": {},
    "maximum": {},
}

After (new mode added):

var ValidPowerdModes = map[string]struct{}{
    "hadp": {},
    "hiadp": {},
    "adaptive": {},
    "minimum": {},
    "maximum": {},
    "balanced": {}, // New OPNsense 24.x mode
}

Both validators immediately recognize the new value without code changes in either validator file.

Use Cases#

System Configuration Hardening#

When operators deploy hardened firewall configurations, both validation points enforce identical security constraints:

  • Pipeline validation rejects non-compliant configurations early in the processing pipeline, preventing wasted processing time
  • Schema validation (via the internal/validator package) provides comprehensive feedback through the CLI validate command
  • Shared constants ensure both enforcement points maintain consistent security posture

Configuration Migration Between OPNsense Versions#

When migrating configurations between OPNsense versions with evolving constraints:

  • Update internal/constants/constants.go once to reflect new version requirements
  • Both validators automatically enforce updated rules
  • No risk of validators diverging on version-specific constraints

Preventing Validation Drift#

The pattern prevents the common failure mode where validators gradually diverge:

Without the pattern:

  • Developer updates processor validation for new mode
  • Forgets to update schema validator
  • Schema validator rejects configurations the processor accepts
  • Users experience inconsistent validation behavior

With the pattern:

  • Developer updates constants.ValidModes
  • Both validators automatically synchronize
  • Impossible for validators to have different rule sets

Code Quality Standards#

The opnDossier project enforces strict development standards for validation code:

Error Handling: Always wrap errors with context using fmt.Errorf with %w format verb.

Testing: Table-driven tests with t.Run() subtests, minimum >80% coverage requirement.

Logging: Structured logging with charmbracelet/log package.

Code Style: Follow Google Go Style Guide, use gofmt.

Validation Standards: Input validation with clear error reporting, size limits to prevent resource exhaustion, proper encoding handling.

Quality Gates: All code must pass linting via golangci-lint and meet coverage requirements before merge.

Validation Helper Functions#

Both validators share similar helper function patterns with pre-compiled regex patterns:

var (
    hostnamePattern = regexp.MustCompile(`^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?$`)
    sysctlNamePattern = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_.]*$`)
    connRatePattern = regexp.MustCompile(`^\d+/\d+$`)
)

Pre-compiling regex patterns at package initialization avoids repeated compilation overhead during validation.

Cross-Reference Validation#

The processor validator implements cross-reference validation using map-based lookups:

// Build interface name set
ifaceSet := make(map[string]struct{}, len(ifaces))
for _, iface := range ifaces {
    if iface.Name != "" {
        ifaceSet[iface.Name] = struct{}{}
    }
}

// Validate DHCP scope references
if scope.Interface != "" {
    if _, ok := ifaceSet[scope.Interface]; !ok {
        errors = append(errors,
            ValidationError{Field: prefix + ".interface",
                Message: "DHCP scope references unknown interface"},
        )
    }
}

This pattern ensures referential integrity—DHCP scopes must reference existing interfaces, users must reference existing groups, and firewall rules must reference defined interfaces.

Inline Whitelists#

Some validation rules use inline whitelists when the values are not shared between validators:

if s.WebGUI.Protocol != "" {
    validProtocols := map[string]struct{}{"http": {}, "https": {}}
    if _, ok := validProtocols[s.WebGUI.Protocol]; !ok {
        errors = append(errors,
            ValidationError{Field: "system.webGui.protocol",
                Message: "invalid web GUI protocol"},
        )
    }
}

These inline whitelists are appropriate when the validation rule is specific to one validator and doesn't need synchronization with the other validator.

Relevant Code Files#

The schema-level validator was refactored in PR #417 to improve code organization by separating validation concerns into domain-specific files while maintaining the same validation behavior and shared constants pattern.

File PathPurposeLines of CodeKey Responsibilities
internal/constants/constants.goShared validation constants124Single source of truth for validation whitelists, network bounds, optimization modes, powerd modes
internal/validator/opnsense.goSchema validator entry point~210Main ValidateOpnSenseDocument entry point and shared helper functions (hostname, timezone, IP, CIDR, port validation)
internal/validator/validate_system.goSystem configuration validation364Validates system config, hostname, timezone, optimization, webgui, power management, bogons, users/groups, sysctl
internal/validator/validate_network.goNetwork validation259Validates network interfaces, DHCP, IP addresses, subnets, MTU, track6
internal/validator/validate_security.goSecurity validation255Validates firewall filter rules, NAT configuration, ports, protocols
internal/processor/validate.goPipeline-level validator537Best-effort semantic validation, early detection of misconfigurations in processing pipeline
internal/processor/validate_helpers.goValidation utilities147Helper functions with pre-compiled regex patterns for hostname, timezone, port, sysctl validation
internal/sanitizer/sanitizer.goStats tracking exampleImplements Stats Tracking Pattern with pure check functions and single-point stats updates
AGENTS.mdPattern documentationDocuments Stats Tracking Pattern (Section 5.19) and other Go development standards

Validation Architecture — The broader validation system in opnDossier includes struct tag validation, custom business logic checks, and cross-field validation operating on platform-agnostic data models.

Stats Tracking Pattern — Complementary pattern ensuring check logic remains pure and separate from metrics updates, preventing double-counting during fallback logic scenarios.

Configuration Complexity Scoring — The constants layer also defines complexity weights for different configuration elements, with load balancers (weight 8) contributing more to complexity scores than firewall rules (weight 2).

OPNsense Configuration Model — The validation system operates on two data models: schema.OpnSenseDocument for raw XML and common.CommonDevice for normalized representations.

Error Handling Standards — The project follows comprehensive error handling guidelines including context wrapping, structured error types, and detailed error messages.

Dual Validator Synchronization | Dosu