config.go 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. package firewall
  2. import (
  3. "errors"
  4. "fmt"
  5. "log/slog"
  6. "net/netip"
  7. "reflect"
  8. "slices"
  9. "strconv"
  10. "strings"
  11. "github.com/slackhq/nebula/config"
  12. "github.com/slackhq/nebula/iputil"
  13. )
  14. // ruleAdder takes firewall rules, it lets tests see what addRulesFromConfig adds
  15. type ruleAdder interface {
  16. AddRule(incoming bool, proto uint8, startPort int32, endPort int32, groups []string, host string, cidr, localCidr string, caName string, caSha string) error
  17. }
  18. // RulesFromConfig reads the inbound and outbound firewall rules from c and builds them, see RulesBuilder.Build
  19. func RulesFromConfig(l *slog.Logger, c *config.C, vpnNetworks, unsafeNetworks []netip.Prefix) (Rules, error) {
  20. b := NewRulesBuilder(l)
  21. b.defaultLocalCIDRAny = c.GetBool("firewall.default_local_cidr_any", false)
  22. if err := addRulesFromConfig(l, false, c, b); err != nil {
  23. return Rules{}, err
  24. }
  25. if err := addRulesFromConfig(l, true, c, b); err != nil {
  26. return Rules{}, err
  27. }
  28. return b.Build(vpnNetworks, unsafeNetworks), nil
  29. }
  30. func addRulesFromConfig(l *slog.Logger, inbound bool, c *config.C, ra ruleAdder) error {
  31. var table string
  32. if inbound {
  33. table = "firewall.inbound"
  34. } else {
  35. table = "firewall.outbound"
  36. }
  37. r := c.Get(table)
  38. if r == nil {
  39. return nil
  40. }
  41. rs, ok := r.([]any)
  42. if !ok {
  43. return fmt.Errorf("%s failed to parse, should be an array of rules", table)
  44. }
  45. for i, t := range rs {
  46. r, err := yamlToConfigRule(l, t, table, i)
  47. if err != nil {
  48. return fmt.Errorf("%s rule #%v; %s", table, i, err)
  49. }
  50. if r.Code != "" && r.Port != "" {
  51. return fmt.Errorf("%s rule #%v; only one of port or code should be provided", table, i)
  52. }
  53. if r.Host == "" && len(r.Groups) == 0 && r.Cidr == "" && r.LocalCidr == "" && r.CAName == "" && r.CASha == "" {
  54. return fmt.Errorf("%s rule #%v; at least one of host, group, cidr, local_cidr, ca_name, or ca_sha must be provided", table, i)
  55. }
  56. var sPort, errPort string
  57. if r.Code != "" {
  58. errPort = "code"
  59. sPort = r.Code
  60. } else {
  61. errPort = "port"
  62. sPort = r.Port
  63. }
  64. proto, ok := protoByName(r.Proto)
  65. if !ok {
  66. // Any other protocol by number. 0 is reserved for `any`.
  67. n, perr := strconv.ParseUint(r.Proto, 10, 8)
  68. if perr != nil || n == 0 {
  69. return fmt.Errorf("%s rule #%v; proto was not understood; `%s`", table, i, r.Proto)
  70. }
  71. proto = uint8(n)
  72. }
  73. var startPort, endPort int32
  74. if proto == iputil.IPProtocolICMP || proto == iputil.IPProtocolICMPv6 {
  75. // RulesBuilder.AddRule coerces ICMP ports to `any` as well. This copy stays so that an ICMP rule's
  76. // port is warned about rather than parsed, whatever it holds.
  77. startPort = PortAny
  78. endPort = PortAny
  79. if sPort != "" {
  80. l.Warn("ignoring port specification for ICMP firewall rule", "port", sPort)
  81. }
  82. } else {
  83. startPort, endPort, err = parsePort(sPort)
  84. }
  85. if err != nil {
  86. return fmt.Errorf("%s rule #%v; %s %s", table, i, errPort, err)
  87. }
  88. if warning := r.sanity(); warning != nil {
  89. l.Warn("firewall rule sanity check",
  90. "table", table,
  91. "rule", i,
  92. "warning", warning,
  93. )
  94. }
  95. err = ra.AddRule(inbound, proto, startPort, endPort, r.Groups, r.Host, r.Cidr, r.LocalCidr, r.CAName, r.CASha)
  96. if err != nil {
  97. return fmt.Errorf("%s rule #%v; %s", table, i, err)
  98. }
  99. }
  100. return nil
  101. }
  102. type configRule struct {
  103. Port string
  104. Code string
  105. Proto string
  106. Host string
  107. Groups []string
  108. Cidr string
  109. LocalCidr string
  110. CAName string
  111. CASha string
  112. }
  113. func yamlToConfigRule(l *slog.Logger, p any, table string, i int) (configRule, error) {
  114. r := configRule{}
  115. m, ok := p.(map[string]any)
  116. if !ok {
  117. return r, errors.New("could not parse rule")
  118. }
  119. toString := func(k string, m map[string]any) string {
  120. v, ok := m[k]
  121. if !ok {
  122. return ""
  123. }
  124. return fmt.Sprintf("%v", v)
  125. }
  126. r.Port = toString("port", m)
  127. r.Code = toString("code", m)
  128. r.Proto = toString("proto", m)
  129. r.Host = toString("host", m)
  130. r.Cidr = toString("cidr", m)
  131. r.LocalCidr = toString("local_cidr", m)
  132. r.CAName = toString("ca_name", m)
  133. r.CASha = toString("ca_sha", m)
  134. // Make sure group isn't an array
  135. if v, ok := m["group"].([]any); ok {
  136. if len(v) > 1 {
  137. return r, errors.New("group should contain a single value, an array with more than one entry was provided")
  138. }
  139. if len(v) == 0 {
  140. return r, errors.New("group should contain a single value, an empty array was provided")
  141. }
  142. l.Warn("group was an array with a single value, converting to simple value",
  143. "table", table,
  144. "rule", i,
  145. )
  146. m["group"] = v[0]
  147. }
  148. singleGroup := toString("group", m)
  149. // A null value, `groups:` with nothing after it, is the same as no groups
  150. if rg, ok := m["groups"]; ok && rg != nil {
  151. switch reflect.TypeOf(rg).Kind() {
  152. case reflect.Slice:
  153. // Every entry is read as a string, as the other fields are, so `groups: [1, 2]` is the groups "1" and "2"
  154. v := reflect.ValueOf(rg)
  155. r.Groups = make([]string, v.Len())
  156. for i := 0; i < v.Len(); i++ {
  157. r.Groups[i] = fmt.Sprintf("%v", v.Index(i).Interface())
  158. }
  159. case reflect.String:
  160. r.Groups = []string{rg.(string)}
  161. default:
  162. r.Groups = []string{fmt.Sprintf("%v", rg)}
  163. }
  164. }
  165. //flatten group vs groups
  166. if singleGroup != "" {
  167. // Check if we have both groups and group provided in the rule config
  168. if len(r.Groups) > 0 {
  169. return r, fmt.Errorf("only one of group or groups should be defined, both provided")
  170. }
  171. r.Groups = []string{singleGroup}
  172. }
  173. return r, nil
  174. }
  175. // sanity returns an error if the rule would be evaluated in a way that would short-circuit a configured check on a wildcard value
  176. // rules are evaluated as "port AND proto AND (ca_sha OR ca_name) AND (host OR group OR groups OR cidr) AND local_cidr"
  177. func (r *configRule) sanity() error {
  178. //port, proto, local_cidr are AND, no need to check here
  179. //ca_sha and ca_name don't have a wildcard value, no need to check here
  180. groupsEmpty := len(r.Groups) == 0
  181. hostEmpty := r.Host == ""
  182. cidrEmpty := r.Cidr == ""
  183. if (groupsEmpty && hostEmpty && cidrEmpty) == true {
  184. return nil //no content!
  185. }
  186. groupsHasAny := slices.Contains(r.Groups, "any")
  187. if groupsHasAny && len(r.Groups) > 1 {
  188. return fmt.Errorf("groups spec [%s] contains the group '\"any\". This rule will ignore the other groups specified", r.Groups)
  189. }
  190. if r.Host == "any" {
  191. if !groupsEmpty {
  192. return fmt.Errorf("groups specified as %s, but host=any will match any host, regardless of groups", r.Groups)
  193. }
  194. if !cidrEmpty {
  195. return fmt.Errorf("cidr specified as %s, but host=any will match any host, regardless of cidr", r.Cidr)
  196. }
  197. }
  198. if groupsHasAny {
  199. if !hostEmpty && r.Host != "any" {
  200. return fmt.Errorf("groups spec [%s] contains the group '\"any\". This rule will ignore the specified host %s", r.Groups, r.Host)
  201. }
  202. if !cidrEmpty {
  203. return fmt.Errorf("groups spec [%s] contains the group '\"any\". This rule will ignore the specified cidr %s", r.Groups, r.Cidr)
  204. }
  205. }
  206. if r.Code != "" {
  207. return fmt.Errorf("code specified as [%s]. Support for 'code' will be dropped in a future release, as it has never been functional", r.Code)
  208. }
  209. //todo alert on cidr-any
  210. return nil
  211. }
  212. func parsePort(s string) (int32, int32, error) {
  213. const notAPort int32 = -2
  214. if s == "any" {
  215. return PortAny, PortAny, nil
  216. }
  217. if s == "fragment" {
  218. return PortFragment, PortFragment, nil
  219. }
  220. if !strings.Contains(s, `-`) {
  221. rPort, err := parsePortValue("", s)
  222. if err != nil {
  223. return notAPort, notAPort, err
  224. }
  225. return rPort, rPort, nil
  226. }
  227. sPorts := strings.SplitN(s, `-`, 2)
  228. for i := range sPorts {
  229. sPorts[i] = strings.Trim(sPorts[i], " ")
  230. }
  231. if len(sPorts) != 2 || sPorts[0] == "" || sPorts[1] == "" {
  232. return notAPort, notAPort, fmt.Errorf("appears to be a range but could not be parsed; `%s`", s)
  233. }
  234. startPort, err := parsePortValue("beginning range ", sPorts[0])
  235. if err != nil {
  236. return notAPort, notAPort, err
  237. }
  238. endPort, err := parsePortValue("ending range ", sPorts[1])
  239. if err != nil {
  240. return notAPort, notAPort, err
  241. }
  242. if startPort == PortAny {
  243. endPort = PortAny
  244. }
  245. return startPort, endPort, nil
  246. }
  247. // parsePortValue accepts a base-10 decimal in [0, 65535] and returns it
  248. // widened to int32. Using strconv.ParseUint with bitSize 16 rejects
  249. // negative input, out-of-range input (>65535), and any non-decimal byte
  250. // by construction, so the int32 widening that follows is provably safe
  251. // and cannot collide with PortAny (0) or PortFragment
  252. // (-1) via integer truncation.
  253. //
  254. // prefix is prepended to both error messages so callers can disambiguate
  255. // the single-port path (prefix="") from the range bounds (prefix="beginning
  256. // range " / "ending range "), preserving the historical error strings.
  257. func parsePortValue(prefix, s string) (int32, error) {
  258. n, err := strconv.ParseUint(s, 10, 16)
  259. if err == nil {
  260. return int32(n), nil
  261. }
  262. if errors.Is(err, strconv.ErrRange) {
  263. return 0, fmt.Errorf("%sout of range [0,65535]; `%s`", prefix, s)
  264. }
  265. return 0, fmt.Errorf("%swas not a number; `%s`", prefix, s)
  266. }