iptables.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639
  1. package firewall
  2. import (
  3. "bufio"
  4. "bytes"
  5. "encoding/json"
  6. "fmt"
  7. "hash/fnv"
  8. "os"
  9. "os/exec"
  10. "sort"
  11. "strconv"
  12. "strings"
  13. )
  14. // MarkNameToID converts a string mark name into a deterministic 32-bit fwmark integer
  15. func MarkNameToID(name string) int {
  16. if name == "" {
  17. return 0
  18. }
  19. if id, err := strconv.Atoi(name); err == nil && id > 0 {
  20. return id
  21. }
  22. h := fnv.New32a()
  23. h.Write([]byte(name))
  24. val := int(h.Sum32() % 65000)
  25. if val < 100 {
  26. val += 100
  27. }
  28. return val
  29. }
  30. // ReadRuleCounters reads `iptables-save -c` and populates the Packets and Bytes fields for all rules
  31. func ReadRuleCounters(filterRules []FilterRule, natRules []NATRule, mangleRules []MangleRule) (map[string][2]uint64, error) {
  32. out, err := exec.Command("iptables-save", "-c").Output()
  33. if err != nil {
  34. return nil, err
  35. }
  36. counters := make(map[string][2]uint64)
  37. scanner := bufio.NewScanner(bytes.NewReader(out))
  38. for scanner.Scan() {
  39. line := strings.TrimSpace(scanner.Text())
  40. if !strings.HasPrefix(line, "[") {
  41. continue
  42. }
  43. closeBracket := strings.Index(line, "]")
  44. if closeBracket == -1 {
  45. continue
  46. }
  47. countsStr := line[1:closeBracket]
  48. parts := strings.Split(countsStr, ":")
  49. if len(parts) != 2 {
  50. continue
  51. }
  52. pkts, _ := strconv.ParseUint(parts[0], 10, 64)
  53. bts, _ := strconv.ParseUint(parts[1], 10, 64)
  54. commentIdx := strings.Index(line, "--comment \"id:")
  55. if commentIdx != -1 {
  56. sub := line[commentIdx+len("--comment \"id:"):]
  57. endQuote := strings.Index(sub, "\"")
  58. if endQuote != -1 {
  59. ruleID := sub[:endQuote]
  60. existing := counters[ruleID]
  61. counters[ruleID] = [2]uint64{existing[0] + pkts, existing[1] + bts}
  62. }
  63. }
  64. }
  65. return counters, nil
  66. }
  67. // resolveInterfaceList resolves an interface list or role name (e.g. "WAN", "LAN") to interface names.
  68. func resolveInterfaceList(listName string) []string {
  69. listName = strings.TrimSpace(listName)
  70. if listName == "" {
  71. return nil
  72. }
  73. data, err := os.ReadFile("/etc/proxmox-router/metadata.json")
  74. if err != nil {
  75. return nil
  76. }
  77. var meta struct {
  78. Interfaces map[string]struct {
  79. Role string `json:"role"`
  80. } `json:"interfaces"`
  81. }
  82. if err := json.Unmarshal(data, &meta); err != nil {
  83. return nil
  84. }
  85. var matched []string
  86. for ifaceName, info := range meta.Interfaces {
  87. if strings.EqualFold(info.Role, listName) {
  88. matched = append(matched, ifaceName)
  89. }
  90. }
  91. sort.Strings(matched)
  92. return matched
  93. }
  94. func getInterfaceCombinations(inIface, inList, outIface, outList string) [][2]string {
  95. var inListResolved []string
  96. if inIface != "" {
  97. inListResolved = []string{inIface}
  98. } else if inList != "" {
  99. inListResolved = resolveInterfaceList(inList)
  100. } else {
  101. inListResolved = []string{""}
  102. }
  103. var outListResolved []string
  104. if outIface != "" {
  105. outListResolved = []string{outIface}
  106. } else if outList != "" {
  107. outListResolved = resolveInterfaceList(outList)
  108. } else {
  109. outListResolved = []string{""}
  110. }
  111. if inList != "" && inIface == "" && len(inListResolved) == 0 {
  112. return nil
  113. }
  114. if outList != "" && outIface == "" && len(outListResolved) == 0 {
  115. return nil
  116. }
  117. var combos [][2]string
  118. for _, in := range inListResolved {
  119. for _, out := range outListResolved {
  120. combos = append(combos, [2]string{in, out})
  121. }
  122. }
  123. return combos
  124. }
  125. // BuildIPTablesRestore generates the atomic configuration for `iptables-restore`
  126. func BuildIPTablesRestore(filterRules []FilterRule, natRules []NATRule, mangleRules []MangleRule) string {
  127. var sb strings.Builder
  128. // ================= MANGLE TABLE =================
  129. sb.WriteString("*mangle\n")
  130. sb.WriteString(":PREROUTING ACCEPT [0:0]\n")
  131. sb.WriteString(":INPUT ACCEPT [0:0]\n")
  132. sb.WriteString(":FORWARD ACCEPT [0:0]\n")
  133. sb.WriteString(":OUTPUT ACCEPT [0:0]\n")
  134. sb.WriteString(":POSTROUTING ACCEPT [0:0]\n")
  135. // Custom user chains in mangle
  136. userMangleChains := make(map[string]bool)
  137. for _, r := range mangleRules {
  138. ch := strings.ToUpper(r.Chain)
  139. if ch != "PREROUTING" && ch != "POSTROUTING" && ch != "INPUT" && ch != "OUTPUT" && ch != "FORWARD" && ch != "" {
  140. if !userMangleChains[ch] {
  141. sb.WriteString(fmt.Sprintf(":%s - [0:0]\n", ch))
  142. userMangleChains[ch] = true
  143. }
  144. }
  145. if r.Action == "jump" && r.JumpTarget != "" {
  146. jt := strings.ToUpper(r.JumpTarget)
  147. if jt != "ACCEPT" && jt != "DROP" && jt != "RETURN" && !userMangleChains[jt] {
  148. sb.WriteString(fmt.Sprintf(":%s - [0:0]\n", jt))
  149. userMangleChains[jt] = true
  150. }
  151. }
  152. }
  153. for _, r := range mangleRules {
  154. if r.Disabled {
  155. continue
  156. }
  157. for _, line := range buildMangleRuleLines(r) {
  158. if line != "" {
  159. sb.WriteString(line + "\n")
  160. }
  161. }
  162. }
  163. sb.WriteString("COMMIT\n")
  164. // ================= NAT TABLE =================
  165. sb.WriteString("*nat\n")
  166. sb.WriteString(":PREROUTING ACCEPT [0:0]\n")
  167. sb.WriteString(":INPUT ACCEPT [0:0]\n")
  168. sb.WriteString(":OUTPUT ACCEPT [0:0]\n")
  169. sb.WriteString(":POSTROUTING ACCEPT [0:0]\n")
  170. userNATChains := make(map[string]bool)
  171. for _, r := range natRules {
  172. ch := strings.ToUpper(r.Chain)
  173. if ch != "PREROUTING" && ch != "POSTROUTING" && ch != "OUTPUT" && ch != "INPUT" && ch != "SRCNAT" && ch != "DSTNAT" && ch != "" {
  174. if !userNATChains[ch] {
  175. sb.WriteString(fmt.Sprintf(":%s - [0:0]\n", ch))
  176. userNATChains[ch] = true
  177. }
  178. }
  179. if r.Action == "jump" && r.JumpTarget != "" {
  180. jt := strings.ToUpper(r.JumpTarget)
  181. if jt != "ACCEPT" && jt != "RETURN" && !userNATChains[jt] {
  182. sb.WriteString(fmt.Sprintf(":%s - [0:0]\n", jt))
  183. userNATChains[jt] = true
  184. }
  185. }
  186. }
  187. for _, r := range natRules {
  188. if r.Disabled {
  189. continue
  190. }
  191. for _, line := range buildNATRuleLines(r) {
  192. if line != "" {
  193. sb.WriteString(line + "\n")
  194. }
  195. }
  196. }
  197. sb.WriteString("COMMIT\n")
  198. // ================= FILTER TABLE =================
  199. sb.WriteString("*filter\n")
  200. sb.WriteString(":INPUT ACCEPT [0:0]\n")
  201. sb.WriteString(":FORWARD ACCEPT [0:0]\n")
  202. sb.WriteString(":OUTPUT ACCEPT [0:0]\n")
  203. userFilterChains := make(map[string]bool)
  204. for _, r := range filterRules {
  205. ch := strings.ToUpper(r.Chain)
  206. if ch != "INPUT" && ch != "FORWARD" && ch != "OUTPUT" && ch != "" {
  207. if !userFilterChains[ch] {
  208. sb.WriteString(fmt.Sprintf(":%s - [0:0]\n", ch))
  209. userFilterChains[ch] = true
  210. }
  211. }
  212. if r.Action == "jump" && r.JumpTarget != "" {
  213. jt := strings.ToUpper(r.JumpTarget)
  214. if jt != "ACCEPT" && jt != "DROP" && jt != "REJECT" && jt != "RETURN" && !userFilterChains[jt] {
  215. sb.WriteString(fmt.Sprintf(":%s - [0:0]\n", jt))
  216. userFilterChains[jt] = true
  217. }
  218. }
  219. }
  220. for _, r := range filterRules {
  221. if r.Disabled {
  222. continue
  223. }
  224. for _, line := range buildFilterRuleLines(r) {
  225. if line != "" {
  226. sb.WriteString(line + "\n")
  227. }
  228. }
  229. }
  230. sb.WriteString("COMMIT\n")
  231. return sb.String()
  232. }
  233. func buildFilterRuleLines(r FilterRule) []string {
  234. combos := getInterfaceCombinations(r.InInterface, r.InInterfaceList, r.OutInterface, r.OutInterfaceList)
  235. if len(combos) == 0 {
  236. return nil
  237. }
  238. chain := strings.ToUpper(r.Chain)
  239. if chain == "" {
  240. chain = "FORWARD"
  241. }
  242. var lines []string
  243. for _, combo := range combos {
  244. inIface, outIface := combo[0], combo[1]
  245. var args []string
  246. args = append(args, fmt.Sprintf("-A %s", chain))
  247. // Match criteria
  248. args = appendMatchCriteria(args, r.SrcAddress, r.DstAddress, r.Protocol, r.SrcPort, r.DstPort, inIface, outIface, r.ConnectionState, r.ConnectionMark, r.RoutingMark, r.SrcAddressList, r.DstAddressList, r.SrcGeoIP, r.DstGeoIP)
  249. // Comment ID for counters mapping & user comment
  250. args = append(args, fmt.Sprintf("-m comment --comment \"id:%s\"", r.ID))
  251. // Action
  252. act := strings.ToLower(r.Action)
  253. switch act {
  254. case "accept":
  255. args = append(args, "-j ACCEPT")
  256. case "drop":
  257. args = append(args, "-j DROP")
  258. case "reject":
  259. args = append(args, "-j REJECT")
  260. if r.RejectWith != "" {
  261. args = append(args, fmt.Sprintf("--reject-with %s", r.RejectWith))
  262. }
  263. case "return":
  264. args = append(args, "-j RETURN")
  265. case "jump":
  266. if r.JumpTarget != "" {
  267. args = append(args, fmt.Sprintf("-j %s", strings.ToUpper(r.JumpTarget)))
  268. }
  269. case "log":
  270. args = append(args, "-j LOG")
  271. if r.LogPrefix != "" {
  272. args = append(args, fmt.Sprintf("--log-prefix \"%s: \"", r.LogPrefix))
  273. }
  274. case "add-src-to-address-list":
  275. if r.AddressList != "" {
  276. args = append(args, fmt.Sprintf("-j SET --add-set %s src", GetIPSetName(r.AddressList)))
  277. }
  278. case "add-dst-to-address-list":
  279. if r.AddressList != "" {
  280. args = append(args, fmt.Sprintf("-j SET --add-set %s dst", GetIPSetName(r.AddressList)))
  281. }
  282. case "passthrough":
  283. // in iptables, passthrough continues evaluation
  284. continue
  285. default:
  286. args = append(args, "-j ACCEPT")
  287. }
  288. lines = append(lines, strings.Join(args, " "))
  289. }
  290. return lines
  291. }
  292. func buildNATRuleLines(r NATRule) []string {
  293. combos := getInterfaceCombinations(r.InInterface, r.InInterfaceList, r.OutInterface, r.OutInterfaceList)
  294. if len(combos) == 0 {
  295. return nil
  296. }
  297. chain := strings.ToUpper(r.Chain)
  298. if chain == "SRCNAT" {
  299. chain = "POSTROUTING"
  300. } else if chain == "DSTNAT" {
  301. chain = "PREROUTING"
  302. }
  303. var lines []string
  304. for _, combo := range combos {
  305. inIface, outIface := combo[0], combo[1]
  306. var args []string
  307. args = append(args, fmt.Sprintf("-A %s", chain))
  308. args = appendMatchCriteria(args, r.SrcAddress, r.DstAddress, r.Protocol, r.SrcPort, r.DstPort, inIface, outIface, nil, "", "", r.SrcAddressList, r.DstAddressList, r.SrcGeoIP, r.DstGeoIP)
  309. args = append(args, fmt.Sprintf("-m comment --comment \"id:%s\"", r.ID))
  310. act := strings.ToLower(r.Action)
  311. switch act {
  312. case "masquerade":
  313. args = append(args, "-j MASQUERADE")
  314. case "src-nat":
  315. if r.ToAddresses != "" {
  316. target := r.ToAddresses
  317. if r.ToPorts != "" {
  318. target = fmt.Sprintf("%s:%s", r.ToAddresses, r.ToPorts)
  319. }
  320. args = append(args, fmt.Sprintf("-j SNAT --to-source %s", target))
  321. } else {
  322. args = append(args, "-j MASQUERADE")
  323. }
  324. case "dst-nat":
  325. if r.ToAddresses != "" {
  326. target := r.ToAddresses
  327. if r.ToPorts != "" {
  328. target = fmt.Sprintf("%s:%s", r.ToAddresses, r.ToPorts)
  329. }
  330. args = append(args, fmt.Sprintf("-j DNAT --to-destination %s", target))
  331. }
  332. case "redirect":
  333. if r.ToPorts != "" {
  334. args = append(args, fmt.Sprintf("-j REDIRECT --to-ports %s", r.ToPorts))
  335. } else {
  336. args = append(args, "-j REDIRECT")
  337. }
  338. case "jump":
  339. if r.JumpTarget != "" {
  340. args = append(args, fmt.Sprintf("-j %s", strings.ToUpper(r.JumpTarget)))
  341. }
  342. case "log":
  343. args = append(args, "-j LOG")
  344. if r.LogPrefix != "" {
  345. args = append(args, fmt.Sprintf("--log-prefix \"%s: \"", r.LogPrefix))
  346. }
  347. case "add-src-to-address-list":
  348. if r.AddressList != "" {
  349. args = append(args, fmt.Sprintf("-j SET --add-set %s src", GetIPSetName(r.AddressList)))
  350. }
  351. case "add-dst-to-address-list":
  352. if r.AddressList != "" {
  353. args = append(args, fmt.Sprintf("-j SET --add-set %s dst", GetIPSetName(r.AddressList)))
  354. }
  355. case "accept":
  356. args = append(args, "-j ACCEPT")
  357. case "return":
  358. args = append(args, "-j RETURN")
  359. default:
  360. args = append(args, "-j MASQUERADE")
  361. }
  362. lines = append(lines, strings.Join(args, " "))
  363. }
  364. return lines
  365. }
  366. func buildMangleRuleLines(r MangleRule) []string {
  367. combos := getInterfaceCombinations(r.InInterface, r.InInterfaceList, r.OutInterface, r.OutInterfaceList)
  368. if len(combos) == 0 {
  369. return nil
  370. }
  371. chain := strings.ToUpper(r.Chain)
  372. if chain == "" {
  373. chain = "PREROUTING"
  374. }
  375. var lines []string
  376. for _, combo := range combos {
  377. inIface, outIface := combo[0], combo[1]
  378. var args []string
  379. args = append(args, fmt.Sprintf("-A %s", chain))
  380. args = appendMatchCriteria(args, r.SrcAddress, r.DstAddress, r.Protocol, r.SrcPort, r.DstPort, inIface, outIface, r.ConnectionState, r.ConnectionMark, r.RoutingMark, r.SrcAddressList, r.DstAddressList, r.SrcGeoIP, r.DstGeoIP)
  381. args = append(args, fmt.Sprintf("-m comment --comment \"id:%s\"", r.ID))
  382. act := strings.ToLower(r.Action)
  383. switch act {
  384. case "mark-connection":
  385. if r.NewConnectionMark != "" {
  386. markID := MarkNameToID(r.NewConnectionMark)
  387. args = append(args, fmt.Sprintf("-j CONNMARK --set-mark %d", markID))
  388. }
  389. case "mark-routing":
  390. if r.NewRoutingMark != "" {
  391. markID := MarkNameToID(r.NewRoutingMark)
  392. args = append(args, fmt.Sprintf("-j MARK --set-mark %d", markID))
  393. }
  394. case "mark-packet":
  395. if r.NewPacketMark != "" {
  396. markID := MarkNameToID(r.NewPacketMark)
  397. args = append(args, fmt.Sprintf("-j MARK --set-mark %d", markID))
  398. }
  399. case "change-mss":
  400. args = append(args, "-p tcp --tcp-flags SYN,RST SYN -j TCPMSS")
  401. if r.NewMSS == "clamp-to-pmtu" || r.NewMSS == "" {
  402. args = append(args, "--clamp-mss-to-pmtu")
  403. } else {
  404. args = append(args, fmt.Sprintf("--set-mss %s", r.NewMSS))
  405. }
  406. case "set-dscp", "change-dscp":
  407. if r.NewDSCP > 0 {
  408. args = append(args, fmt.Sprintf("-j DSCP --set-dscp %d", r.NewDSCP))
  409. }
  410. case "change-ttl":
  411. if r.NewTTL != "" {
  412. if strings.HasPrefix(r.NewTTL, "+") {
  413. args = append(args, fmt.Sprintf("-j TTL --ttl-inc %s", strings.TrimPrefix(r.NewTTL, "+")))
  414. } else if strings.HasPrefix(r.NewTTL, "-") {
  415. args = append(args, fmt.Sprintf("-j TTL --ttl-dec %s", strings.TrimPrefix(r.NewTTL, "-")))
  416. } else {
  417. args = append(args, fmt.Sprintf("-j TTL --ttl-set %s", r.NewTTL))
  418. }
  419. }
  420. case "set-priority":
  421. if r.NewPriority != "" && chain == "POSTROUTING" {
  422. args = append(args, fmt.Sprintf("-j CLASSIFY --set-class 0:%s", r.NewPriority))
  423. }
  424. case "drop":
  425. args = append(args, "-j DROP")
  426. case "jump":
  427. if r.JumpTarget != "" {
  428. args = append(args, fmt.Sprintf("-j %s", strings.ToUpper(r.JumpTarget)))
  429. }
  430. case "log":
  431. args = append(args, "-j LOG")
  432. if r.LogPrefix != "" {
  433. args = append(args, fmt.Sprintf("--log-prefix \"%s: \"", r.LogPrefix))
  434. }
  435. case "add-src-to-address-list":
  436. if r.AddressList != "" {
  437. args = append(args, fmt.Sprintf("-j SET --add-set %s src", GetIPSetName(r.AddressList)))
  438. }
  439. case "add-dst-to-address-list":
  440. if r.AddressList != "" {
  441. args = append(args, fmt.Sprintf("-j SET --add-set %s dst", GetIPSetName(r.AddressList)))
  442. }
  443. case "accept":
  444. args = append(args, "-j ACCEPT")
  445. case "return":
  446. args = append(args, "-j RETURN")
  447. case "clear-df", "strip-ipv4-options", "route", "sniff-pc", "sniff-tzsp", "passthrough":
  448. continue
  449. default:
  450. continue
  451. }
  452. lines = append(lines, strings.Join(args, " "))
  453. }
  454. return lines
  455. }
  456. func appendMatchCriteria(args []string, srcAddr, dstAddr, proto, srcPort, dstPort, inIface, outIface string, ctStates []string, connMark, routingMark, srcList, dstList, srcGeoIP, dstGeoIP string) []string {
  457. if srcAddr != "" {
  458. if strings.HasPrefix(srcAddr, "!") {
  459. args = append(args, fmt.Sprintf("! -s %s", strings.TrimPrefix(srcAddr, "!")))
  460. } else {
  461. args = append(args, fmt.Sprintf("-s %s", srcAddr))
  462. }
  463. }
  464. if dstAddr != "" {
  465. if strings.HasPrefix(dstAddr, "!") {
  466. args = append(args, fmt.Sprintf("! -d %s", strings.TrimPrefix(dstAddr, "!")))
  467. } else {
  468. args = append(args, fmt.Sprintf("-d %s", dstAddr))
  469. }
  470. }
  471. effectiveProto := strings.ToLower(proto)
  472. if effectiveProto != "" && effectiveProto != "all" {
  473. args = append(args, fmt.Sprintf("-p %s", effectiveProto))
  474. } else if srcPort != "" || dstPort != "" {
  475. effectiveProto = "tcp"
  476. args = append(args, "-p tcp")
  477. }
  478. if srcPort != "" && (effectiveProto == "tcp" || effectiveProto == "udp") {
  479. if strings.Contains(srcPort, ",") {
  480. args = append(args, fmt.Sprintf("-m multiport --sports %s", srcPort))
  481. } else {
  482. args = append(args, fmt.Sprintf("--sport %s", srcPort))
  483. }
  484. }
  485. if dstPort != "" && (effectiveProto == "tcp" || effectiveProto == "udp") {
  486. if strings.Contains(dstPort, ",") {
  487. args = append(args, fmt.Sprintf("-m multiport --dports %s", dstPort))
  488. } else {
  489. args = append(args, fmt.Sprintf("--dport %s", dstPort))
  490. }
  491. }
  492. if inIface != "" {
  493. if strings.HasPrefix(inIface, "!") {
  494. args = append(args, fmt.Sprintf("! -i %s", strings.TrimPrefix(inIface, "!")))
  495. } else {
  496. args = append(args, fmt.Sprintf("-i %s", inIface))
  497. }
  498. }
  499. if outIface != "" {
  500. if strings.HasPrefix(outIface, "!") {
  501. args = append(args, fmt.Sprintf("! -o %s", strings.TrimPrefix(outIface, "!")))
  502. } else {
  503. args = append(args, fmt.Sprintf("-o %s", outIface))
  504. }
  505. }
  506. if len(ctStates) > 0 {
  507. var upperStates []string
  508. for _, s := range ctStates {
  509. upperStates = append(upperStates, strings.ToUpper(s))
  510. }
  511. args = append(args, fmt.Sprintf("-m conntrack --ctstate %s", strings.Join(upperStates, ",")))
  512. }
  513. if connMark != "" {
  514. markID := MarkNameToID(connMark)
  515. args = append(args, fmt.Sprintf("-m connmark --mark %d", markID))
  516. }
  517. if routingMark != "" {
  518. markID := MarkNameToID(routingMark)
  519. args = append(args, fmt.Sprintf("-m mark --mark %d", markID))
  520. }
  521. if srcList != "" {
  522. invert := ""
  523. cleanList := srcList
  524. if strings.HasPrefix(cleanList, "!") {
  525. invert = "! "
  526. cleanList = strings.TrimPrefix(cleanList, "!")
  527. }
  528. setName := GetIPSetName(cleanList)
  529. args = append(args, fmt.Sprintf("-m set %s--match-set %s src", invert, setName))
  530. }
  531. if dstList != "" {
  532. invert := ""
  533. cleanList := dstList
  534. if strings.HasPrefix(cleanList, "!") {
  535. invert = "! "
  536. cleanList = strings.TrimPrefix(cleanList, "!")
  537. }
  538. setName := GetIPSetName(cleanList)
  539. args = append(args, fmt.Sprintf("-m set %s--match-set %s dst", invert, setName))
  540. }
  541. if srcGeoIP != "" {
  542. invert := ""
  543. cleanGeo := srcGeoIP
  544. if strings.HasPrefix(cleanGeo, "!") {
  545. invert = "! "
  546. cleanGeo = strings.TrimPrefix(cleanGeo, "!")
  547. }
  548. setName := GetGeoIPSetName(cleanGeo)
  549. args = append(args, fmt.Sprintf("-m set %s--match-set %s src", invert, setName))
  550. }
  551. if dstGeoIP != "" {
  552. invert := ""
  553. cleanGeo := dstGeoIP
  554. if strings.HasPrefix(cleanGeo, "!") {
  555. invert = "! "
  556. cleanGeo = strings.TrimPrefix(cleanGeo, "!")
  557. }
  558. setName := GetGeoIPSetName(cleanGeo)
  559. args = append(args, fmt.Sprintf("-m set %s--match-set %s dst", invert, setName))
  560. }
  561. return args
  562. }
  563. // ApplyIPTables executes `iptables-restore` with the given rule set payload
  564. func ApplyIPTables(payload string) error {
  565. cmd := exec.Command("iptables-restore", "-c")
  566. cmd.Stdin = strings.NewReader(payload)
  567. out, err := cmd.CombinedOutput()
  568. if err != nil {
  569. return fmt.Errorf("failed to apply iptables: %s: %w", string(out), err)
  570. }
  571. return nil
  572. }