config.go 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  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. var proto uint8
  65. switch r.Proto {
  66. case "any":
  67. proto = ProtoAny
  68. case "tcp":
  69. proto = iputil.IPProtocolTCP
  70. case "udp":
  71. proto = iputil.IPProtocolUDP
  72. case "udplite":
  73. proto = iputil.IPProtocolUDPLite
  74. case "dccp":
  75. proto = iputil.IPProtocolDCCP
  76. case "sctp":
  77. proto = iputil.IPProtocolSCTP
  78. case "icmp":
  79. proto = iputil.IPProtocolICMP
  80. default:
  81. // Any other protocol by number. 0 is reserved for `any`.
  82. n, perr := strconv.ParseUint(r.Proto, 10, 8)
  83. if perr != nil || n == 0 {
  84. return fmt.Errorf("%s rule #%v; proto was not understood; `%s`", table, i, r.Proto)
  85. }
  86. proto = uint8(n)
  87. }
  88. var startPort, endPort int32
  89. if proto == iputil.IPProtocolICMP || proto == iputil.IPProtocolICMPv6 {
  90. startPort = PortAny
  91. endPort = PortAny
  92. if sPort != "" {
  93. l.Warn("ignoring port specification for ICMP firewall rule", "port", sPort)
  94. }
  95. } else {
  96. startPort, endPort, err = parsePort(sPort)
  97. }
  98. if err != nil {
  99. return fmt.Errorf("%s rule #%v; %s %s", table, i, errPort, err)
  100. }
  101. if warning := r.sanity(); warning != nil {
  102. l.Warn("firewall rule sanity check",
  103. "table", table,
  104. "rule", i,
  105. "warning", warning,
  106. )
  107. }
  108. err = ra.AddRule(inbound, proto, startPort, endPort, r.Groups, r.Host, r.Cidr, r.LocalCidr, r.CAName, r.CASha)
  109. if err != nil {
  110. return fmt.Errorf("%s rule #%v; %s", table, i, err)
  111. }
  112. }
  113. return nil
  114. }
  115. type configRule struct {
  116. Port string
  117. Code string
  118. Proto string
  119. Host string
  120. Groups []string
  121. Cidr string
  122. LocalCidr string
  123. CAName string
  124. CASha string
  125. }
  126. func yamlToConfigRule(l *slog.Logger, p any, table string, i int) (configRule, error) {
  127. r := configRule{}
  128. m, ok := p.(map[string]any)
  129. if !ok {
  130. return r, errors.New("could not parse rule")
  131. }
  132. toString := func(k string, m map[string]any) string {
  133. v, ok := m[k]
  134. if !ok {
  135. return ""
  136. }
  137. return fmt.Sprintf("%v", v)
  138. }
  139. r.Port = toString("port", m)
  140. r.Code = toString("code", m)
  141. r.Proto = toString("proto", m)
  142. r.Host = toString("host", m)
  143. r.Cidr = toString("cidr", m)
  144. r.LocalCidr = toString("local_cidr", m)
  145. r.CAName = toString("ca_name", m)
  146. r.CASha = toString("ca_sha", m)
  147. // Make sure group isn't an array
  148. if v, ok := m["group"].([]any); ok {
  149. if len(v) > 1 {
  150. return r, errors.New("group should contain a single value, an array with more than one entry was provided")
  151. }
  152. l.Warn("group was an array with a single value, converting to simple value",
  153. "table", table,
  154. "rule", i,
  155. )
  156. m["group"] = v[0]
  157. }
  158. singleGroup := toString("group", m)
  159. if rg, ok := m["groups"]; ok {
  160. switch reflect.TypeOf(rg).Kind() {
  161. case reflect.Slice:
  162. v := reflect.ValueOf(rg)
  163. r.Groups = make([]string, v.Len())
  164. for i := 0; i < v.Len(); i++ {
  165. r.Groups[i] = v.Index(i).Interface().(string)
  166. }
  167. case reflect.String:
  168. r.Groups = []string{rg.(string)}
  169. default:
  170. r.Groups = []string{fmt.Sprintf("%v", rg)}
  171. }
  172. }
  173. //flatten group vs groups
  174. if singleGroup != "" {
  175. // Check if we have both groups and group provided in the rule config
  176. if len(r.Groups) > 0 {
  177. return r, fmt.Errorf("only one of group or groups should be defined, both provided")
  178. }
  179. r.Groups = []string{singleGroup}
  180. }
  181. return r, nil
  182. }
  183. // sanity returns an error if the rule would be evaluated in a way that would short-circuit a configured check on a wildcard value
  184. // rules are evaluated as "port AND proto AND (ca_sha OR ca_name) AND (host OR group OR groups OR cidr) AND local_cidr"
  185. func (r *configRule) sanity() error {
  186. //port, proto, local_cidr are AND, no need to check here
  187. //ca_sha and ca_name don't have a wildcard value, no need to check here
  188. groupsEmpty := len(r.Groups) == 0
  189. hostEmpty := r.Host == ""
  190. cidrEmpty := r.Cidr == ""
  191. if (groupsEmpty && hostEmpty && cidrEmpty) == true {
  192. return nil //no content!
  193. }
  194. groupsHasAny := slices.Contains(r.Groups, "any")
  195. if groupsHasAny && len(r.Groups) > 1 {
  196. return fmt.Errorf("groups spec [%s] contains the group '\"any\". This rule will ignore the other groups specified", r.Groups)
  197. }
  198. if r.Host == "any" {
  199. if !groupsEmpty {
  200. return fmt.Errorf("groups specified as %s, but host=any will match any host, regardless of groups", r.Groups)
  201. }
  202. if !cidrEmpty {
  203. return fmt.Errorf("cidr specified as %s, but host=any will match any host, regardless of cidr", r.Cidr)
  204. }
  205. }
  206. if groupsHasAny {
  207. if !hostEmpty && r.Host != "any" {
  208. return fmt.Errorf("groups spec [%s] contains the group '\"any\". This rule will ignore the specified host %s", r.Groups, r.Host)
  209. }
  210. if !cidrEmpty {
  211. return fmt.Errorf("groups spec [%s] contains the group '\"any\". This rule will ignore the specified cidr %s", r.Groups, r.Cidr)
  212. }
  213. }
  214. if r.Code != "" {
  215. 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)
  216. }
  217. //todo alert on cidr-any
  218. return nil
  219. }
  220. func parsePort(s string) (int32, int32, error) {
  221. const notAPort int32 = -2
  222. if s == "any" {
  223. return PortAny, PortAny, nil
  224. }
  225. if s == "fragment" {
  226. return PortFragment, PortFragment, nil
  227. }
  228. if !strings.Contains(s, `-`) {
  229. rPort, err := parsePortValue("", s)
  230. if err != nil {
  231. return notAPort, notAPort, err
  232. }
  233. return rPort, rPort, nil
  234. }
  235. sPorts := strings.SplitN(s, `-`, 2)
  236. for i := range sPorts {
  237. sPorts[i] = strings.Trim(sPorts[i], " ")
  238. }
  239. if len(sPorts) != 2 || sPorts[0] == "" || sPorts[1] == "" {
  240. return notAPort, notAPort, fmt.Errorf("appears to be a range but could not be parsed; `%s`", s)
  241. }
  242. startPort, err := parsePortValue("beginning range ", sPorts[0])
  243. if err != nil {
  244. return notAPort, notAPort, err
  245. }
  246. endPort, err := parsePortValue("ending range ", sPorts[1])
  247. if err != nil {
  248. return notAPort, notAPort, err
  249. }
  250. if startPort == PortAny {
  251. endPort = PortAny
  252. }
  253. return startPort, endPort, nil
  254. }
  255. // parsePortValue accepts a base-10 decimal in [0, 65535] and returns it
  256. // widened to int32. Using strconv.ParseUint with bitSize 16 rejects
  257. // negative input, out-of-range input (>65535), and any non-decimal byte
  258. // by construction, so the int32 widening that follows is provably safe
  259. // and cannot collide with PortAny (0) or PortFragment
  260. // (-1) via integer truncation.
  261. //
  262. // prefix is prepended to both error messages so callers can disambiguate
  263. // the single-port path (prefix="") from the range bounds (prefix="beginning
  264. // range " / "ending range "), preserving the historical error strings.
  265. func parsePortValue(prefix, s string) (int32, error) {
  266. n, err := strconv.ParseUint(s, 10, 16)
  267. if err == nil {
  268. return int32(n), nil
  269. }
  270. if errors.Is(err, strconv.ErrRange) {
  271. return 0, fmt.Errorf("%sout of range [0,65535]; `%s`", prefix, s)
  272. }
  273. return 0, fmt.Errorf("%swas not a number; `%s`", prefix, s)
  274. }