mirror of
https://github.com/slackhq/nebula.git
synced 2026-11-15 16:26:52 +01:00
improvements to config parsing
This commit is contained in:
1 parent
ed8ae57fee
commit
04e80f0dcf
6 files changed
+58
-7
No files matched your search
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Firewall rules are held as a flat list with a bitset of rule ids per port, instead of a tree of maps copied
|
||||||
|
into every port of a range. A rule like `port: 1024-65535` now costs kilobytes rather than tens of megabytes,
|
||||||
|
and matching is faster on every path.
|
||||||
|
- A firewall rule's `proto` can be `udplite`, `dccp`, `sctp`, `icmpv6`, or any IP protocol number. A rule with
|
||||||
|
a port or port range on a protocol that has no ports, such as `proto: 47`, is rejected rather than silently
|
||||||
|
never matching. `port: any` and `port: fragment` apply to every protocol.
|
||||||
|
- An ICMP non-first fragment is matched against `port: fragment` rules like a fragment of any other protocol.
|
||||||
|
It was only matched against `port: any` rules before.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- A firewall rule with `group: []`, `groups:` with no value, or a `groups` entry that isn't a string, such as
|
||||||
|
`groups: [1, 2]`, no longer crashes Nebula on startup or reload. An empty `group` array is rejected, a null
|
||||||
|
`groups` means no groups, and non-string entries are read as strings.
|
||||||
|
|
||||||
## [1.11.1] - 2026-08-21
|
## [1.11.1] - 2026-08-21
|
||||||
|
|
||||||
See the [v1.11.1](https://github.com/slackhq/nebula/milestone/30?closed=1) milestone for a complete list of changes.
|
See the [v1.11.1](https://github.com/slackhq/nebula/milestone/30?closed=1) milestone for a complete list of changes.
|
||||||
|
|||||||
+7
-2
@@ -159,6 +159,9 @@ func yamlToConfigRule(l *slog.Logger, p any, table string, i int) (configRule, e
|
|||||||
if len(v) > 1 {
|
if len(v) > 1 {
|
||||||
return r, errors.New("group should contain a single value, an array with more than one entry was provided")
|
return r, errors.New("group should contain a single value, an array with more than one entry was provided")
|
||||||
}
|
}
|
||||||
|
if len(v) == 0 {
|
||||||
|
return r, errors.New("group should contain a single value, an empty array was provided")
|
||||||
|
}
|
||||||
|
|
||||||
l.Warn("group was an array with a single value, converting to simple value",
|
l.Warn("group was an array with a single value, converting to simple value",
|
||||||
"table", table,
|
"table", table,
|
||||||
@@ -169,13 +172,15 @@ func yamlToConfigRule(l *slog.Logger, p any, table string, i int) (configRule, e
|
|||||||
|
|
||||||
singleGroup := toString("group", m)
|
singleGroup := toString("group", m)
|
||||||
|
|
||||||
if rg, ok := m["groups"]; ok {
|
// A null value, `groups:` with nothing after it, is the same as no groups
|
||||||
|
if rg, ok := m["groups"]; ok && rg != nil {
|
||||||
switch reflect.TypeOf(rg).Kind() {
|
switch reflect.TypeOf(rg).Kind() {
|
||||||
case reflect.Slice:
|
case reflect.Slice:
|
||||||
|
// Every entry is read as a string, as the other fields are, so `groups: [1, 2]` is the groups "1" and "2"
|
||||||
v := reflect.ValueOf(rg)
|
v := reflect.ValueOf(rg)
|
||||||
r.Groups = make([]string, v.Len())
|
r.Groups = make([]string, v.Len())
|
||||||
for i := 0; i < v.Len(); i++ {
|
for i := 0; i < v.Len(); i++ {
|
||||||
r.Groups[i] = v.Index(i).Interface().(string)
|
r.Groups[i] = fmt.Sprintf("%v", v.Index(i).Interface())
|
||||||
}
|
}
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
r.Groups = []string{rg.(string)}
|
r.Groups = []string{rg.(string)}
|
||||||
|
|||||||
@@ -291,6 +291,26 @@ func TestAddRulesFromConfig(t *testing.T) {
|
|||||||
require.NoError(t, addRulesFromConfig(l, true, conf, mf))
|
require.NoError(t, addRulesFromConfig(l, true, conf, mf))
|
||||||
assert.Equal(t, addRuleCall{incoming: true, proto: ProtoAny, startPort: 1, endPort: 1, groups: []string{"a", "b"}, ip: "", localIp: ""}, mf.lastCall)
|
assert.Equal(t, addRuleCall{incoming: true, proto: ProtoAny, startPort: 1, endPort: 1, groups: []string{"a", "b"}, ip: "", localIp: ""}, mf.lastCall)
|
||||||
|
|
||||||
|
// Test group and groups values that used to crash: an empty group array is rejected, groups entries that
|
||||||
|
// aren't strings are read as strings, and a null groups is no groups
|
||||||
|
conf = config.NewC(test.NewLogger())
|
||||||
|
mf = &mockFirewall{}
|
||||||
|
conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "group": []any{}}}}
|
||||||
|
require.EqualError(t, addRulesFromConfig(l, true, conf, mf), "firewall.inbound rule #0; group should contain a single value, an empty array was provided")
|
||||||
|
|
||||||
|
conf = config.NewC(test.NewLogger())
|
||||||
|
mf = &mockFirewall{}
|
||||||
|
conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "groups": []any{1, 2}}}}
|
||||||
|
require.NoError(t, addRulesFromConfig(l, true, conf, mf))
|
||||||
|
assert.Equal(t, []string{"1", "2"}, mf.lastCall.groups)
|
||||||
|
|
||||||
|
conf = config.NewC(test.NewLogger())
|
||||||
|
mf = &mockFirewall{}
|
||||||
|
conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "groups": nil, "host": "a"}}}
|
||||||
|
require.NoError(t, addRulesFromConfig(l, true, conf, mf))
|
||||||
|
assert.Empty(t, mf.lastCall.groups)
|
||||||
|
assert.Equal(t, "a", mf.lastCall.host)
|
||||||
|
|
||||||
// Test Add error
|
// Test Add error
|
||||||
conf = config.NewC(test.NewLogger())
|
conf = config.NewC(test.NewLogger())
|
||||||
mf = &mockFirewall{}
|
mf = &mockFirewall{}
|
||||||
|
|||||||
@@ -110,6 +110,11 @@ func parseRule(proto uint8, startPort, endPort int32, groups []string, host, cid
|
|||||||
if startPort <= PortAny && PortAny <= endPort {
|
if startPort <= PortAny && PortAny <= endPort {
|
||||||
startPort, endPort = PortAny, PortAny
|
startPort, endPort = PortAny, PortAny
|
||||||
}
|
}
|
||||||
|
// A port rule on a protocol without ports could never match. ICMP rules were coerced to `any` by AddRule,
|
||||||
|
// and a proto `any` rule with a port applies to the protocols that have them.
|
||||||
|
if startPort > PortAny && proto != ProtoAny && !iputil.HasPorts(proto) {
|
||||||
|
return rule{}, fmt.Errorf("protocol %d has no ports, port must be any or fragment", proto)
|
||||||
|
}
|
||||||
|
|
||||||
r := rule{
|
r := rule{
|
||||||
proto: proto,
|
proto: proto,
|
||||||
@@ -214,7 +219,8 @@ func (pi *protoIndex) add(r *rule, id int) {
|
|||||||
case PortFragment:
|
case PortFragment:
|
||||||
pi.fragment.add(id)
|
pi.fragment.add(id)
|
||||||
default:
|
default:
|
||||||
// A range only applies to a protocol with ports.
|
// A proto `any` rule with a port is indexed for the protocols that have ports, parseRule rejects a port on
|
||||||
|
// a protocol without them.
|
||||||
if pi.hasPorts {
|
if pi.hasPorts {
|
||||||
pi.addPorts(int(r.startPort), int(r.endPort), id)
|
pi.addPorts(int(r.startPort), int(r.endPort), id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -223,8 +223,9 @@ func TestRulesBuilder_protoIndex(t *testing.T) {
|
|||||||
require.NoError(t, rb.AddRule(true, iputil.IPProtocolTCP, PortFragment, PortFragment, nil, "fragment", "", "", "", ""))
|
require.NoError(t, rb.AddRule(true, iputil.IPProtocolTCP, PortFragment, PortFragment, nil, "fragment", "", "", "", ""))
|
||||||
// A range that includes port 0 is port `any`.
|
// A range that includes port 0 is port `any`.
|
||||||
require.NoError(t, rb.AddRule(true, iputil.IPProtocolUDP, 0, 100, nil, "zero", "", "", "", ""))
|
require.NoError(t, rb.AddRule(true, iputil.IPProtocolUDP, 0, 100, nil, "zero", "", "", "", ""))
|
||||||
// GRE has no ports, so a port rule on it never applies.
|
// GRE has no ports, so a port rule on it is rejected rather than never applying, and `any` and `fragment` apply
|
||||||
require.NoError(t, rb.AddRule(true, 47, 22, 22, nil, "gre", "", "", "", ""))
|
require.Error(t, rb.AddRule(true, 47, 22, 22, nil, "gre", "", "", "", ""))
|
||||||
|
require.NoError(t, rb.AddRule(true, 47, PortFragment, PortFragment, nil, "gre", "", "", "", ""))
|
||||||
rules := rb.Build(nil, nil)
|
rules := rb.Build(nil, nil)
|
||||||
|
|
||||||
hosts := func(proto uint8, key int32) []string {
|
hosts := func(proto uint8, key int32) []string {
|
||||||
@@ -252,7 +253,7 @@ func TestRulesBuilder_protoIndex(t *testing.T) {
|
|||||||
assert.False(t, rules.In.protos[47].hasPorts)
|
assert.False(t, rules.In.protos[47].hasPorts)
|
||||||
assert.Equal(t, []string{"any"}, hosts(47, PortAny))
|
assert.Equal(t, []string{"any"}, hosts(47, PortAny))
|
||||||
assert.Equal(t, []string{"any"}, hosts(47, 22))
|
assert.Equal(t, []string{"any"}, hosts(47, 22))
|
||||||
assert.Equal(t, []string{"any"}, hosts(47, PortFragment))
|
assert.Equal(t, []string{"any", "gre"}, hosts(47, PortFragment))
|
||||||
assert.Equal(t, []string{"any"}, hosts(iputil.IPProtocolICMP, PortFragment))
|
assert.Equal(t, []string{"any"}, hosts(iputil.IPProtocolICMP, PortFragment))
|
||||||
|
|
||||||
// Protocols without rules of their own share an index of the proto `any` rules: one for protocols with
|
// Protocols without rules of their own share an index of the proto `any` rules: one for protocols with
|
||||||
|
|||||||
+3
-1
@@ -32,7 +32,9 @@ type Table struct {
|
|||||||
protos [256]*protoIndex
|
protos [256]*protoIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
// protoIndex holds the rules that can apply to one protocol, in a set for each kind of packet.
|
// protoIndex holds the rules that can apply to one protocol, in a set for each kind of packet. Rule ids are
|
||||||
|
// indexes into the direction's rules, so every set is sized by the direction's rule count, not the protocol's:
|
||||||
|
// byPort costs 64 KB per 8 rules in the direction.
|
||||||
type protoIndex struct {
|
type protoIndex struct {
|
||||||
// hasPorts reports whether the protocol has ports; see iputil.HasPorts.
|
// hasPorts reports whether the protocol has ports; see iputil.HasPorts.
|
||||||
hasPorts bool
|
hasPorts bool
|
||||||
|
|||||||
Reference in new issue
Block a user