iptables.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  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. }
  84. for _, r := range mangleRules {
  85. if r.Disabled {
  86. continue
  87. }
  88. ruleStr := buildMangleRuleLine(r)
  89. if ruleStr != "" {
  90. sb.WriteString(ruleStr + "\n")
  91. }
  92. }
  93. sb.WriteString("COMMIT\n")
  94. // ================= NAT TABLE =================
  95. sb.WriteString("*nat\n")
  96. sb.WriteString(":PREROUTING ACCEPT [0:0]\n")
  97. sb.WriteString(":INPUT ACCEPT [0:0]\n")
  98. sb.WriteString(":OUTPUT ACCEPT [0:0]\n")
  99. sb.WriteString(":POSTROUTING ACCEPT [0:0]\n")
  100. userNATChains := make(map[string]bool)
  101. for _, r := range natRules {
  102. ch := strings.ToUpper(r.Chain)
  103. if ch != "PREROUTING" && ch != "POSTROUTING" && ch != "OUTPUT" && ch != "INPUT" && ch != "SRCNAT" && ch != "DSTNAT" && ch != "" {
  104. if !userNATChains[ch] {
  105. sb.WriteString(fmt.Sprintf(":%s - [0:0]\n", ch))
  106. userNATChains[ch] = true
  107. }
  108. }
  109. }
  110. for _, r := range natRules {
  111. if r.Disabled {
  112. continue
  113. }
  114. ruleStr := buildNATRuleLine(r)
  115. if ruleStr != "" {
  116. sb.WriteString(ruleStr + "\n")
  117. }
  118. }
  119. sb.WriteString("COMMIT\n")
  120. // ================= FILTER TABLE =================
  121. sb.WriteString("*filter\n")
  122. sb.WriteString(":INPUT ACCEPT [0:0]\n")
  123. sb.WriteString(":FORWARD ACCEPT [0:0]\n")
  124. sb.WriteString(":OUTPUT ACCEPT [0:0]\n")
  125. userFilterChains := make(map[string]bool)
  126. for _, r := range filterRules {
  127. ch := strings.ToUpper(r.Chain)
  128. if ch != "INPUT" && ch != "FORWARD" && ch != "OUTPUT" && ch != "" {
  129. if !userFilterChains[ch] {
  130. sb.WriteString(fmt.Sprintf(":%s - [0:0]\n", ch))
  131. userFilterChains[ch] = true
  132. }
  133. }
  134. }
  135. for _, r := range filterRules {
  136. if r.Disabled {
  137. continue
  138. }
  139. ruleStr := buildFilterRuleLine(r)
  140. if ruleStr != "" {
  141. sb.WriteString(ruleStr + "\n")
  142. }
  143. }
  144. sb.WriteString("COMMIT\n")
  145. return sb.String()
  146. }
  147. func buildFilterRuleLine(r FilterRule) string {
  148. chain := strings.ToUpper(r.Chain)
  149. if chain == "" {
  150. chain = "FORWARD"
  151. }
  152. var args []string
  153. args = append(args, fmt.Sprintf("-A %s", chain))
  154. // Match criteria
  155. 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)
  156. // Comment ID for counters mapping & user comment
  157. args = append(args, fmt.Sprintf("-m comment --comment \"id:%s\"", r.ID))
  158. // Action
  159. act := strings.ToLower(r.Action)
  160. switch act {
  161. case "accept":
  162. args = append(args, "-j ACCEPT")
  163. case "drop":
  164. args = append(args, "-j DROP")
  165. case "reject":
  166. args = append(args, "-j REJECT")
  167. if r.RejectWith != "" {
  168. args = append(args, fmt.Sprintf("--reject-with %s", r.RejectWith))
  169. }
  170. case "return":
  171. args = append(args, "-j RETURN")
  172. case "jump":
  173. if r.JumpTarget != "" {
  174. args = append(args, fmt.Sprintf("-j %s", strings.ToUpper(r.JumpTarget)))
  175. }
  176. case "log":
  177. args = append(args, "-j LOG")
  178. if r.LogPrefix != "" {
  179. args = append(args, fmt.Sprintf("--log-prefix \"%s: \"", r.LogPrefix))
  180. }
  181. case "passthrough":
  182. // in iptables, passthrough continues evaluation
  183. return ""
  184. default:
  185. args = append(args, "-j ACCEPT")
  186. }
  187. return strings.Join(args, " ")
  188. }
  189. func buildNATRuleLine(r NATRule) string {
  190. chain := strings.ToUpper(r.Chain)
  191. if chain == "SRCNAT" {
  192. chain = "POSTROUTING"
  193. } else if chain == "DSTNAT" {
  194. chain = "PREROUTING"
  195. }
  196. var args []string
  197. args = append(args, fmt.Sprintf("-A %s", chain))
  198. 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)
  199. args = append(args, fmt.Sprintf("-m comment --comment \"id:%s\"", r.ID))
  200. act := strings.ToLower(r.Action)
  201. switch act {
  202. case "masquerade":
  203. args = append(args, "-j MASQUERADE")
  204. case "src-nat":
  205. if r.ToAddresses != "" {
  206. target := r.ToAddresses
  207. if r.ToPorts != "" {
  208. target = fmt.Sprintf("%s:%s", r.ToAddresses, r.ToPorts)
  209. }
  210. args = append(args, fmt.Sprintf("-j SNAT --to-source %s", target))
  211. } else {
  212. args = append(args, "-j MASQUERADE")
  213. }
  214. case "dst-nat":
  215. if r.ToAddresses != "" {
  216. target := r.ToAddresses
  217. if r.ToPorts != "" {
  218. target = fmt.Sprintf("%s:%s", r.ToAddresses, r.ToPorts)
  219. }
  220. args = append(args, fmt.Sprintf("-j DNAT --to-destination %s", target))
  221. }
  222. case "redirect":
  223. if r.ToPorts != "" {
  224. args = append(args, fmt.Sprintf("-j REDIRECT --to-ports %s", r.ToPorts))
  225. } else {
  226. args = append(args, "-j REDIRECT")
  227. }
  228. case "accept":
  229. args = append(args, "-j ACCEPT")
  230. case "return":
  231. args = append(args, "-j RETURN")
  232. default:
  233. args = append(args, "-j MASQUERADE")
  234. }
  235. return strings.Join(args, " ")
  236. }
  237. func buildMangleRuleLine(r MangleRule) string {
  238. chain := strings.ToUpper(r.Chain)
  239. if chain == "" {
  240. chain = "PREROUTING"
  241. }
  242. var args []string
  243. args = append(args, fmt.Sprintf("-A %s", chain))
  244. 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)
  245. args = append(args, fmt.Sprintf("-m comment --comment \"id:%s\"", r.ID))
  246. act := strings.ToLower(r.Action)
  247. switch act {
  248. case "mark-connection":
  249. if r.NewConnectionMark != "" {
  250. markID := MarkNameToID(r.NewConnectionMark)
  251. args = append(args, fmt.Sprintf("-j CONNMARK --set-mark %d", markID))
  252. }
  253. case "mark-routing":
  254. if r.NewRoutingMark != "" {
  255. markID := MarkNameToID(r.NewRoutingMark)
  256. args = append(args, fmt.Sprintf("-j MARK --set-mark %d", markID))
  257. }
  258. case "mark-packet":
  259. if r.NewPacketMark != "" {
  260. markID := MarkNameToID(r.NewPacketMark)
  261. args = append(args, fmt.Sprintf("-j MARK --set-mark %d", markID))
  262. }
  263. case "change-mss":
  264. args = append(args, "-p tcp --tcp-flags SYN,RST SYN -j TCPMSS")
  265. if r.NewMSS == "clamp-to-pmtu" || r.NewMSS == "" {
  266. args = append(args, "--clamp-mss-to-pmtu")
  267. } else {
  268. args = append(args, fmt.Sprintf("--set-mss %s", r.NewMSS))
  269. }
  270. case "set-dscp":
  271. if r.NewDSCP > 0 {
  272. args = append(args, fmt.Sprintf("-j DSCP --set-dscp %d", r.NewDSCP))
  273. }
  274. case "accept":
  275. args = append(args, "-j ACCEPT")
  276. case "return":
  277. args = append(args, "-j RETURN")
  278. default:
  279. return ""
  280. }
  281. return strings.Join(args, " ")
  282. }
  283. func appendMatchCriteria(args []string, srcAddr, dstAddr, proto, srcPort, dstPort, inIface, outIface string, ctStates []string, connMark, routingMark, srcList, dstList, srcGeoIP, dstGeoIP string) []string {
  284. if srcAddr != "" {
  285. if strings.HasPrefix(srcAddr, "!") {
  286. args = append(args, fmt.Sprintf("! -s %s", strings.TrimPrefix(srcAddr, "!")))
  287. } else {
  288. args = append(args, fmt.Sprintf("-s %s", srcAddr))
  289. }
  290. }
  291. if dstAddr != "" {
  292. if strings.HasPrefix(dstAddr, "!") {
  293. args = append(args, fmt.Sprintf("! -d %s", strings.TrimPrefix(dstAddr, "!")))
  294. } else {
  295. args = append(args, fmt.Sprintf("-d %s", dstAddr))
  296. }
  297. }
  298. effectiveProto := strings.ToLower(proto)
  299. if effectiveProto != "" && effectiveProto != "all" {
  300. args = append(args, fmt.Sprintf("-p %s", effectiveProto))
  301. } else if srcPort != "" || dstPort != "" {
  302. effectiveProto = "tcp"
  303. args = append(args, "-p tcp")
  304. }
  305. if srcPort != "" && (effectiveProto == "tcp" || effectiveProto == "udp") {
  306. if strings.Contains(srcPort, ",") {
  307. args = append(args, fmt.Sprintf("-m multiport --sports %s", srcPort))
  308. } else {
  309. args = append(args, fmt.Sprintf("--sport %s", srcPort))
  310. }
  311. }
  312. if dstPort != "" && (effectiveProto == "tcp" || effectiveProto == "udp") {
  313. if strings.Contains(dstPort, ",") {
  314. args = append(args, fmt.Sprintf("-m multiport --dports %s", dstPort))
  315. } else {
  316. args = append(args, fmt.Sprintf("--dport %s", dstPort))
  317. }
  318. }
  319. if inIface != "" {
  320. if strings.HasPrefix(inIface, "!") {
  321. args = append(args, fmt.Sprintf("! -i %s", strings.TrimPrefix(inIface, "!")))
  322. } else {
  323. args = append(args, fmt.Sprintf("-i %s", inIface))
  324. }
  325. }
  326. if outIface != "" {
  327. if strings.HasPrefix(outIface, "!") {
  328. args = append(args, fmt.Sprintf("! -o %s", strings.TrimPrefix(outIface, "!")))
  329. } else {
  330. args = append(args, fmt.Sprintf("-o %s", outIface))
  331. }
  332. }
  333. if len(ctStates) > 0 {
  334. var upperStates []string
  335. for _, s := range ctStates {
  336. upperStates = append(upperStates, strings.ToUpper(s))
  337. }
  338. args = append(args, fmt.Sprintf("-m conntrack --ctstate %s", strings.Join(upperStates, ",")))
  339. }
  340. if connMark != "" {
  341. markID := MarkNameToID(connMark)
  342. args = append(args, fmt.Sprintf("-m connmark --mark %d", markID))
  343. }
  344. if routingMark != "" {
  345. markID := MarkNameToID(routingMark)
  346. args = append(args, fmt.Sprintf("-m mark --mark %d", markID))
  347. }
  348. if srcList != "" {
  349. invert := ""
  350. cleanList := srcList
  351. if strings.HasPrefix(cleanList, "!") {
  352. invert = "! "
  353. cleanList = strings.TrimPrefix(cleanList, "!")
  354. }
  355. setName := GetIPSetName(cleanList)
  356. args = append(args, fmt.Sprintf("-m set %s--match-set %s src", invert, setName))
  357. }
  358. if dstList != "" {
  359. invert := ""
  360. cleanList := dstList
  361. if strings.HasPrefix(cleanList, "!") {
  362. invert = "! "
  363. cleanList = strings.TrimPrefix(cleanList, "!")
  364. }
  365. setName := GetIPSetName(cleanList)
  366. args = append(args, fmt.Sprintf("-m set %s--match-set %s dst", invert, setName))
  367. }
  368. if srcGeoIP != "" {
  369. invert := ""
  370. cleanGeo := srcGeoIP
  371. if strings.HasPrefix(cleanGeo, "!") {
  372. invert = "! "
  373. cleanGeo = strings.TrimPrefix(cleanGeo, "!")
  374. }
  375. setName := GetGeoIPSetName(cleanGeo)
  376. args = append(args, fmt.Sprintf("-m set %s--match-set %s src", invert, setName))
  377. }
  378. if dstGeoIP != "" {
  379. invert := ""
  380. cleanGeo := dstGeoIP
  381. if strings.HasPrefix(cleanGeo, "!") {
  382. invert = "! "
  383. cleanGeo = strings.TrimPrefix(cleanGeo, "!")
  384. }
  385. setName := GetGeoIPSetName(cleanGeo)
  386. args = append(args, fmt.Sprintf("-m set %s--match-set %s dst", invert, setName))
  387. }
  388. return args
  389. }
  390. // ApplyIPTables executes `iptables-restore` with the given rule set payload
  391. func ApplyIPTables(payload string) error {
  392. cmd := exec.Command("iptables-restore", "-c")
  393. cmd.Stdin = strings.NewReader(payload)
  394. out, err := cmd.CombinedOutput()
  395. if err != nil {
  396. return fmt.Errorf("failed to apply iptables: %s: %w", string(out), err)
  397. }
  398. return nil
  399. }