iptables.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709
  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. return resolveRoles(data, listName)
  83. }
  84. func resolveRoles(data []byte, listName string) []string {
  85. var meta struct {
  86. Interfaces map[string]struct {
  87. Role string `json:"role"`
  88. } `json:"interfaces"`
  89. }
  90. if err := json.Unmarshal(data, &meta); err != nil {
  91. return nil
  92. }
  93. var matched []string
  94. for ifaceName, info := range meta.Interfaces {
  95. if strings.EqualFold(info.Role, listName) {
  96. matched = append(matched, ifaceName)
  97. }
  98. }
  99. sort.Strings(matched)
  100. return matched
  101. }
  102. func getInterfaceCombinations(inIface, inList, outIface, outList string) [][2]string {
  103. var inListResolved []string
  104. if inIface != "" {
  105. inListResolved = []string{inIface}
  106. } else if inList != "" {
  107. inListResolved = resolveInterfaceList(inList)
  108. } else {
  109. inListResolved = []string{""}
  110. }
  111. var outListResolved []string
  112. if outIface != "" {
  113. outListResolved = []string{outIface}
  114. } else if outList != "" {
  115. outListResolved = resolveInterfaceList(outList)
  116. } else {
  117. outListResolved = []string{""}
  118. }
  119. if inList != "" && inIface == "" && len(inListResolved) == 0 {
  120. return nil
  121. }
  122. if outList != "" && outIface == "" && len(outListResolved) == 0 {
  123. return nil
  124. }
  125. var combos [][2]string
  126. for _, in := range inListResolved {
  127. for _, out := range outListResolved {
  128. combos = append(combos, [2]string{in, out})
  129. }
  130. }
  131. return combos
  132. }
  133. // BuildIPTablesRestore generates the atomic configuration for `iptables-restore`
  134. func BuildIPTablesRestore(filterRules []FilterRule, natRules []NATRule, mangleRules []MangleRule) string {
  135. var sb strings.Builder
  136. // ================= MANGLE TABLE =================
  137. sb.WriteString("*mangle\n")
  138. sb.WriteString(":PREROUTING ACCEPT [0:0]\n")
  139. sb.WriteString(":INPUT ACCEPT [0:0]\n")
  140. sb.WriteString(":FORWARD ACCEPT [0:0]\n")
  141. sb.WriteString(":OUTPUT ACCEPT [0:0]\n")
  142. sb.WriteString(":POSTROUTING ACCEPT [0:0]\n")
  143. // Custom user chains in mangle
  144. userMangleChains := make(map[string]bool)
  145. for _, r := range mangleRules {
  146. if r.Disabled {
  147. continue
  148. }
  149. ch := strings.ToUpper(r.Chain)
  150. if ch != "PREROUTING" && ch != "POSTROUTING" && ch != "INPUT" && ch != "OUTPUT" && ch != "FORWARD" && ch != "" {
  151. if !userMangleChains[ch] {
  152. sb.WriteString(fmt.Sprintf(":%s - [0:0]\n", ch))
  153. userMangleChains[ch] = true
  154. }
  155. }
  156. if r.Action == "jump" && r.JumpTarget != "" {
  157. jt := strings.ToUpper(r.JumpTarget)
  158. if jt != "ACCEPT" && jt != "DROP" && jt != "RETURN" && !userMangleChains[jt] {
  159. sb.WriteString(fmt.Sprintf(":%s - [0:0]\n", jt))
  160. userMangleChains[jt] = true
  161. }
  162. }
  163. }
  164. for _, r := range mangleRules {
  165. if r.Disabled {
  166. continue
  167. }
  168. for _, line := range buildMangleRuleLines(r) {
  169. if line != "" {
  170. sb.WriteString(line + "\n")
  171. }
  172. }
  173. }
  174. sb.WriteString("COMMIT\n")
  175. // ================= NAT TABLE =================
  176. sb.WriteString("*nat\n")
  177. sb.WriteString(":PREROUTING ACCEPT [0:0]\n")
  178. sb.WriteString(":INPUT ACCEPT [0:0]\n")
  179. sb.WriteString(":OUTPUT ACCEPT [0:0]\n")
  180. sb.WriteString(":POSTROUTING ACCEPT [0:0]\n")
  181. userNATChains := make(map[string]bool)
  182. for _, r := range natRules {
  183. if r.Disabled {
  184. continue
  185. }
  186. ch := strings.ToUpper(r.Chain)
  187. if ch != "PREROUTING" && ch != "POSTROUTING" && ch != "OUTPUT" && ch != "INPUT" && ch != "SRCNAT" && ch != "DSTNAT" && ch != "" {
  188. if !userNATChains[ch] {
  189. sb.WriteString(fmt.Sprintf(":%s - [0:0]\n", ch))
  190. userNATChains[ch] = true
  191. }
  192. }
  193. if r.Action == "jump" && r.JumpTarget != "" {
  194. jt := strings.ToUpper(r.JumpTarget)
  195. if jt != "ACCEPT" && jt != "RETURN" && !userNATChains[jt] {
  196. sb.WriteString(fmt.Sprintf(":%s - [0:0]\n", jt))
  197. userNATChains[jt] = true
  198. }
  199. }
  200. }
  201. for _, r := range natRules {
  202. if r.Disabled {
  203. continue
  204. }
  205. for _, line := range buildNATRuleLines(r) {
  206. if line != "" {
  207. sb.WriteString(line + "\n")
  208. }
  209. }
  210. }
  211. sb.WriteString("COMMIT\n")
  212. // ================= FILTER TABLE =================
  213. sb.WriteString("*filter\n")
  214. sb.WriteString(":INPUT ACCEPT [0:0]\n")
  215. sb.WriteString(":FORWARD ACCEPT [0:0]\n")
  216. sb.WriteString(":OUTPUT ACCEPT [0:0]\n")
  217. userFilterChains := make(map[string]bool)
  218. for _, r := range filterRules {
  219. if r.Disabled {
  220. continue
  221. }
  222. ch := strings.ToUpper(r.Chain)
  223. if ch != "INPUT" && ch != "FORWARD" && ch != "OUTPUT" && ch != "" {
  224. if !userFilterChains[ch] {
  225. sb.WriteString(fmt.Sprintf(":%s - [0:0]\n", ch))
  226. userFilterChains[ch] = true
  227. }
  228. }
  229. if r.Action == "jump" && r.JumpTarget != "" {
  230. jt := strings.ToUpper(r.JumpTarget)
  231. if jt != "ACCEPT" && jt != "DROP" && jt != "REJECT" && jt != "RETURN" && !userFilterChains[jt] {
  232. sb.WriteString(fmt.Sprintf(":%s - [0:0]\n", jt))
  233. userFilterChains[jt] = true
  234. }
  235. }
  236. }
  237. for _, r := range filterRules {
  238. if r.Disabled {
  239. continue
  240. }
  241. for _, line := range buildFilterRuleLines(r) {
  242. if line != "" {
  243. sb.WriteString(line + "\n")
  244. }
  245. }
  246. }
  247. sb.WriteString("COMMIT\n")
  248. return sb.String()
  249. }
  250. func buildFilterRuleLines(r FilterRule) []string {
  251. r.Protocol = inferProtocol(values(r))
  252. combos := getInterfaceCombinations(r.InInterface, r.InInterfaceList, r.OutInterface, r.OutInterfaceList)
  253. if len(combos) == 0 {
  254. return nil
  255. }
  256. chain := strings.ToUpper(r.Chain)
  257. if chain == "" {
  258. chain = "FORWARD"
  259. }
  260. var lines []string
  261. for _, combo := range combos {
  262. inIface, outIface := combo[0], combo[1]
  263. var args []string
  264. args = append(args, fmt.Sprintf("-A %s", chain))
  265. // Match criteria
  266. 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)
  267. args = appendAdvanced(args, r)
  268. // Comment ID for counters mapping & user comment
  269. args = append(args, fmt.Sprintf("-m comment --comment \"id:%s\"", r.ID))
  270. // Action
  271. act := strings.ToLower(r.Action)
  272. switch act {
  273. case "accept":
  274. args = append(args, "-j ACCEPT")
  275. case "drop":
  276. args = append(args, "-j DROP")
  277. case "reject":
  278. args = append(args, "-j REJECT")
  279. if r.RejectWith != "" {
  280. args = append(args, fmt.Sprintf("--reject-with %s", r.RejectWith))
  281. }
  282. case "return":
  283. args = append(args, "-j RETURN")
  284. case "jump":
  285. if r.JumpTarget != "" {
  286. args = append(args, fmt.Sprintf("-j %s", strings.ToUpper(r.JumpTarget)))
  287. }
  288. case "log":
  289. args = append(args, "-j LOG")
  290. if r.LogPrefix != "" {
  291. args = append(args, "--log-prefix "+quote(r.LogPrefix))
  292. }
  293. case "add-src-to-address-list":
  294. if r.AddressList != "" {
  295. args = append(args, fmt.Sprintf("-j SET --add-set %s src", GetDynamicIPSetName(r.AddressList)))
  296. if seconds, _ := timeoutSeconds(r.AddressListTimeout); seconds > 0 {
  297. args = append(args, fmt.Sprintf("--timeout %d", seconds))
  298. }
  299. }
  300. case "add-dst-to-address-list":
  301. if r.AddressList != "" {
  302. args = append(args, fmt.Sprintf("-j SET --add-set %s dst", GetDynamicIPSetName(r.AddressList)))
  303. if seconds, _ := timeoutSeconds(r.AddressListTimeout); seconds > 0 {
  304. args = append(args, fmt.Sprintf("--timeout %d", seconds))
  305. }
  306. }
  307. case "passthrough":
  308. // A rule without a target counts matching packets and continues.
  309. default:
  310. continue
  311. }
  312. lines = append(lines, strings.Join(args, " "))
  313. }
  314. return decorateRuleLines("filter", r, lines)
  315. }
  316. func buildNATRuleLines(r NATRule) []string {
  317. r.Protocol = inferProtocol(values(r))
  318. combos := getInterfaceCombinations(r.InInterface, r.InInterfaceList, r.OutInterface, r.OutInterfaceList)
  319. if len(combos) == 0 {
  320. return nil
  321. }
  322. chain := strings.ToUpper(r.Chain)
  323. if chain == "SRCNAT" {
  324. chain = "POSTROUTING"
  325. } else if chain == "DSTNAT" {
  326. chain = "PREROUTING"
  327. }
  328. var lines []string
  329. for _, combo := range combos {
  330. inIface, outIface := combo[0], combo[1]
  331. var args []string
  332. args = append(args, fmt.Sprintf("-A %s", chain))
  333. args = appendMatchCriteria(args, r.SrcAddress, r.DstAddress, r.Protocol, r.SrcPort, r.DstPort, inIface, outIface, nil, r.ConnectionMark, r.RoutingMark, r.SrcAddressList, r.DstAddressList, r.SrcGeoIP, r.DstGeoIP)
  334. args = appendAdvanced(args, r)
  335. args = append(args, fmt.Sprintf("-m comment --comment \"id:%s\"", r.ID))
  336. act := strings.ToLower(r.Action)
  337. switch act {
  338. case "masquerade":
  339. args = append(args, "-j MASQUERADE")
  340. if r.ToPorts != "" {
  341. args = append(args, "--to-ports "+r.ToPorts)
  342. }
  343. case "src-nat":
  344. if r.ToAddresses != "" {
  345. target := r.ToAddresses
  346. if r.ToPorts != "" {
  347. target = net.JoinHostPort(r.ToAddresses, r.ToPorts)
  348. }
  349. args = append(args, fmt.Sprintf("-j SNAT --to-source %s", target))
  350. } else {
  351. args = append(args, "-j MASQUERADE")
  352. }
  353. case "dst-nat":
  354. if r.ToAddresses != "" {
  355. target := r.ToAddresses
  356. if r.ToPorts != "" {
  357. target = net.JoinHostPort(r.ToAddresses, r.ToPorts)
  358. }
  359. args = append(args, fmt.Sprintf("-j DNAT --to-destination %s", target))
  360. }
  361. case "redirect":
  362. if r.ToPorts != "" {
  363. args = append(args, fmt.Sprintf("-j REDIRECT --to-ports %s", r.ToPorts))
  364. } else {
  365. args = append(args, "-j REDIRECT")
  366. }
  367. case "jump":
  368. if r.JumpTarget != "" {
  369. args = append(args, fmt.Sprintf("-j %s", strings.ToUpper(r.JumpTarget)))
  370. }
  371. case "log":
  372. args = append(args, "-j LOG")
  373. if r.LogPrefix != "" {
  374. args = append(args, "--log-prefix "+quote(r.LogPrefix))
  375. }
  376. case "add-src-to-address-list":
  377. if r.AddressList != "" {
  378. args = append(args, fmt.Sprintf("-j SET --add-set %s src", GetDynamicIPSetName(r.AddressList)))
  379. if seconds, _ := timeoutSeconds(r.AddressListTimeout); seconds > 0 {
  380. args = append(args, fmt.Sprintf("--timeout %d", seconds))
  381. }
  382. }
  383. case "add-dst-to-address-list":
  384. if r.AddressList != "" {
  385. args = append(args, fmt.Sprintf("-j SET --add-set %s dst", GetDynamicIPSetName(r.AddressList)))
  386. if seconds, _ := timeoutSeconds(r.AddressListTimeout); seconds > 0 {
  387. args = append(args, fmt.Sprintf("--timeout %d", seconds))
  388. }
  389. }
  390. case "accept":
  391. args = append(args, "-j ACCEPT")
  392. case "return":
  393. args = append(args, "-j RETURN")
  394. case "netmap":
  395. args = append(args, "-j NETMAP --to "+r.ToAddresses)
  396. case "passthrough":
  397. default:
  398. continue
  399. }
  400. lines = append(lines, strings.Join(args, " "))
  401. }
  402. return decorateRuleLines("nat", r, lines)
  403. }
  404. func buildMangleRuleLines(r MangleRule) []string {
  405. r.Protocol = inferProtocol(values(r))
  406. combos := getInterfaceCombinations(r.InInterface, r.InInterfaceList, r.OutInterface, r.OutInterfaceList)
  407. if len(combos) == 0 {
  408. return nil
  409. }
  410. chain := strings.ToUpper(r.Chain)
  411. if chain == "" {
  412. chain = "PREROUTING"
  413. }
  414. var lines []string
  415. for _, combo := range combos {
  416. inIface, outIface := combo[0], combo[1]
  417. var args []string
  418. args = append(args, fmt.Sprintf("-A %s", chain))
  419. 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)
  420. args = appendAdvanced(args, r)
  421. args = append(args, fmt.Sprintf("-m comment --comment \"id:%s\"", r.ID))
  422. act := strings.ToLower(r.Action)
  423. switch act {
  424. case "mark-connection":
  425. if r.NewConnectionMark != "" {
  426. markID := MarkNameToID(r.NewConnectionMark)
  427. args = append(args, fmt.Sprintf("-j CONNMARK --set-mark %d", markID))
  428. }
  429. case "mark-routing":
  430. if r.NewRoutingMark != "" {
  431. markID := MarkNameToID(r.NewRoutingMark)
  432. args = append(args, fmt.Sprintf("-j MARK --set-mark %d", markID))
  433. }
  434. case "mark-packet":
  435. if r.NewPacketMark != "" {
  436. markID := MarkNameToID(r.NewPacketMark)
  437. args = append(args, fmt.Sprintf("-j MARK --set-mark %d", markID))
  438. }
  439. case "change-mss":
  440. if r.TCPFlags == "" {
  441. args = append(args, "--tcp-flags SYN,RST SYN")
  442. }
  443. args = append(args, "-j TCPMSS")
  444. if r.NewMSS == "clamp-to-pmtu" || r.NewMSS == "" {
  445. args = append(args, "--clamp-mss-to-pmtu")
  446. } else {
  447. args = append(args, fmt.Sprintf("--set-mss %s", r.NewMSS))
  448. }
  449. case "set-dscp", "change-dscp":
  450. if r.NewDSCP >= 0 {
  451. args = append(args, fmt.Sprintf("-j DSCP --set-dscp %d", r.NewDSCP))
  452. }
  453. case "change-ttl":
  454. if r.NewTTL != "" {
  455. module, flag := "TTL", "ttl"
  456. if r.IPVersion == 6 {
  457. module, flag = "HL", "hl"
  458. }
  459. if strings.HasPrefix(r.NewTTL, "+") {
  460. args = append(args, fmt.Sprintf("-j %s --%s-inc %s", module, flag, strings.TrimPrefix(r.NewTTL, "+")))
  461. } else if strings.HasPrefix(r.NewTTL, "-") {
  462. args = append(args, fmt.Sprintf("-j %s --%s-dec %s", module, flag, strings.TrimPrefix(r.NewTTL, "-")))
  463. } else {
  464. args = append(args, fmt.Sprintf("-j %s --%s-set %s", module, flag, r.NewTTL))
  465. }
  466. }
  467. case "set-priority":
  468. if r.NewPriority != "" && chain == "POSTROUTING" {
  469. args = append(args, fmt.Sprintf("-j CLASSIFY --set-class 0:%s", r.NewPriority))
  470. }
  471. case "drop":
  472. args = append(args, "-j DROP")
  473. case "jump":
  474. if r.JumpTarget != "" {
  475. args = append(args, fmt.Sprintf("-j %s", strings.ToUpper(r.JumpTarget)))
  476. }
  477. case "log":
  478. args = append(args, "-j LOG")
  479. if r.LogPrefix != "" {
  480. args = append(args, "--log-prefix "+quote(r.LogPrefix))
  481. }
  482. case "add-src-to-address-list":
  483. if r.AddressList != "" {
  484. args = append(args, fmt.Sprintf("-j SET --add-set %s src", GetDynamicIPSetName(r.AddressList)))
  485. if seconds, _ := timeoutSeconds(r.AddressListTimeout); seconds > 0 {
  486. args = append(args, fmt.Sprintf("--timeout %d", seconds))
  487. }
  488. }
  489. case "add-dst-to-address-list":
  490. if r.AddressList != "" {
  491. args = append(args, fmt.Sprintf("-j SET --add-set %s dst", GetDynamicIPSetName(r.AddressList)))
  492. if seconds, _ := timeoutSeconds(r.AddressListTimeout); seconds > 0 {
  493. args = append(args, fmt.Sprintf("--timeout %d", seconds))
  494. }
  495. }
  496. case "accept":
  497. args = append(args, "-j ACCEPT")
  498. case "return":
  499. args = append(args, "-j RETURN")
  500. case "passthrough":
  501. default:
  502. continue
  503. }
  504. lines = append(lines, strings.Join(args, " "))
  505. }
  506. return decorateRuleLines("mangle", r, lines)
  507. }
  508. func appendMatchCriteria(args []string, srcAddr, dstAddr, proto, srcPort, dstPort, inIface, outIface string, ctStates []string, connMark, routingMark, srcList, dstList, srcGeoIP, dstGeoIP string) []string {
  509. if srcAddr != "" {
  510. if strings.HasPrefix(srcAddr, "!") {
  511. args = append(args, fmt.Sprintf("! -s %s", strings.TrimPrefix(srcAddr, "!")))
  512. } else {
  513. args = append(args, fmt.Sprintf("-s %s", srcAddr))
  514. }
  515. }
  516. if dstAddr != "" {
  517. if strings.HasPrefix(dstAddr, "!") {
  518. args = append(args, fmt.Sprintf("! -d %s", strings.TrimPrefix(dstAddr, "!")))
  519. } else {
  520. args = append(args, fmt.Sprintf("-d %s", dstAddr))
  521. }
  522. }
  523. protoInvert, protoName := plain(strings.ToLower(proto))
  524. effectiveProto := protoName
  525. if effectiveProto != "" && effectiveProto != "all" {
  526. args = append(args, fmt.Sprintf("%s-p %s", protoInvert, effectiveProto))
  527. } else if srcPort != "" || dstPort != "" {
  528. effectiveProto = "tcp"
  529. args = append(args, "-p tcp")
  530. }
  531. if srcPort != "" && (effectiveProto == "tcp" || effectiveProto == "udp") {
  532. inv, normalized := plain(srcPort)
  533. normalized = portValue(normalized)
  534. if strings.Contains(normalized, ",") {
  535. args = append(args, fmt.Sprintf("-m multiport %s--sports %s", inv, normalized))
  536. } else {
  537. args = append(args, fmt.Sprintf("%s--sport %s", inv, normalized))
  538. }
  539. }
  540. if dstPort != "" && (effectiveProto == "tcp" || effectiveProto == "udp") {
  541. inv, normalized := plain(dstPort)
  542. normalized = portValue(normalized)
  543. if strings.Contains(normalized, ",") {
  544. args = append(args, fmt.Sprintf("-m multiport %s--dports %s", inv, normalized))
  545. } else {
  546. args = append(args, fmt.Sprintf("%s--dport %s", inv, normalized))
  547. }
  548. }
  549. if inIface != "" {
  550. if strings.HasPrefix(inIface, "!") {
  551. args = append(args, fmt.Sprintf("! -i %s", strings.TrimPrefix(inIface, "!")))
  552. } else {
  553. args = append(args, fmt.Sprintf("-i %s", inIface))
  554. }
  555. }
  556. if outIface != "" {
  557. if strings.HasPrefix(outIface, "!") {
  558. args = append(args, fmt.Sprintf("! -o %s", strings.TrimPrefix(outIface, "!")))
  559. } else {
  560. args = append(args, fmt.Sprintf("-o %s", outIface))
  561. }
  562. }
  563. if len(ctStates) > 0 {
  564. var upperStates []string
  565. for _, s := range ctStates {
  566. upperStates = append(upperStates, strings.ToUpper(s))
  567. }
  568. args = append(args, fmt.Sprintf("-m conntrack --ctstate %s", strings.Join(upperStates, ",")))
  569. }
  570. if connMark != "" {
  571. inv, name := plain(connMark)
  572. markID := MarkNameToID(name)
  573. args = append(args, fmt.Sprintf("-m connmark %s--mark %d", inv, markID))
  574. }
  575. if routingMark != "" {
  576. inv, name := plain(routingMark)
  577. markID := MarkNameToID(name)
  578. args = append(args, fmt.Sprintf("-m mark %s--mark %d", inv, markID))
  579. }
  580. if srcList != "" {
  581. invert := ""
  582. cleanList := srcList
  583. if strings.HasPrefix(cleanList, "!") {
  584. invert = "! "
  585. cleanList = strings.TrimPrefix(cleanList, "!")
  586. }
  587. setName := GetIPSetName(cleanList)
  588. args = append(args, fmt.Sprintf("-m set %s--match-set %s src", invert, setName))
  589. }
  590. if dstList != "" {
  591. invert := ""
  592. cleanList := dstList
  593. if strings.HasPrefix(cleanList, "!") {
  594. invert = "! "
  595. cleanList = strings.TrimPrefix(cleanList, "!")
  596. }
  597. setName := GetIPSetName(cleanList)
  598. args = append(args, fmt.Sprintf("-m set %s--match-set %s dst", invert, setName))
  599. }
  600. if srcGeoIP != "" {
  601. invert := ""
  602. cleanGeo := srcGeoIP
  603. if strings.HasPrefix(cleanGeo, "!") {
  604. invert = "! "
  605. cleanGeo = strings.TrimPrefix(cleanGeo, "!")
  606. }
  607. setName := GetGeoIPSetName(cleanGeo)
  608. args = append(args, fmt.Sprintf("-m set %s--match-set %s src", invert, setName))
  609. }
  610. if dstGeoIP != "" {
  611. invert := ""
  612. cleanGeo := dstGeoIP
  613. if strings.HasPrefix(cleanGeo, "!") {
  614. invert = "! "
  615. cleanGeo = strings.TrimPrefix(cleanGeo, "!")
  616. }
  617. setName := GetGeoIPSetName(cleanGeo)
  618. args = append(args, fmt.Sprintf("-m set %s--match-set %s dst", invert, setName))
  619. }
  620. return args
  621. }
  622. // ApplyIPTables executes `iptables-restore` with the given rule set payload
  623. func ApplyIPTables(payload string) error {
  624. live, err := exec.Command("iptables-save", "-c").Output()
  625. if err != nil {
  626. return fmt.Errorf("cannot snapshot existing firewall: %w", err)
  627. }
  628. payload = ownedPayload(payload, string(live))
  629. for _, test := range []bool{true, false} {
  630. args := []string{"--noflush", "-c"}
  631. if test {
  632. args = append(args, "--test")
  633. }
  634. cmd := exec.Command("iptables-restore", args...)
  635. cmd.Stdin = strings.NewReader(payload)
  636. if out, err := cmd.CombinedOutput(); err != nil {
  637. return fmt.Errorf("iptables apply (test=%v) failed: %s: %w", test, out, err)
  638. }
  639. }
  640. return nil
  641. }