iptables.go 15 KB

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