- 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
109 lines
3.9 KiB
Go
109 lines
3.9 KiB
Go
// Package firewall builds and (optionally) applies an nftables ruleset for
|
|
// a WireGuard server. Every server gets its own table
|
|
// (inet wireguard_ui_<serverID>) so applying/removing one server's rules
|
|
// never touches any other table on the system.
|
|
package firewall
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/ngoduykhanh/wireguard-ui/model"
|
|
)
|
|
|
|
// TableName returns the dedicated nftables table name for a server.
|
|
func TableName(serverID string) string {
|
|
return "wireguard_ui_" + serverID
|
|
}
|
|
|
|
// buildRuleLine renders one custom rule as an nftables statement.
|
|
func buildRuleLine(rule model.FirewallRule) string {
|
|
var parts []string
|
|
if rule.Source != "" {
|
|
parts = append(parts, fmt.Sprintf("ip saddr %s", rule.Source))
|
|
}
|
|
switch {
|
|
case rule.Protocol != "" && rule.Port != "":
|
|
parts = append(parts, fmt.Sprintf("%s dport %s", rule.Protocol, rule.Port))
|
|
case rule.Protocol != "":
|
|
parts = append(parts, fmt.Sprintf("meta l4proto %s", rule.Protocol))
|
|
case rule.Port != "":
|
|
parts = append(parts, fmt.Sprintf("th dport %s", rule.Port))
|
|
}
|
|
parts = append(parts, rule.Action)
|
|
comment := rule.Comment
|
|
if comment == "" {
|
|
comment = "wg-ui-multi custom rule"
|
|
}
|
|
parts = append(parts, fmt.Sprintf("comment %q", comment))
|
|
return " " + strings.Join(parts, " ")
|
|
}
|
|
|
|
// GenerateRuleset renders the full nftables ruleset for a server: the
|
|
// baseline (listen-port accept, WireGuard-interface forwarding, optional
|
|
// LAN forwarding) plus every enabled custom rule, grouped by chain.
|
|
func GenerateRuleset(server model.Server, settings model.ServerSetting, rules []model.FirewallRule) string {
|
|
ifaceName := "wgX"
|
|
listenPort := 0
|
|
if server.Interface != nil {
|
|
if server.Interface.Name != "" {
|
|
ifaceName = server.Interface.Name
|
|
}
|
|
listenPort = server.Interface.ListenPort
|
|
}
|
|
|
|
var inputExtra, forwardExtra []string
|
|
for _, rule := range rules {
|
|
if !rule.Enabled {
|
|
continue
|
|
}
|
|
line := buildRuleLine(rule)
|
|
if rule.Direction == "forward" {
|
|
forwardExtra = append(forwardExtra, line)
|
|
} else {
|
|
inputExtra = append(inputExtra, line)
|
|
}
|
|
}
|
|
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "# nftables ruleset for server %q (%s)\n", server.Name, server.ID)
|
|
fmt.Fprintf(&b, "# Generated by wireguard-ui-multi.\n\n")
|
|
|
|
fmt.Fprintf(&b, "table inet %s {\n", TableName(server.ID))
|
|
fmt.Fprintf(&b, " chain input {\n")
|
|
fmt.Fprintf(&b, " type filter hook input priority 0; policy accept;\n")
|
|
fmt.Fprintf(&b, " udp dport %d accept comment \"wg-ui-multi: %s\"\n", listenPort, server.ID)
|
|
for _, line := range inputExtra {
|
|
fmt.Fprintf(&b, "%s\n", line)
|
|
}
|
|
fmt.Fprintf(&b, " }\n\n")
|
|
|
|
fmt.Fprintf(&b, " chain forward {\n")
|
|
fmt.Fprintf(&b, " type filter hook forward priority 0; policy accept;\n")
|
|
fmt.Fprintf(&b, " iifname \"%s\" accept comment \"wg-ui-multi: %s inbound\"\n", ifaceName, server.ID)
|
|
fmt.Fprintf(&b, " oifname \"%s\" accept comment \"wg-ui-multi: %s outbound\"\n", ifaceName, server.ID)
|
|
if settings.LanInterface != "" {
|
|
fmt.Fprintf(&b, " iifname \"%s\" oifname \"%s\" accept comment \"wg-ui-multi: %s -> lan\"\n", ifaceName, settings.LanInterface, server.ID)
|
|
fmt.Fprintf(&b, " iifname \"%s\" oifname \"%s\" accept comment \"wg-ui-multi: lan -> %s\"\n", settings.LanInterface, ifaceName, server.ID)
|
|
}
|
|
for _, line := range forwardExtra {
|
|
fmt.Fprintf(&b, "%s\n", line)
|
|
}
|
|
fmt.Fprintf(&b, " }\n")
|
|
|
|
if settings.WanInterface != "" {
|
|
fmt.Fprintf(&b, "\n chain postrouting {\n")
|
|
fmt.Fprintf(&b, " type nat hook postrouting priority 100; policy accept;\n")
|
|
if settings.EgressSNATIP != "" {
|
|
fmt.Fprintf(&b, " iifname \"%s\" oifname \"%s\" snat to %s comment \"wg-ui-multi: %s egress\"\n", ifaceName, settings.WanInterface, settings.EgressSNATIP, server.ID)
|
|
} else {
|
|
fmt.Fprintf(&b, " iifname \"%s\" oifname \"%s\" masquerade comment \"wg-ui-multi: %s egress\"\n", ifaceName, settings.WanInterface, server.ID)
|
|
}
|
|
fmt.Fprintf(&b, " }\n")
|
|
}
|
|
|
|
fmt.Fprintf(&b, "}\n")
|
|
|
|
return b.String()
|
|
}
|