// Package opnsense parses OPNsense's WireGuard config.xml export and maps // it into staging structures that an admin can review and edit before // wireguard-ui-multi commits them as model.Server/model.ServerSetting/ // model.Client records. Parsing and mapping never touch the store, and the // resulting data is never auto-applied (no wg-quick / systemctl calls // happen anywhere in this package) - see handler/routes_opnsense_import.go // for the two-step preview/commit flow that does the actual store writes. // // Schema reference (verified against OPNsense core master, // src/opnsense/mvc/app/models/OPNsense/Wireguard/{Server,Client}.xml, and // against real config.xml exports/fixtures, which nest plugin model data // under a capitalized element inside the lowercase // root - the two are NOT the same element): // // // ... // // // // // // // // // // // // // ... // package opnsense import ( "encoding/xml" "fmt" "io" "strings" ) // rawConfig mirrors the on-disk config.xml structure. The root element is // the lowercase (the whole firewall config); plugin/core model // data lives inside a capitalized child element - the two are // distinct tags, not a casing quirk of one. type rawConfig struct { XMLName xml.Name `xml:"opnsense"` Ns struct { Wireguard struct { Server struct { Servers struct { Server []rawServer `xml:"server"` } `xml:"servers"` } `xml:"server"` Client struct { Clients struct { Client []rawClient `xml:"client"` } `xml:"clients"` } `xml:"client"` General struct { Enabled string `xml:"enabled"` } `xml:"general"` } `xml:"wireguard"` } `xml:"OPNsense"` StaticRoutes struct { Route []rawStaticRoute `xml:"route"` } `xml:"staticroutes"` Filter struct { Rule []rawFilterRule `xml:"rule"` } `xml:"filter"` Nat struct { Outbound struct { Mode string `xml:"mode"` Rule []rawNatRule `xml:"rule"` } `xml:"outbound"` } `xml:"nat"` } // rawStaticRoute mirrors - a manually configured // route not otherwise expressible via WireGuard's own tunneladdress/peers // fields. Surfaced as a review item only; never auto-applied. type rawStaticRoute struct { Network string `xml:"network"` Gateway string `xml:"gateway"` Descr string `xml:"descr"` } // rawFilterRule mirrors - a firewall rule. Only the fields // needed to flag rules that reference a WireGuard interface are captured. type rawFilterRule struct { Type string `xml:"type"` Interface string `xml:"interface"` Descr string `xml:"descr"` Source struct { Network string `xml:"network"` } `xml:"source"` Destination struct { Network string `xml:"network"` } `xml:"destination"` } // rawNatRule mirrors - a manual outbound NAT/SNAT // rule. Surfaced as a review item so the admin can recreate the // equivalent via ServerSetting.WanInterface/EgressSNATIP if needed. type rawNatRule struct { Interface string `xml:"interface"` Source struct { Network string `xml:"network"` } `xml:"source"` Target string `xml:"target"` Descr string `xml:"descr"` } type rawServer struct { UUID string `xml:"uuid,attr"` Enabled string `xml:"enabled"` Name string `xml:"name"` Instance string `xml:"instance"` PubKey string `xml:"pubkey"` PrivKey string `xml:"privkey"` Port string `xml:"port"` MTU string `xml:"mtu"` DNS string `xml:"dns"` TunnelAddress string `xml:"tunneladdress"` DisableRoutes string `xml:"disableroutes"` Gateway string `xml:"gateway"` Peers string `xml:"peers"` Debug string `xml:"debug"` } type rawClient struct { UUID string `xml:"uuid,attr"` Enabled string `xml:"enabled"` Name string `xml:"name"` PubKey string `xml:"pubkey"` PSK string `xml:"psk"` TunnelAddress string `xml:"tunneladdress"` ServerAddress string `xml:"serveraddress"` ServerPort string `xml:"serverport"` Keepalive string `xml:"keepalive"` } // ParsedConfig is the raw parsed result, before any admin-editable mapping // is applied. type ParsedConfig struct { Servers []rawServer Clients []rawClient StaticRoutes []rawStaticRoute FilterRules []rawFilterRule NatRules []rawNatRule } // Parse reads an OPNsense config.xml document and extracts the WireGuard // server/client definitions. It returns a clean error (never panics) if // the document isn't valid XML or doesn't have the expected root element. // Go's encoding/xml does not resolve external entities, so this is not // vulnerable to XXE; no custom entity handling is added. func Parse(r io.Reader) (*ParsedConfig, error) { var cfg rawConfig dec := xml.NewDecoder(r) if err := dec.Decode(&cfg); err != nil { return nil, fmt.Errorf("could not parse config.xml: %w", err) } if cfg.XMLName.Local != "opnsense" { return nil, fmt.Errorf("not an OPNsense config.xml (unexpected root element %q)", cfg.XMLName.Local) } return &ParsedConfig{ Servers: cfg.Ns.Wireguard.Server.Servers.Server, Clients: cfg.Ns.Wireguard.Client.Clients.Client, StaticRoutes: cfg.StaticRoutes.Route, FilterRules: cfg.Filter.Rule, NatRules: cfg.Nat.Outbound.Rule, }, nil } // splitList splits an OPNsense comma-separated field (tunneladdress, dns, // peers, ...) into trimmed, non-empty parts. func splitList(s string) []string { if strings.TrimSpace(s) == "" { return nil } parts := strings.Split(s, ",") out := make([]string, 0, len(parts)) for _, p := range parts { p = strings.TrimSpace(p) if p != "" { out = append(out, p) } } return out } // isTruthy interprets OPNsense's boolean-ish "1"/"0" (or empty) fields. func isTruthy(s string) bool { s = strings.TrimSpace(s) return s == "1" || strings.EqualFold(s, "true") }