| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416 |
- package firewall
- import (
- "bufio"
- "bytes"
- "fmt"
- "hash/fnv"
- "os/exec"
- "strconv"
- "strings"
- )
- // MarkNameToID converts a string mark name into a deterministic 32-bit fwmark integer
- func MarkNameToID(name string) int {
- if name == "" {
- return 0
- }
- if id, err := strconv.Atoi(name); err == nil && id > 0 {
- return id
- }
- h := fnv.New32a()
- h.Write([]byte(name))
- val := int(h.Sum32() % 65000)
- if val < 100 {
- val += 100
- }
- return val
- }
- // ReadRuleCounters reads `iptables-save -c` and populates the Packets and Bytes fields for all rules
- func ReadRuleCounters(filterRules []FilterRule, natRules []NATRule, mangleRules []MangleRule) (map[string][2]uint64, error) {
- out, err := exec.Command("iptables-save", "-c").Output()
- if err != nil {
- return nil, err
- }
- counters := make(map[string][2]uint64)
- scanner := bufio.NewScanner(bytes.NewReader(out))
- for scanner.Scan() {
- line := strings.TrimSpace(scanner.Text())
- if !strings.HasPrefix(line, "[") {
- continue
- }
- closeBracket := strings.Index(line, "]")
- if closeBracket == -1 {
- continue
- }
- countsStr := line[1:closeBracket]
- parts := strings.Split(countsStr, ":")
- if len(parts) != 2 {
- continue
- }
- pkts, _ := strconv.ParseUint(parts[0], 10, 64)
- bts, _ := strconv.ParseUint(parts[1], 10, 64)
- commentIdx := strings.Index(line, "--comment \"id:")
- if commentIdx != -1 {
- sub := line[commentIdx+len("--comment \"id:"):]
- endQuote := strings.Index(sub, "\"")
- if endQuote != -1 {
- ruleID := sub[:endQuote]
- counters[ruleID] = [2]uint64{pkts, bts}
- }
- }
- }
- return counters, nil
- }
- // BuildIPTablesRestore generates the atomic configuration for `iptables-restore`
- func BuildIPTablesRestore(filterRules []FilterRule, natRules []NATRule, mangleRules []MangleRule) string {
- var sb strings.Builder
- // ================= MANGLE TABLE =================
- sb.WriteString("*mangle\n")
- sb.WriteString(":PREROUTING ACCEPT [0:0]\n")
- sb.WriteString(":INPUT ACCEPT [0:0]\n")
- sb.WriteString(":FORWARD ACCEPT [0:0]\n")
- sb.WriteString(":OUTPUT ACCEPT [0:0]\n")
- sb.WriteString(":POSTROUTING ACCEPT [0:0]\n")
- // Custom user chains in mangle
- userMangleChains := make(map[string]bool)
- for _, r := range mangleRules {
- ch := strings.ToUpper(r.Chain)
- if ch != "PREROUTING" && ch != "POSTROUTING" && ch != "INPUT" && ch != "OUTPUT" && ch != "FORWARD" && ch != "" {
- if !userMangleChains[ch] {
- sb.WriteString(fmt.Sprintf(":%s - [0:0]\n", ch))
- userMangleChains[ch] = true
- }
- }
- }
- for _, r := range mangleRules {
- if r.Disabled {
- continue
- }
- ruleStr := buildMangleRuleLine(r)
- if ruleStr != "" {
- sb.WriteString(ruleStr + "\n")
- }
- }
- sb.WriteString("COMMIT\n")
- // ================= NAT TABLE =================
- sb.WriteString("*nat\n")
- sb.WriteString(":PREROUTING ACCEPT [0:0]\n")
- sb.WriteString(":INPUT ACCEPT [0:0]\n")
- sb.WriteString(":OUTPUT ACCEPT [0:0]\n")
- sb.WriteString(":POSTROUTING ACCEPT [0:0]\n")
- userNATChains := make(map[string]bool)
- for _, r := range natRules {
- ch := strings.ToUpper(r.Chain)
- if ch != "PREROUTING" && ch != "POSTROUTING" && ch != "OUTPUT" && ch != "INPUT" && ch != "SRCNAT" && ch != "DSTNAT" && ch != "" {
- if !userNATChains[ch] {
- sb.WriteString(fmt.Sprintf(":%s - [0:0]\n", ch))
- userNATChains[ch] = true
- }
- }
- }
- for _, r := range natRules {
- if r.Disabled {
- continue
- }
- ruleStr := buildNATRuleLine(r)
- if ruleStr != "" {
- sb.WriteString(ruleStr + "\n")
- }
- }
- sb.WriteString("COMMIT\n")
- // ================= FILTER TABLE =================
- sb.WriteString("*filter\n")
- sb.WriteString(":INPUT ACCEPT [0:0]\n")
- sb.WriteString(":FORWARD ACCEPT [0:0]\n")
- sb.WriteString(":OUTPUT ACCEPT [0:0]\n")
- userFilterChains := make(map[string]bool)
- for _, r := range filterRules {
- ch := strings.ToUpper(r.Chain)
- if ch != "INPUT" && ch != "FORWARD" && ch != "OUTPUT" && ch != "" {
- if !userFilterChains[ch] {
- sb.WriteString(fmt.Sprintf(":%s - [0:0]\n", ch))
- userFilterChains[ch] = true
- }
- }
- }
- for _, r := range filterRules {
- if r.Disabled {
- continue
- }
- ruleStr := buildFilterRuleLine(r)
- if ruleStr != "" {
- sb.WriteString(ruleStr + "\n")
- }
- }
- sb.WriteString("COMMIT\n")
- return sb.String()
- }
- func buildFilterRuleLine(r FilterRule) string {
- chain := strings.ToUpper(r.Chain)
- if chain == "" {
- chain = "FORWARD"
- }
- var args []string
- args = append(args, fmt.Sprintf("-A %s", chain))
- // Match criteria
- args = appendMatchCriteria(args, r.SrcAddress, r.DstAddress, r.Protocol, r.SrcPort, r.DstPort, r.InInterface, r.OutInterface, r.ConnectionState, r.ConnectionMark, r.RoutingMark, r.SrcAddressList, r.DstAddressList)
- // Comment ID for counters mapping & user comment
- args = append(args, fmt.Sprintf("-m comment --comment \"id:%s\"", r.ID))
- // Action
- act := strings.ToLower(r.Action)
- switch act {
- case "accept":
- args = append(args, "-j ACCEPT")
- case "drop":
- args = append(args, "-j DROP")
- case "reject":
- args = append(args, "-j REJECT")
- if r.RejectWith != "" {
- args = append(args, fmt.Sprintf("--reject-with %s", r.RejectWith))
- }
- case "return":
- args = append(args, "-j RETURN")
- case "jump":
- if r.JumpTarget != "" {
- args = append(args, fmt.Sprintf("-j %s", strings.ToUpper(r.JumpTarget)))
- }
- case "log":
- args = append(args, "-j LOG")
- if r.LogPrefix != "" {
- args = append(args, fmt.Sprintf("--log-prefix \"%s: \"", r.LogPrefix))
- }
- case "passthrough":
- // in iptables, passthrough continues evaluation
- return ""
- default:
- args = append(args, "-j ACCEPT")
- }
- return strings.Join(args, " ")
- }
- func buildNATRuleLine(r NATRule) string {
- chain := strings.ToUpper(r.Chain)
- if chain == "SRCNAT" {
- chain = "POSTROUTING"
- } else if chain == "DSTNAT" {
- chain = "PREROUTING"
- }
- var args []string
- args = append(args, fmt.Sprintf("-A %s", chain))
- args = appendMatchCriteria(args, r.SrcAddress, r.DstAddress, r.Protocol, r.SrcPort, r.DstPort, r.InInterface, r.OutInterface, nil, "", "", r.SrcAddressList, r.DstAddressList)
- args = append(args, fmt.Sprintf("-m comment --comment \"id:%s\"", r.ID))
- act := strings.ToLower(r.Action)
- switch act {
- case "masquerade":
- args = append(args, "-j MASQUERADE")
- case "src-nat":
- if r.ToAddresses != "" {
- target := r.ToAddresses
- if r.ToPorts != "" {
- target = fmt.Sprintf("%s:%s", r.ToAddresses, r.ToPorts)
- }
- args = append(args, fmt.Sprintf("-j SNAT --to-source %s", target))
- } else {
- args = append(args, "-j MASQUERADE")
- }
- case "dst-nat":
- if r.ToAddresses != "" {
- target := r.ToAddresses
- if r.ToPorts != "" {
- target = fmt.Sprintf("%s:%s", r.ToAddresses, r.ToPorts)
- }
- args = append(args, fmt.Sprintf("-j DNAT --to-destination %s", target))
- }
- case "redirect":
- if r.ToPorts != "" {
- args = append(args, fmt.Sprintf("-j REDIRECT --to-ports %s", r.ToPorts))
- } else {
- args = append(args, "-j REDIRECT")
- }
- case "accept":
- args = append(args, "-j ACCEPT")
- case "return":
- args = append(args, "-j RETURN")
- default:
- args = append(args, "-j MASQUERADE")
- }
- return strings.Join(args, " ")
- }
- func buildMangleRuleLine(r MangleRule) string {
- chain := strings.ToUpper(r.Chain)
- if chain == "" {
- chain = "PREROUTING"
- }
- var args []string
- args = append(args, fmt.Sprintf("-A %s", chain))
- args = appendMatchCriteria(args, r.SrcAddress, r.DstAddress, r.Protocol, r.SrcPort, r.DstPort, r.InInterface, r.OutInterface, r.ConnectionState, r.ConnectionMark, r.RoutingMark, r.SrcAddressList, r.DstAddressList)
- args = append(args, fmt.Sprintf("-m comment --comment \"id:%s\"", r.ID))
- act := strings.ToLower(r.Action)
- switch act {
- case "mark-connection":
- if r.NewConnectionMark != "" {
- markID := MarkNameToID(r.NewConnectionMark)
- args = append(args, fmt.Sprintf("-j CONNMARK --set-mark %d", markID))
- }
- case "mark-routing":
- if r.NewRoutingMark != "" {
- markID := MarkNameToID(r.NewRoutingMark)
- args = append(args, fmt.Sprintf("-j MARK --set-mark %d", markID))
- }
- case "mark-packet":
- if r.NewPacketMark != "" {
- markID := MarkNameToID(r.NewPacketMark)
- args = append(args, fmt.Sprintf("-j MARK --set-mark %d", markID))
- }
- case "change-mss":
- args = append(args, "-p tcp --tcp-flags SYN,RST SYN -j TCPMSS")
- if r.NewMSS == "clamp-to-pmtu" || r.NewMSS == "" {
- args = append(args, "--clamp-mss-to-pmtu")
- } else {
- args = append(args, fmt.Sprintf("--set-mss %s", r.NewMSS))
- }
- case "set-dscp":
- if r.NewDSCP > 0 {
- args = append(args, fmt.Sprintf("-j DSCP --set-dscp %d", r.NewDSCP))
- }
- case "accept":
- args = append(args, "-j ACCEPT")
- case "return":
- args = append(args, "-j RETURN")
- default:
- return ""
- }
- return strings.Join(args, " ")
- }
- func appendMatchCriteria(args []string, srcAddr, dstAddr, proto, srcPort, dstPort, inIface, outIface string, ctStates []string, connMark, routingMark, srcList, dstList string) []string {
- if srcAddr != "" {
- if strings.HasPrefix(srcAddr, "!") {
- args = append(args, fmt.Sprintf("! -s %s", strings.TrimPrefix(srcAddr, "!")))
- } else {
- args = append(args, fmt.Sprintf("-s %s", srcAddr))
- }
- }
- if dstAddr != "" {
- if strings.HasPrefix(dstAddr, "!") {
- args = append(args, fmt.Sprintf("! -d %s", strings.TrimPrefix(dstAddr, "!")))
- } else {
- args = append(args, fmt.Sprintf("-d %s", dstAddr))
- }
- }
- effectiveProto := strings.ToLower(proto)
- if effectiveProto != "" && effectiveProto != "all" {
- args = append(args, fmt.Sprintf("-p %s", effectiveProto))
- } else if srcPort != "" || dstPort != "" {
- effectiveProto = "tcp"
- args = append(args, "-p tcp")
- }
- if srcPort != "" && (effectiveProto == "tcp" || effectiveProto == "udp") {
- if strings.Contains(srcPort, ",") {
- args = append(args, fmt.Sprintf("-m multiport --sports %s", srcPort))
- } else {
- args = append(args, fmt.Sprintf("--sport %s", srcPort))
- }
- }
- if dstPort != "" && (effectiveProto == "tcp" || effectiveProto == "udp") {
- if strings.Contains(dstPort, ",") {
- args = append(args, fmt.Sprintf("-m multiport --dports %s", dstPort))
- } else {
- args = append(args, fmt.Sprintf("--dport %s", dstPort))
- }
- }
- if inIface != "" {
- if strings.HasPrefix(inIface, "!") {
- args = append(args, fmt.Sprintf("! -i %s", strings.TrimPrefix(inIface, "!")))
- } else {
- args = append(args, fmt.Sprintf("-i %s", inIface))
- }
- }
- if outIface != "" {
- if strings.HasPrefix(outIface, "!") {
- args = append(args, fmt.Sprintf("! -o %s", strings.TrimPrefix(outIface, "!")))
- } else {
- args = append(args, fmt.Sprintf("-o %s", outIface))
- }
- }
- if len(ctStates) > 0 {
- var upperStates []string
- for _, s := range ctStates {
- upperStates = append(upperStates, strings.ToUpper(s))
- }
- args = append(args, fmt.Sprintf("-m conntrack --ctstate %s", strings.Join(upperStates, ",")))
- }
- if connMark != "" {
- markID := MarkNameToID(connMark)
- args = append(args, fmt.Sprintf("-m connmark --mark %d", markID))
- }
- if routingMark != "" {
- markID := MarkNameToID(routingMark)
- args = append(args, fmt.Sprintf("-m mark --mark %d", markID))
- }
- if srcList != "" {
- args = append(args, fmt.Sprintf("-m set --match-set %s src", GetIPSetName(srcList)))
- }
- if dstList != "" {
- args = append(args, fmt.Sprintf("-m set --match-set %s dst", GetIPSetName(dstList)))
- }
- return args
- }
- // ApplyIPTables executes `iptables-restore` with the given rule set payload
- func ApplyIPTables(payload string) error {
- cmd := exec.Command("iptables-restore", "-c")
- cmd.Stdin = strings.NewReader(payload)
- out, err := cmd.CombinedOutput()
- if err != nil {
- return fmt.Errorf("failed to apply iptables: %s: %w", string(out), err)
- }
- return nil
- }
|