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) { for _, protocol := range normalizedProtocols(rule.Protocols) { b.WriteString(fmt.Sprintf(" %s dport %d dnat %s to %s comment \"flvx forward:%d %s\"\n", protocol, rule.InPort, dnatFamilyPrefix(rule.TargetHost), formatDNATTarget(rule.TargetHost, rule.TargetPort), rule.ForwardID, 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") b.WriteString(" }\n") b.WriteString("}\n") return b.String() } 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 dnatFamilyPrefix(host string) string { trimmed := strings.Trim(strings.TrimSpace(host), "[]") ip := net.ParseIP(trimmed) if ip == nil { return "" } if ip.To4() != nil { return "ip" } return "ip6" }