Add per-server NAT egress, ip_forward auto-enable, OPNsense import review checklist

- ServerSetting gains WanInterface/EgressSNATIP for optional per-server
  masquerade/SNAT of client traffic, isolated in each server's own
  nftables table
- wireguard.Start/Restart now ensure net.ipv4.ip_forward and
  net.ipv6.conf.all.forwarding are enabled before bringing an interface up
- OPNsense config.xml import now parses staticroutes/filter/nat rules and
  surfaces them as a manual-review checklist in the preview UI (never
  auto-applied)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ATVUwTa4Pqwq26orW5BcDW
This commit is contained in:
sysops
2026-07-29 13:08:52 +02:00
co-authored by Claude Sonnet 5
parent cf9c136874
commit 83b1da291f
7 changed files with 1132 additions and 5 deletions
+56 -1
View File
@@ -50,11 +50,24 @@ type PreviewServer struct {
Warnings []string `json:"warnings,omitempty"`
}
// ReviewChecklistItem is one manual-review entry surfaced from parts of
// config.xml that wireguard-ui-multi does not import or model at all
// (static routes, firewall rules, outbound NAT). These are never applied -
// they exist purely so the admin knows what else the old OPNsense config
// was doing and can recreate the equivalent manually (e.g. via
// ServerSetting.WanInterface/EgressSNATIP or a custom FirewallRule) before
// cutover, instead of discovering the gap after go-live.
type ReviewChecklistItem struct {
Kind string `json:"kind"` // "static_route" | "filter_rule" | "nat_rule"
Description string `json:"description"`
}
// PreviewResult is the full response of the preview step: every server
// OPNsense defined, mapped and ready for the admin to review/edit before
// confirming the import. It is never written to the store by itself.
type PreviewResult struct {
Servers []PreviewServer `json:"servers"`
Servers []PreviewServer `json:"servers"`
ReviewChecklist []ReviewChecklistItem `json:"review_checklist,omitempty"`
}
var slugInvalidChars = regexp.MustCompile(`[^a-zA-Z0-9_-]+`)
@@ -210,9 +223,51 @@ func ToPreview(cfg *ParsedConfig, existingServerIDs []string) *PreviewResult {
result.Servers = append(result.Servers, ps)
}
result.ReviewChecklist = buildReviewChecklist(cfg)
return result
}
// buildReviewChecklist summarizes static routes, firewall rules, and
// outbound NAT rules found in the source config.xml that wireguard-ui-multi
// has no equivalent import path for. Purely informational.
func buildReviewChecklist(cfg *ParsedConfig) []ReviewChecklistItem {
var items []ReviewChecklistItem
for _, r := range cfg.StaticRoutes {
desc := fmt.Sprintf("route %s via %s", r.Network, r.Gateway)
if r.Descr != "" {
desc += fmt.Sprintf(" (%s)", r.Descr)
}
items = append(items, ReviewChecklistItem{Kind: "static_route", Description: desc})
}
for _, r := range cfg.FilterRules {
desc := fmt.Sprintf("%s rule on %s: %s -> %s", orDefault(r.Type, "pass"), r.Interface, orDefault(r.Source.Network, "any"), orDefault(r.Destination.Network, "any"))
if r.Descr != "" {
desc += fmt.Sprintf(" (%s)", r.Descr)
}
items = append(items, ReviewChecklistItem{Kind: "filter_rule", Description: desc})
}
for _, r := range cfg.NatRules {
desc := fmt.Sprintf("outbound NAT on %s: %s -> %s", r.Interface, orDefault(r.Source.Network, "any"), orDefault(r.Target, "interface address"))
if r.Descr != "" {
desc += fmt.Sprintf(" (%s)", r.Descr)
}
items = append(items, ReviewChecklistItem{Kind: "nat_rule", Description: desc})
}
return items
}
func orDefault(s, fallback string) string {
if strings.TrimSpace(s) == "" {
return fallback
}
return s
}
func mapClient(rc rawClient) PreviewClient {
pc := PreviewClient{
SourceUUID: rc.UUID,
+57 -4
View File
@@ -60,6 +60,53 @@ type rawConfig struct {
} `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 <staticroutes><route> - 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 <filter><rule> - 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 <nat><outbound><rule> - 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 {
@@ -94,8 +141,11 @@ type rawClient struct {
// ParsedConfig is the raw parsed result, before any admin-editable mapping
// is applied.
type ParsedConfig struct {
Servers []rawServer
Clients []rawClient
Servers []rawServer
Clients []rawClient
StaticRoutes []rawStaticRoute
FilterRules []rawFilterRule
NatRules []rawNatRule
}
// Parse reads an OPNsense config.xml document and extracts the WireGuard
@@ -114,8 +164,11 @@ func Parse(r io.Reader) (*ParsedConfig, error) {
}
return &ParsedConfig{
Servers: cfg.Ns.Wireguard.Server.Servers.Server,
Clients: cfg.Ns.Wireguard.Client.Clients.Client,
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
}