iptables.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. package iptables
  2. import (
  3. "errors"
  4. "fmt"
  5. "net"
  6. "os/exec"
  7. "regexp"
  8. "strconv"
  9. "strings"
  10. "sync"
  11. "time"
  12. "github.com/sirupsen/logrus"
  13. )
  14. // Action signifies the iptable action.
  15. type Action string
  16. // Policy is the default iptable policies
  17. type Policy string
  18. // Table refers to Nat, Filter or Mangle.
  19. type Table string
  20. const (
  21. // Append appends the rule at the end of the chain.
  22. Append Action = "-A"
  23. // Delete deletes the rule from the chain.
  24. Delete Action = "-D"
  25. // Insert inserts the rule at the top of the chain.
  26. Insert Action = "-I"
  27. // Nat table is used for nat translation rules.
  28. Nat Table = "nat"
  29. // Filter table is used for filter rules.
  30. Filter Table = "filter"
  31. // Mangle table is used for mangling the packet.
  32. Mangle Table = "mangle"
  33. // Drop is the default iptables DROP policy
  34. Drop Policy = "DROP"
  35. // Accept is the default iptables ACCEPT policy
  36. Accept Policy = "ACCEPT"
  37. )
  38. var (
  39. iptablesPath string
  40. supportsXlock = false
  41. supportsCOpt = false
  42. xLockWaitMsg = "Another app is currently holding the xtables lock"
  43. // used to lock iptables commands if xtables lock is not supported
  44. bestEffortLock sync.Mutex
  45. // ErrIptablesNotFound is returned when the rule is not found.
  46. ErrIptablesNotFound = errors.New("Iptables not found")
  47. initOnce sync.Once
  48. )
  49. // ChainInfo defines the iptables chain.
  50. type ChainInfo struct {
  51. Name string
  52. Table Table
  53. HairpinMode bool
  54. }
  55. // ChainError is returned to represent errors during ip table operation.
  56. type ChainError struct {
  57. Chain string
  58. Output []byte
  59. }
  60. func (e ChainError) Error() string {
  61. return fmt.Sprintf("Error iptables %s: %s", e.Chain, string(e.Output))
  62. }
  63. func probe() {
  64. path, err := exec.LookPath("iptables")
  65. if err != nil {
  66. logrus.Warnf("Failed to find iptables: %v", err)
  67. return
  68. }
  69. if out, err := exec.Command(path, "--wait", "-t", "nat", "-L", "-n").CombinedOutput(); err != nil {
  70. logrus.Warnf("Running iptables --wait -t nat -L -n failed with message: `%s`, error: %v", strings.TrimSpace(string(out)), err)
  71. }
  72. }
  73. func initFirewalld() {
  74. if err := FirewalldInit(); err != nil {
  75. logrus.Debugf("Fail to initialize firewalld: %v, using raw iptables instead", err)
  76. }
  77. }
  78. func detectIptables() {
  79. path, err := exec.LookPath("iptables")
  80. if err != nil {
  81. return
  82. }
  83. iptablesPath = path
  84. supportsXlock = exec.Command(iptablesPath, "--wait", "-L", "-n").Run() == nil
  85. mj, mn, mc, err := GetVersion()
  86. if err != nil {
  87. logrus.Warnf("Failed to read iptables version: %v", err)
  88. return
  89. }
  90. supportsCOpt = supportsCOption(mj, mn, mc)
  91. }
  92. func initDependencies() {
  93. probe()
  94. initFirewalld()
  95. detectIptables()
  96. }
  97. func initCheck() error {
  98. initOnce.Do(initDependencies)
  99. if iptablesPath == "" {
  100. return ErrIptablesNotFound
  101. }
  102. return nil
  103. }
  104. // NewChain adds a new chain to ip table.
  105. func NewChain(name string, table Table, hairpinMode bool) (*ChainInfo, error) {
  106. c := &ChainInfo{
  107. Name: name,
  108. Table: table,
  109. HairpinMode: hairpinMode,
  110. }
  111. if string(c.Table) == "" {
  112. c.Table = Filter
  113. }
  114. // Add chain if it doesn't exist
  115. if _, err := Raw("-t", string(c.Table), "-n", "-L", c.Name); err != nil {
  116. if output, err := Raw("-t", string(c.Table), "-N", c.Name); err != nil {
  117. return nil, err
  118. } else if len(output) != 0 {
  119. return nil, fmt.Errorf("Could not create %s/%s chain: %s", c.Table, c.Name, output)
  120. }
  121. }
  122. return c, nil
  123. }
  124. // ProgramChain is used to add rules to a chain
  125. func ProgramChain(c *ChainInfo, bridgeName string, hairpinMode, enable bool) error {
  126. if c.Name == "" {
  127. return errors.New("Could not program chain, missing chain name")
  128. }
  129. switch c.Table {
  130. case Nat:
  131. preroute := []string{
  132. "-m", "addrtype",
  133. "--dst-type", "LOCAL",
  134. "-j", c.Name}
  135. if !Exists(Nat, "PREROUTING", preroute...) && enable {
  136. if err := c.Prerouting(Append, preroute...); err != nil {
  137. return fmt.Errorf("Failed to inject %s in PREROUTING chain: %s", c.Name, err)
  138. }
  139. } else if Exists(Nat, "PREROUTING", preroute...) && !enable {
  140. if err := c.Prerouting(Delete, preroute...); err != nil {
  141. return fmt.Errorf("Failed to remove %s in PREROUTING chain: %s", c.Name, err)
  142. }
  143. }
  144. output := []string{
  145. "-m", "addrtype",
  146. "--dst-type", "LOCAL",
  147. "-j", c.Name}
  148. if !hairpinMode {
  149. output = append(output, "!", "--dst", "127.0.0.0/8")
  150. }
  151. if !Exists(Nat, "OUTPUT", output...) && enable {
  152. if err := c.Output(Append, output...); err != nil {
  153. return fmt.Errorf("Failed to inject %s in OUTPUT chain: %s", c.Name, err)
  154. }
  155. } else if Exists(Nat, "OUTPUT", output...) && !enable {
  156. if err := c.Output(Delete, output...); err != nil {
  157. return fmt.Errorf("Failed to inject %s in OUTPUT chain: %s", c.Name, err)
  158. }
  159. }
  160. case Filter:
  161. if bridgeName == "" {
  162. return fmt.Errorf("Could not program chain %s/%s, missing bridge name",
  163. c.Table, c.Name)
  164. }
  165. link := []string{
  166. "-o", bridgeName,
  167. "-j", c.Name}
  168. if !Exists(Filter, "FORWARD", link...) && enable {
  169. insert := append([]string{string(Insert), "FORWARD"}, link...)
  170. if output, err := Raw(insert...); err != nil {
  171. return err
  172. } else if len(output) != 0 {
  173. return fmt.Errorf("Could not create linking rule to %s/%s: %s", c.Table, c.Name, output)
  174. }
  175. } else if Exists(Filter, "FORWARD", link...) && !enable {
  176. del := append([]string{string(Delete), "FORWARD"}, link...)
  177. if output, err := Raw(del...); err != nil {
  178. return err
  179. } else if len(output) != 0 {
  180. return fmt.Errorf("Could not delete linking rule from %s/%s: %s", c.Table, c.Name, output)
  181. }
  182. }
  183. establish := []string{
  184. "-o", bridgeName,
  185. "-m", "conntrack",
  186. "--ctstate", "RELATED,ESTABLISHED",
  187. "-j", "ACCEPT"}
  188. if !Exists(Filter, "FORWARD", establish...) && enable {
  189. insert := append([]string{string(Insert), "FORWARD"}, establish...)
  190. if output, err := Raw(insert...); err != nil {
  191. return err
  192. } else if len(output) != 0 {
  193. return fmt.Errorf("Could not create establish rule to %s: %s", c.Table, output)
  194. }
  195. } else if Exists(Filter, "FORWARD", establish...) && !enable {
  196. del := append([]string{string(Delete), "FORWARD"}, establish...)
  197. if output, err := Raw(del...); err != nil {
  198. return err
  199. } else if len(output) != 0 {
  200. return fmt.Errorf("Could not delete establish rule from %s: %s", c.Table, output)
  201. }
  202. }
  203. }
  204. return nil
  205. }
  206. // RemoveExistingChain removes existing chain from the table.
  207. func RemoveExistingChain(name string, table Table) error {
  208. c := &ChainInfo{
  209. Name: name,
  210. Table: table,
  211. }
  212. if string(c.Table) == "" {
  213. c.Table = Filter
  214. }
  215. return c.Remove()
  216. }
  217. // Forward adds forwarding rule to 'filter' table and corresponding nat rule to 'nat' table.
  218. func (c *ChainInfo) Forward(action Action, ip net.IP, port int, proto, destAddr string, destPort int, bridgeName string) error {
  219. daddr := ip.String()
  220. if ip.IsUnspecified() {
  221. // iptables interprets "0.0.0.0" as "0.0.0.0/32", whereas we
  222. // want "0.0.0.0/0". "0/0" is correctly interpreted as "any
  223. // value" by both iptables and ip6tables.
  224. daddr = "0/0"
  225. }
  226. args := []string{
  227. "-p", proto,
  228. "-d", daddr,
  229. "--dport", strconv.Itoa(port),
  230. "-j", "DNAT",
  231. "--to-destination", net.JoinHostPort(destAddr, strconv.Itoa(destPort))}
  232. if !c.HairpinMode {
  233. args = append(args, "!", "-i", bridgeName)
  234. }
  235. if err := ProgramRule(Nat, c.Name, action, args); err != nil {
  236. return err
  237. }
  238. args = []string{
  239. "!", "-i", bridgeName,
  240. "-o", bridgeName,
  241. "-p", proto,
  242. "-d", destAddr,
  243. "--dport", strconv.Itoa(destPort),
  244. "-j", "ACCEPT",
  245. }
  246. if err := ProgramRule(Filter, c.Name, action, args); err != nil {
  247. return err
  248. }
  249. args = []string{
  250. "-p", proto,
  251. "-s", destAddr,
  252. "-d", destAddr,
  253. "--dport", strconv.Itoa(destPort),
  254. "-j", "MASQUERADE",
  255. }
  256. if err := ProgramRule(Nat, "POSTROUTING", action, args); err != nil {
  257. return err
  258. }
  259. if proto == "sctp" {
  260. // Linux kernel v4.9 and below enables NETIF_F_SCTP_CRC for veth by
  261. // the following commit.
  262. // This introduces a problem when conbined with a physical NIC without
  263. // NETIF_F_SCTP_CRC. As for a workaround, here we add an iptables entry
  264. // to fill the checksum.
  265. //
  266. // https://github.com/torvalds/linux/commit/c80fafbbb59ef9924962f83aac85531039395b18
  267. args = []string{
  268. "-p", proto,
  269. "--sport", strconv.Itoa(destPort),
  270. "-j", "CHECKSUM",
  271. "--checksum-fill",
  272. }
  273. if err := ProgramRule(Mangle, "POSTROUTING", action, args); err != nil {
  274. return err
  275. }
  276. }
  277. return nil
  278. }
  279. // Link adds reciprocal ACCEPT rule for two supplied IP addresses.
  280. // Traffic is allowed from ip1 to ip2 and vice-versa
  281. func (c *ChainInfo) Link(action Action, ip1, ip2 net.IP, port int, proto string, bridgeName string) error {
  282. // forward
  283. args := []string{
  284. "-i", bridgeName, "-o", bridgeName,
  285. "-p", proto,
  286. "-s", ip1.String(),
  287. "-d", ip2.String(),
  288. "--dport", strconv.Itoa(port),
  289. "-j", "ACCEPT",
  290. }
  291. if err := ProgramRule(Filter, c.Name, action, args); err != nil {
  292. return err
  293. }
  294. // reverse
  295. args[7], args[9] = args[9], args[7]
  296. args[10] = "--sport"
  297. return ProgramRule(Filter, c.Name, action, args)
  298. }
  299. // ProgramRule adds the rule specified by args only if the
  300. // rule is not already present in the chain. Reciprocally,
  301. // it removes the rule only if present.
  302. func ProgramRule(table Table, chain string, action Action, args []string) error {
  303. if Exists(table, chain, args...) != (action == Delete) {
  304. return nil
  305. }
  306. return RawCombinedOutput(append([]string{"-t", string(table), string(action), chain}, args...)...)
  307. }
  308. // Prerouting adds linking rule to nat/PREROUTING chain.
  309. func (c *ChainInfo) Prerouting(action Action, args ...string) error {
  310. a := []string{"-t", string(Nat), string(action), "PREROUTING"}
  311. if len(args) > 0 {
  312. a = append(a, args...)
  313. }
  314. if output, err := Raw(a...); err != nil {
  315. return err
  316. } else if len(output) != 0 {
  317. return ChainError{Chain: "PREROUTING", Output: output}
  318. }
  319. return nil
  320. }
  321. // Output adds linking rule to an OUTPUT chain.
  322. func (c *ChainInfo) Output(action Action, args ...string) error {
  323. a := []string{"-t", string(c.Table), string(action), "OUTPUT"}
  324. if len(args) > 0 {
  325. a = append(a, args...)
  326. }
  327. if output, err := Raw(a...); err != nil {
  328. return err
  329. } else if len(output) != 0 {
  330. return ChainError{Chain: "OUTPUT", Output: output}
  331. }
  332. return nil
  333. }
  334. // Remove removes the chain.
  335. func (c *ChainInfo) Remove() error {
  336. // Ignore errors - This could mean the chains were never set up
  337. if c.Table == Nat {
  338. c.Prerouting(Delete, "-m", "addrtype", "--dst-type", "LOCAL", "-j", c.Name)
  339. c.Output(Delete, "-m", "addrtype", "--dst-type", "LOCAL", "!", "--dst", "127.0.0.0/8", "-j", c.Name)
  340. c.Output(Delete, "-m", "addrtype", "--dst-type", "LOCAL", "-j", c.Name) // Created in versions <= 0.1.6
  341. c.Prerouting(Delete)
  342. c.Output(Delete)
  343. }
  344. Raw("-t", string(c.Table), "-F", c.Name)
  345. Raw("-t", string(c.Table), "-X", c.Name)
  346. return nil
  347. }
  348. // Exists checks if a rule exists
  349. func Exists(table Table, chain string, rule ...string) bool {
  350. return exists(false, table, chain, rule...)
  351. }
  352. // ExistsNative behaves as Exists with the difference it
  353. // will always invoke `iptables` binary.
  354. func ExistsNative(table Table, chain string, rule ...string) bool {
  355. return exists(true, table, chain, rule...)
  356. }
  357. func exists(native bool, table Table, chain string, rule ...string) bool {
  358. f := Raw
  359. if native {
  360. f = raw
  361. }
  362. if string(table) == "" {
  363. table = Filter
  364. }
  365. if err := initCheck(); err != nil {
  366. // The exists() signature does not allow us to return an error, but at least
  367. // we can skip the (likely invalid) exec invocation.
  368. return false
  369. }
  370. if supportsCOpt {
  371. // if exit status is 0 then return true, the rule exists
  372. _, err := f(append([]string{"-t", string(table), "-C", chain}, rule...)...)
  373. return err == nil
  374. }
  375. // parse "iptables -S" for the rule (it checks rules in a specific chain
  376. // in a specific table and it is very unreliable)
  377. return existsRaw(table, chain, rule...)
  378. }
  379. func existsRaw(table Table, chain string, rule ...string) bool {
  380. ruleString := fmt.Sprintf("%s %s\n", chain, strings.Join(rule, " "))
  381. existingRules, _ := exec.Command(iptablesPath, "-t", string(table), "-S", chain).Output()
  382. return strings.Contains(string(existingRules), ruleString)
  383. }
  384. // Maximum duration that an iptables operation can take
  385. // before flagging a warning.
  386. const opWarnTime = 2 * time.Second
  387. func filterOutput(start time.Time, output []byte, args ...string) []byte {
  388. // Flag operations that have taken a long time to complete
  389. opTime := time.Since(start)
  390. if opTime > opWarnTime {
  391. logrus.Warnf("xtables contention detected while running [%s]: Waited for %.2f seconds and received %q", strings.Join(args, " "), float64(opTime)/float64(time.Second), string(output))
  392. }
  393. // ignore iptables' message about xtables lock:
  394. // it is a warning, not an error.
  395. if strings.Contains(string(output), xLockWaitMsg) {
  396. output = []byte("")
  397. }
  398. // Put further filters here if desired
  399. return output
  400. }
  401. // Raw calls 'iptables' system command, passing supplied arguments.
  402. func Raw(args ...string) ([]byte, error) {
  403. if firewalldRunning {
  404. startTime := time.Now()
  405. output, err := Passthrough(Iptables, args...)
  406. if err == nil || !strings.Contains(err.Error(), "was not provided by any .service files") {
  407. return filterOutput(startTime, output, args...), err
  408. }
  409. }
  410. return raw(args...)
  411. }
  412. func raw(args ...string) ([]byte, error) {
  413. if err := initCheck(); err != nil {
  414. return nil, err
  415. }
  416. if supportsXlock {
  417. args = append([]string{"--wait"}, args...)
  418. } else {
  419. bestEffortLock.Lock()
  420. defer bestEffortLock.Unlock()
  421. }
  422. logrus.Debugf("%s, %v", iptablesPath, args)
  423. startTime := time.Now()
  424. output, err := exec.Command(iptablesPath, args...).CombinedOutput()
  425. if err != nil {
  426. return nil, fmt.Errorf("iptables failed: iptables %v: %s (%s)", strings.Join(args, " "), output, err)
  427. }
  428. return filterOutput(startTime, output, args...), err
  429. }
  430. // RawCombinedOutput internally calls the Raw function and returns a non nil
  431. // error if Raw returned a non nil error or a non empty output
  432. func RawCombinedOutput(args ...string) error {
  433. if output, err := Raw(args...); err != nil || len(output) != 0 {
  434. return fmt.Errorf("%s (%v)", string(output), err)
  435. }
  436. return nil
  437. }
  438. // RawCombinedOutputNative behave as RawCombinedOutput with the difference it
  439. // will always invoke `iptables` binary
  440. func RawCombinedOutputNative(args ...string) error {
  441. if output, err := raw(args...); err != nil || len(output) != 0 {
  442. return fmt.Errorf("%s (%v)", string(output), err)
  443. }
  444. return nil
  445. }
  446. // ExistChain checks if a chain exists
  447. func ExistChain(chain string, table Table) bool {
  448. if _, err := Raw("-t", string(table), "-nL", chain); err == nil {
  449. return true
  450. }
  451. return false
  452. }
  453. // GetVersion reads the iptables version numbers during initialization
  454. func GetVersion() (major, minor, micro int, err error) {
  455. out, err := exec.Command(iptablesPath, "--version").CombinedOutput()
  456. if err == nil {
  457. major, minor, micro = parseVersionNumbers(string(out))
  458. }
  459. return
  460. }
  461. // SetDefaultPolicy sets the passed default policy for the table/chain
  462. func SetDefaultPolicy(table Table, chain string, policy Policy) error {
  463. if err := RawCombinedOutput("-t", string(table), "-P", chain, string(policy)); err != nil {
  464. return fmt.Errorf("setting default policy to %v in %v chain failed: %v", policy, chain, err)
  465. }
  466. return nil
  467. }
  468. func parseVersionNumbers(input string) (major, minor, micro int) {
  469. re := regexp.MustCompile(`v\d*.\d*.\d*`)
  470. line := re.FindString(input)
  471. fmt.Sscanf(line, "v%d.%d.%d", &major, &minor, &micro)
  472. return
  473. }
  474. // iptables -C, --check option was added in v.1.4.11
  475. // http://ftp.netfilter.org/pub/iptables/changes-iptables-1.4.11.txt
  476. func supportsCOption(mj, mn, mc int) bool {
  477. return mj > 1 || (mj == 1 && (mn > 4 || (mn == 4 && mc >= 11)))
  478. }
  479. // AddReturnRule adds a return rule for the chain in the filter table
  480. func AddReturnRule(chain string) error {
  481. var (
  482. table = Filter
  483. args = []string{"-j", "RETURN"}
  484. )
  485. if Exists(table, chain, args...) {
  486. return nil
  487. }
  488. err := RawCombinedOutput(append([]string{"-A", chain}, args...)...)
  489. if err != nil {
  490. return fmt.Errorf("unable to add return rule in %s chain: %s", chain, err.Error())
  491. }
  492. return nil
  493. }
  494. // EnsureJumpRule ensures the jump rule is on top
  495. func EnsureJumpRule(fromChain, toChain string) error {
  496. var (
  497. table = Filter
  498. args = []string{"-j", toChain}
  499. )
  500. if Exists(table, fromChain, args...) {
  501. err := RawCombinedOutput(append([]string{"-D", fromChain}, args...)...)
  502. if err != nil {
  503. return fmt.Errorf("unable to remove jump to %s rule in %s chain: %s", toChain, fromChain, err.Error())
  504. }
  505. }
  506. err := RawCombinedOutput(append([]string{"-I", fromChain}, args...)...)
  507. if err != nil {
  508. return fmt.Errorf("unable to insert jump to %s rule in %s chain: %s", toChain, fromChain, err.Error())
  509. }
  510. return nil
  511. }