iptables.go 18 KB

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