mirror of
https://github.com/slackhq/nebula.git
synced 2026-11-15 15:16:52 +01:00
309 lines
8.4 KiB
Go
309 lines
8.4 KiB
Go
package firewall
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/netip"
|
|
"reflect"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/slackhq/nebula/config"
|
|
"github.com/slackhq/nebula/iputil"
|
|
)
|
|
|
|
// ruleAdder takes firewall rules, it lets tests see what addRulesFromConfig adds
|
|
type ruleAdder interface {
|
|
AddRule(incoming bool, proto uint8, startPort int32, endPort int32, groups []string, host string, cidr, localCidr string, caName string, caSha string) error
|
|
}
|
|
|
|
// RulesFromConfig reads the inbound and outbound firewall rules from c and builds them, see RulesBuilder.Build
|
|
func RulesFromConfig(l *slog.Logger, c *config.C, vpnNetworks, unsafeNetworks []netip.Prefix) (Rules, error) {
|
|
b := NewRulesBuilder(l)
|
|
b.defaultLocalCIDRAny = c.GetBool("firewall.default_local_cidr_any", false)
|
|
|
|
if err := addRulesFromConfig(l, false, c, b); err != nil {
|
|
return Rules{}, err
|
|
}
|
|
if err := addRulesFromConfig(l, true, c, b); err != nil {
|
|
return Rules{}, err
|
|
}
|
|
|
|
return b.Build(vpnNetworks, unsafeNetworks), nil
|
|
}
|
|
|
|
func addRulesFromConfig(l *slog.Logger, inbound bool, c *config.C, ra ruleAdder) error {
|
|
var table string
|
|
if inbound {
|
|
table = "firewall.inbound"
|
|
} else {
|
|
table = "firewall.outbound"
|
|
}
|
|
|
|
r := c.Get(table)
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
|
|
rs, ok := r.([]any)
|
|
if !ok {
|
|
return fmt.Errorf("%s failed to parse, should be an array of rules", table)
|
|
}
|
|
|
|
for i, t := range rs {
|
|
r, err := yamlToConfigRule(l, t, table, i)
|
|
if err != nil {
|
|
return fmt.Errorf("%s rule #%v; %s", table, i, err)
|
|
}
|
|
|
|
if r.Code != "" && r.Port != "" {
|
|
return fmt.Errorf("%s rule #%v; only one of port or code should be provided", table, i)
|
|
}
|
|
|
|
if r.Host == "" && len(r.Groups) == 0 && r.Cidr == "" && r.LocalCidr == "" && r.CAName == "" && r.CASha == "" {
|
|
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)
|
|
}
|
|
|
|
var sPort, errPort string
|
|
if r.Code != "" {
|
|
errPort = "code"
|
|
sPort = r.Code
|
|
} else {
|
|
errPort = "port"
|
|
sPort = r.Port
|
|
}
|
|
|
|
proto, ok := protoByName(r.Proto)
|
|
if !ok {
|
|
// Any other protocol by number. 0 is reserved for `any`.
|
|
n, perr := strconv.ParseUint(r.Proto, 10, 8)
|
|
if perr != nil || n == 0 {
|
|
return fmt.Errorf("%s rule #%v; proto was not understood; `%s`", table, i, r.Proto)
|
|
}
|
|
proto = uint8(n)
|
|
}
|
|
// ICMPv6 rules are ICMP rules, see RulesBuilder.AddRule
|
|
if proto == iputil.IPProtocolICMPv6 {
|
|
proto = iputil.IPProtocolICMP
|
|
}
|
|
|
|
var startPort, endPort int32
|
|
if proto == iputil.IPProtocolICMP {
|
|
startPort = PortAny
|
|
endPort = PortAny
|
|
if sPort != "" {
|
|
l.Warn("ignoring port specification for ICMP firewall rule", "port", sPort)
|
|
}
|
|
} else {
|
|
startPort, endPort, err = parsePort(sPort)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("%s rule #%v; %s %s", table, i, errPort, err)
|
|
}
|
|
|
|
if warning := r.sanity(); warning != nil {
|
|
l.Warn("firewall rule sanity check",
|
|
"table", table,
|
|
"rule", i,
|
|
"warning", warning,
|
|
)
|
|
}
|
|
|
|
err = ra.AddRule(inbound, proto, startPort, endPort, r.Groups, r.Host, r.Cidr, r.LocalCidr, r.CAName, r.CASha)
|
|
if err != nil {
|
|
return fmt.Errorf("%s rule #%v; %s", table, i, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type configRule struct {
|
|
Port string
|
|
Code string
|
|
Proto string
|
|
Host string
|
|
Groups []string
|
|
Cidr string
|
|
LocalCidr string
|
|
CAName string
|
|
CASha string
|
|
}
|
|
|
|
func yamlToConfigRule(l *slog.Logger, p any, table string, i int) (configRule, error) {
|
|
r := configRule{}
|
|
|
|
m, ok := p.(map[string]any)
|
|
if !ok {
|
|
return r, errors.New("could not parse rule")
|
|
}
|
|
|
|
toString := func(k string, m map[string]any) string {
|
|
v, ok := m[k]
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("%v", v)
|
|
}
|
|
|
|
r.Port = toString("port", m)
|
|
r.Code = toString("code", m)
|
|
r.Proto = toString("proto", m)
|
|
r.Host = toString("host", m)
|
|
r.Cidr = toString("cidr", m)
|
|
r.LocalCidr = toString("local_cidr", m)
|
|
r.CAName = toString("ca_name", m)
|
|
r.CASha = toString("ca_sha", m)
|
|
|
|
// Make sure group isn't an array
|
|
if v, ok := m["group"].([]any); ok {
|
|
if len(v) > 1 {
|
|
return r, errors.New("group should contain a single value, an array with more than one entry was provided")
|
|
}
|
|
|
|
l.Warn("group was an array with a single value, converting to simple value",
|
|
"table", table,
|
|
"rule", i,
|
|
)
|
|
m["group"] = v[0]
|
|
}
|
|
|
|
singleGroup := toString("group", m)
|
|
|
|
if rg, ok := m["groups"]; ok {
|
|
switch reflect.TypeOf(rg).Kind() {
|
|
case reflect.Slice:
|
|
v := reflect.ValueOf(rg)
|
|
r.Groups = make([]string, v.Len())
|
|
for i := 0; i < v.Len(); i++ {
|
|
r.Groups[i] = v.Index(i).Interface().(string)
|
|
}
|
|
case reflect.String:
|
|
r.Groups = []string{rg.(string)}
|
|
default:
|
|
r.Groups = []string{fmt.Sprintf("%v", rg)}
|
|
}
|
|
}
|
|
|
|
//flatten group vs groups
|
|
if singleGroup != "" {
|
|
// Check if we have both groups and group provided in the rule config
|
|
if len(r.Groups) > 0 {
|
|
return r, fmt.Errorf("only one of group or groups should be defined, both provided")
|
|
}
|
|
r.Groups = []string{singleGroup}
|
|
}
|
|
|
|
return r, nil
|
|
}
|
|
|
|
// sanity returns an error if the rule would be evaluated in a way that would short-circuit a configured check on a wildcard value
|
|
// rules are evaluated as "port AND proto AND (ca_sha OR ca_name) AND (host OR group OR groups OR cidr) AND local_cidr"
|
|
func (r *configRule) sanity() error {
|
|
//port, proto, local_cidr are AND, no need to check here
|
|
//ca_sha and ca_name don't have a wildcard value, no need to check here
|
|
groupsEmpty := len(r.Groups) == 0
|
|
hostEmpty := r.Host == ""
|
|
cidrEmpty := r.Cidr == ""
|
|
|
|
if (groupsEmpty && hostEmpty && cidrEmpty) == true {
|
|
return nil //no content!
|
|
}
|
|
|
|
groupsHasAny := slices.Contains(r.Groups, "any")
|
|
if groupsHasAny && len(r.Groups) > 1 {
|
|
return fmt.Errorf("groups spec [%s] contains the group '\"any\". This rule will ignore the other groups specified", r.Groups)
|
|
}
|
|
|
|
if r.Host == "any" {
|
|
if !groupsEmpty {
|
|
return fmt.Errorf("groups specified as %s, but host=any will match any host, regardless of groups", r.Groups)
|
|
}
|
|
|
|
if !cidrEmpty {
|
|
return fmt.Errorf("cidr specified as %s, but host=any will match any host, regardless of cidr", r.Cidr)
|
|
}
|
|
}
|
|
|
|
if groupsHasAny {
|
|
if !hostEmpty && r.Host != "any" {
|
|
return fmt.Errorf("groups spec [%s] contains the group '\"any\". This rule will ignore the specified host %s", r.Groups, r.Host)
|
|
}
|
|
if !cidrEmpty {
|
|
return fmt.Errorf("groups spec [%s] contains the group '\"any\". This rule will ignore the specified cidr %s", r.Groups, r.Cidr)
|
|
}
|
|
}
|
|
|
|
if r.Code != "" {
|
|
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)
|
|
}
|
|
|
|
//todo alert on cidr-any
|
|
|
|
return nil
|
|
}
|
|
|
|
func parsePort(s string) (int32, int32, error) {
|
|
const notAPort int32 = -2
|
|
if s == "any" {
|
|
return PortAny, PortAny, nil
|
|
}
|
|
if s == "fragment" {
|
|
return PortFragment, PortFragment, nil
|
|
}
|
|
if !strings.Contains(s, `-`) {
|
|
rPort, err := parsePortValue("", s)
|
|
if err != nil {
|
|
return notAPort, notAPort, err
|
|
}
|
|
return rPort, rPort, nil
|
|
}
|
|
|
|
sPorts := strings.SplitN(s, `-`, 2)
|
|
for i := range sPorts {
|
|
sPorts[i] = strings.Trim(sPorts[i], " ")
|
|
}
|
|
if len(sPorts) != 2 || sPorts[0] == "" || sPorts[1] == "" {
|
|
return notAPort, notAPort, fmt.Errorf("appears to be a range but could not be parsed; `%s`", s)
|
|
}
|
|
|
|
startPort, err := parsePortValue("beginning range ", sPorts[0])
|
|
if err != nil {
|
|
return notAPort, notAPort, err
|
|
}
|
|
|
|
endPort, err := parsePortValue("ending range ", sPorts[1])
|
|
if err != nil {
|
|
return notAPort, notAPort, err
|
|
}
|
|
|
|
if startPort == PortAny {
|
|
endPort = PortAny
|
|
}
|
|
|
|
return startPort, endPort, nil
|
|
}
|
|
|
|
// parsePortValue accepts a base-10 decimal in [0, 65535] and returns it
|
|
// widened to int32. Using strconv.ParseUint with bitSize 16 rejects
|
|
// negative input, out-of-range input (>65535), and any non-decimal byte
|
|
// by construction, so the int32 widening that follows is provably safe
|
|
// and cannot collide with PortAny (0) or PortFragment
|
|
// (-1) via integer truncation.
|
|
//
|
|
// prefix is prepended to both error messages so callers can disambiguate
|
|
// the single-port path (prefix="") from the range bounds (prefix="beginning
|
|
// range " / "ending range "), preserving the historical error strings.
|
|
func parsePortValue(prefix, s string) (int32, error) {
|
|
n, err := strconv.ParseUint(s, 10, 16)
|
|
if err == nil {
|
|
return int32(n), nil
|
|
}
|
|
if errors.Is(err, strconv.ErrRange) {
|
|
return 0, fmt.Errorf("%sout of range [0,65535]; `%s`", prefix, s)
|
|
}
|
|
return 0, fmt.Errorf("%swas not a number; `%s`", prefix, s)
|
|
}
|