iptables.go 22 KB

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