Files
flvx/go-backend/internal/runtime/nftables/renderer.go
T
sagit da7bef88f1 fix: harden panel updates and nftables deployment (#551)
Fix panel update deployment discovery and rollback safety, add nftables compatibility and atomic replacement, and restore reproducible frontend CI installs.
2026-09-01 15:37:33 +08:00

149 lines
3.9 KiB
Go

package nftables
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"net"
"sort"
"strings"
)
func RenderTable(plan NodePlan) string {
var b strings.Builder
b.WriteString("table inet flvx {\n")
b.WriteString(" chain prerouting {\n")
b.WriteString(" type nat hook prerouting priority dstnat; policy accept;\n")
for _, rule := range sortedRules(plan.Rules) {
family := nftAddressFamily(rule.TargetHost)
dnatFamily := ""
if family != "" {
dnatFamily = family + " "
}
for _, protocol := range normalizedProtocols(rule.Protocols) {
b.WriteString(fmt.Sprintf(" %s dport %d counter dnat %sto %s comment %q\n",
protocol,
rule.InPort,
dnatFamily,
formatDNATTarget(rule.TargetHost, rule.TargetPort),
counterComment(rule.ForwardID, CounterDirectionDNAT, protocol),
))
}
}
b.WriteString(" }\n\n")
b.WriteString(" chain postrouting {\n")
b.WriteString(" type nat hook postrouting priority srcnat; policy accept;\n")
if len(plan.Rules) > 0 {
b.WriteString(" masquerade comment \"flvx masquerade\"\n")
}
b.WriteString(" }\n\n")
b.WriteString(" chain forward {\n")
b.WriteString(" type filter hook forward priority filter; policy accept;\n")
for _, rule := range sortedRules(plan.Rules) {
family := nftAddressFamily(rule.TargetHost)
if family == "" {
continue
}
targetHost := strings.Trim(strings.TrimSpace(rule.TargetHost), "[]")
for _, protocol := range normalizedProtocols(rule.Protocols) {
b.WriteString(fmt.Sprintf(" meta l4proto %s ct original proto-dst %d %s daddr %s %s dport %d counter comment %q\n",
protocol,
rule.InPort,
family,
targetHost,
protocol,
rule.TargetPort,
counterComment(rule.ForwardID, CounterDirectionToTarget, protocol),
))
b.WriteString(fmt.Sprintf(" meta l4proto %s ct original proto-dst %d %s saddr %s %s sport %d counter comment %q\n",
protocol,
rule.InPort,
family,
targetHost,
protocol,
rule.TargetPort,
counterComment(rule.ForwardID, CounterDirectionFromTarget, protocol),
))
}
}
b.WriteString(" }\n")
b.WriteString("}\n")
return b.String()
}
func counterComment(forwardID int64, direction, protocol string) string {
return fmt.Sprintf("flvx forward:%d %s %s", forwardID, direction, protocol)
}
func RuleHash(rule Rule) string {
protocols := normalizedProtocols(rule.Protocols)
sum := sha256.Sum256([]byte(fmt.Sprintf("%d|%d|%s|%d|%s",
rule.ForwardID,
rule.InPort,
strings.TrimSpace(rule.TargetHost),
rule.TargetPort,
strings.Join(protocols, ","),
)))
return hex.EncodeToString(sum[:])
}
func PlanHashes(plan NodePlan) map[int64]string {
hashes := make(map[int64]string, len(plan.Rules))
for _, rule := range plan.Rules {
hashes[rule.ForwardID] = RuleHash(rule)
}
return hashes
}
func sortedRules(rules []Rule) []Rule {
out := append([]Rule(nil), rules...)
sort.SliceStable(out, func(i, j int) bool {
if out[i].InPort == out[j].InPort {
return out[i].ForwardID < out[j].ForwardID
}
return out[i].InPort < out[j].InPort
})
return out
}
func normalizedProtocols(protocols []string) []string {
seen := map[string]struct{}{}
out := make([]string, 0, 2)
for _, protocol := range protocols {
p := strings.ToLower(strings.TrimSpace(protocol))
if p != "tcp" && p != "udp" {
continue
}
if _, ok := seen[p]; ok {
continue
}
seen[p] = struct{}{}
out = append(out, p)
}
if len(out) == 0 {
return []string{"tcp", "udp"}
}
sort.Strings(out)
return out
}
func formatDNATTarget(host string, port int) string {
trimmed := strings.Trim(strings.TrimSpace(host), "[]")
if ip := net.ParseIP(trimmed); ip != nil && ip.To4() == nil {
return fmt.Sprintf("[%s]:%d", trimmed, port)
}
return fmt.Sprintf("%s:%d", trimmed, port)
}
func nftAddressFamily(host string) string {
trimmed := strings.Trim(strings.TrimSpace(host), "[]")
ip := net.ParseIP(trimmed)
if ip == nil {
return ""
}
if ip.To4() == nil {
return "ip6"
}
return "ip"
}