detector.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. // Copyright (c) Tailscale Inc & contributors
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. //go:build linux
  4. package linuxfw
  5. import (
  6. "errors"
  7. "os/exec"
  8. "tailscale.com/envknob"
  9. "tailscale.com/feature"
  10. "tailscale.com/feature/buildfeatures"
  11. "tailscale.com/hostinfo"
  12. "tailscale.com/types/logger"
  13. "tailscale.com/version/distro"
  14. )
  15. func detectFirewallMode(logf logger.Logf, prefHint string) FirewallMode {
  16. if distro.Get() == distro.Gokrazy {
  17. // Reduce startup logging on gokrazy. There's no way to do iptables on
  18. // gokrazy anyway.
  19. logf("GoKrazy should use nftables.")
  20. hostinfo.SetFirewallMode("nft-gokrazy")
  21. return FirewallModeNfTables
  22. }
  23. if distro.Get() == distro.JetKVM {
  24. // JetKVM doesn't have iptables.
  25. hostinfo.SetFirewallMode("nft-jetkvm")
  26. return FirewallModeNfTables
  27. }
  28. mode := envknob.String("TS_DEBUG_FIREWALL_MODE")
  29. // If the envknob isn't set, fall back to the pref suggested by c2n or
  30. // nodeattrs.
  31. if mode == "" {
  32. mode = prefHint
  33. logf("using firewall mode pref %s", prefHint)
  34. } else if prefHint != "" {
  35. logf("TS_DEBUG_FIREWALL_MODE set, overriding firewall mode from %s to %s", prefHint, mode)
  36. }
  37. var det linuxFWDetector
  38. if mode == "" {
  39. // We have no preference, so check if `iptables` is even available.
  40. if buildfeatures.HasIPTables {
  41. _, err := det.iptDetect()
  42. if err != nil && errors.Is(err, exec.ErrNotFound) {
  43. logf("iptables not found: %v; falling back to nftables", err)
  44. mode = "nftables"
  45. }
  46. }
  47. }
  48. // We now use iptables as default and have "auto" and "nftables" as
  49. // options for people to test further.
  50. switch mode {
  51. case "auto":
  52. return pickFirewallModeFromInstalledRules(logf, det)
  53. case "nftables":
  54. hostinfo.SetFirewallMode("nft-forced")
  55. return FirewallModeNfTables
  56. case "iptables":
  57. hostinfo.SetFirewallMode("ipt-forced")
  58. return FirewallModeIPTables
  59. }
  60. if buildfeatures.HasIPTables {
  61. logf("default choosing iptables")
  62. hostinfo.SetFirewallMode("ipt-default")
  63. return FirewallModeIPTables
  64. }
  65. logf("default choosing nftables")
  66. hostinfo.SetFirewallMode("nft-default")
  67. return FirewallModeNfTables
  68. }
  69. // tableDetector abstracts helpers to detect the firewall mode.
  70. // It is implemented for testing purposes.
  71. type tableDetector interface {
  72. iptDetect() (int, error)
  73. nftDetect() (int, error)
  74. }
  75. type linuxFWDetector struct{}
  76. // iptDetect returns the number of iptables rules in the current namespace.
  77. func (ld linuxFWDetector) iptDetect() (int, error) {
  78. return detectIptables()
  79. }
  80. var hookDetectNetfilter feature.Hook[func() (int, error)]
  81. // ErrUnsupported is the error returned from all functions on non-Linux
  82. // platforms.
  83. var ErrUnsupported = errors.New("linuxfw:unsupported")
  84. // nftDetect returns the number of nftables rules in the current namespace.
  85. func (ld linuxFWDetector) nftDetect() (int, error) {
  86. if f, ok := hookDetectNetfilter.GetOk(); ok {
  87. return f()
  88. }
  89. return 0, ErrUnsupported
  90. }
  91. // pickFirewallModeFromInstalledRules returns the firewall mode to use based on
  92. // the environment and the system's capabilities.
  93. func pickFirewallModeFromInstalledRules(logf logger.Logf, det tableDetector) FirewallMode {
  94. if !buildfeatures.HasIPTables {
  95. hostinfo.SetFirewallMode("nft-noipt")
  96. return FirewallModeNfTables
  97. }
  98. if distro.Get() == distro.Gokrazy {
  99. // Reduce startup logging on gokrazy. There's no way to do iptables on
  100. // gokrazy anyway.
  101. return FirewallModeNfTables
  102. }
  103. iptAva, nftAva := true, true
  104. iptRuleCount, err := det.iptDetect()
  105. if err != nil {
  106. logf("detect iptables rule: %v", err)
  107. iptAva = false
  108. }
  109. nftRuleCount, err := det.nftDetect()
  110. if err != nil {
  111. logf("detect nftables rule: %v", err)
  112. nftAva = false
  113. }
  114. logf("nftables rule count: %d, iptables rule count: %d", nftRuleCount, iptRuleCount)
  115. switch {
  116. case nftRuleCount > 0 && iptRuleCount == 0:
  117. logf("nftables is currently in use")
  118. hostinfo.SetFirewallMode("nft-inuse")
  119. return FirewallModeNfTables
  120. case iptRuleCount > 0 && nftRuleCount == 0:
  121. logf("iptables is currently in use")
  122. hostinfo.SetFirewallMode("ipt-inuse")
  123. return FirewallModeIPTables
  124. case nftAva:
  125. // if both iptables and nftables are available but
  126. // neither/both are currently used, use nftables.
  127. logf("nftables is available")
  128. hostinfo.SetFirewallMode("nft")
  129. return FirewallModeNfTables
  130. case iptAva:
  131. logf("iptables is available")
  132. hostinfo.SetFirewallMode("ipt")
  133. return FirewallModeIPTables
  134. default:
  135. // if neither iptables nor nftables are available, use iptablesRunner as a dummy
  136. // runner which exists but won't do anything. Creating iptablesRunner errors only
  137. // if the iptables command is missing or doesn’t support "--version", as long as it
  138. // can determine a version then it’ll carry on.
  139. hostinfo.SetFirewallMode("ipt-fb")
  140. return FirewallModeIPTables
  141. }
  142. }