Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions npm/pkg/controlplane/translation/parseSelector.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ import (
// an alphanumeric character (e.g. 'MyValue', or 'my_value', or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?'
var validLabelRegex = regexp.MustCompile("(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?")

// maxFlattenedNSSelectors caps how many labelSelectors a single namespaceSelector may be
// flattened into. Flattening multi-value In requirements produces the Cartesian product of
// their values, and each resulting selector is deep-copied and later turned into its own
// IPSet and ACL, so the cost grows exponentially with the number of such requirements. The
// cap is far above any workable policy (a selector fanning out this wide would already be
// unusable as iptables rules) while keeping a crafted selector from exhausting memory.
const maxFlattenedNSSelectors = 1000

// flattenNameSpaceSelector will help flatten multiple nameSpace selector match Expressions values
// into multiple label selectors helping with the OR condition.
func flattenNameSpaceSelector(nsSelector *metav1.LabelSelector) ([]metav1.LabelSelector, error) {
Expand Down Expand Up @@ -167,10 +175,22 @@ func flattenNameSpaceSelector(nsSelector *metav1.LabelSelector) ([]metav1.LabelS
}

// Now use the baseSelector and loop over multiValueMatchExprs to create all
// combinations of values
flatNsSelectors := []metav1.LabelSelector{
*baseSelector.DeepCopy(),
// combinations of values. The number of combinations is the product of the value
// counts, so it grows exponentially with the number of multi-value In requirements
// (19 two-value requirements already yield 2^19 selectors). Bound the product before
// doing any allocation: every selector below is deep-copied and later becomes its own
// IPSet and ACL, so an unbounded product exhausts memory on every node running NPM.
combinations := 1
for _, req := range multiValueMatchExprs {
if len(req.Values) > maxFlattenedNSSelectors/combinations {
log.Errorf("namespaceSelector [%v] expands past the %d selector limit", *nsSelector, maxFlattenedNSSelectors)
return nil, ErrTooManyFlattenedSelectors
}
combinations *= len(req.Values)
}

flatNsSelectors := make([]metav1.LabelSelector, 0, combinations)
flatNsSelectors = append(flatNsSelectors, *baseSelector.DeepCopy())
for _, req := range multiValueMatchExprs {
flatNsSelectors = zipMatchExprs(flatNsSelectors, req)
}
Expand Down
80 changes: 80 additions & 0 deletions npm/pkg/controlplane/translation/parseSelector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"testing"

"github.com/stretchr/testify/require"
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

Expand Down Expand Up @@ -721,6 +722,85 @@
}
}

// TestFlattenNameSpaceSelectorExpansionLimit verifies that a namespaceSelector whose
// multi-value In requirements would expand into more selectors than NPM is willing to
// translate is rejected before any allocation. Each flattened selector is deep-copied and
// later becomes its own IPSet and ACL, and the count is the product of the value counts,
// so an unbounded selector exhausts memory on every node running NPM.
func TestFlattenNameSpaceSelectorExpansionLimit(t *testing.T) {
twoValueReqs := func(n int) []metav1.LabelSelectorRequirement {
reqs := make([]metav1.LabelSelectorRequirement, 0, n)
for i := 0; i < n; i++ {
reqs = append(reqs, metav1.LabelSelectorRequirement{
Key: fmt.Sprintf("key%d", i),
Operator: metav1.LabelSelectorOpIn,
Values: []string{"a", "b"},
})
}
return reqs
}

// 2^9 = 512 selectors is under the limit and must still translate.
under := &metav1.LabelSelector{MatchExpressions: twoValueReqs(9)}
selectors, err := flattenNameSpaceSelector(under)
require.NoError(t, err)
require.Len(t, selectors, 512)

// 2^19 = 524288 selectors is the reported exhaustion case and must be rejected.
over := &metav1.LabelSelector{MatchExpressions: twoValueReqs(19)}
selectors, err = flattenNameSpaceSelector(over)
require.ErrorIs(t, err, ErrTooManyFlattenedSelectors)
require.Nil(t, selectors)

// A single requirement wider than the limit is rejected on the first iteration,
// so the guard cannot be sidestepped by using one very wide requirement.
values := make([]string, maxFlattenedNSSelectors+1)
for i := range values {
values[i] = fmt.Sprintf("v%d", i)
}
wide := &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{Key: "key", Operator: metav1.LabelSelectorOpIn, Values: values},
},
}
selectors, err = flattenNameSpaceSelector(wide)
require.ErrorIs(t, err, ErrTooManyFlattenedSelectors)
require.Nil(t, selectors)
}

// TestTranslatePolicyExpansionLimit verifies the expansion guard surfaces through the full
// translation path rather than being swallowed, so an oversized policy is rejected instead
// of being expanded.
func TestTranslatePolicyExpansionLimit(t *testing.T) {
reqs := make([]metav1.LabelSelectorRequirement, 0, 19)
for i := 0; i < 19; i++ {
reqs = append(reqs, metav1.LabelSelectorRequirement{
Key: fmt.Sprintf("key%d", i),
Operator: metav1.LabelSelectorOpIn,
Values: []string{"a", "b"},
})
}

pol := &networkingv1.NetworkPolicy{
ObjectMeta: metav1.ObjectMeta{Name: "expand", Namespace: "default"},

Check failure on line 785 in npm/pkg/controlplane/translation/parseSelector_test.go

View workflow job for this annotation

GitHub Actions / Lint (ubuntu-latest)

string `default` has 26 occurrences, but such constant `defaultNS` already exists (goconst)

Check failure on line 785 in npm/pkg/controlplane/translation/parseSelector_test.go

View workflow job for this annotation

GitHub Actions / Lint (windows-latest)

string `default` has 26 occurrences, but such constant `defaultNS` already exists (goconst)
Spec: networkingv1.NetworkPolicySpec{
PodSelector: metav1.LabelSelector{},
PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress},
Ingress: []networkingv1.NetworkPolicyIngressRule{
{
From: []networkingv1.NetworkPolicyPeer{
{NamespaceSelector: &metav1.LabelSelector{MatchExpressions: reqs}},
},
},
},
},
}

npmNetPol, err := TranslatePolicy(pol, false)
require.ErrorIs(t, err, ErrTooManyFlattenedSelectors)
require.Nil(t, npmNetPol)
}

func TestIsValidLabel(t *testing.T) {
good := []string{
"",
Expand Down
4 changes: 4 additions & 0 deletions npm/pkg/controlplane/translation/translatePolicy.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ var (
// none of In, NotIn, Exists or DoesNotExist. NPM fails closed rather than dropping the requirement,
// which could otherwise silently widen the selector.
ErrUnsupportedMatchExpressionOperator = errors.New("unsupported matchExpression operator")
// ErrTooManyFlattenedSelectors is returned when flattening a namespaceSelector's multi-value In
// requirements would produce more labelSelectors than NPM is willing to translate. The count is
// the product of the value counts, so it grows exponentially with the number of such requirements.
ErrTooManyFlattenedSelectors = errors.New("namespaceSelector expands into too many label selectors")
// ErrUnsupportedIPAddress is returned when an unsupported IP address, such as IPV6, is used
ErrUnsupportedIPAddress = errors.New("unsupported IP address")
// ErrUnsupportedNonCIDR is returned when non-CIDR blocks are passed in with NPM Lite enabled. NPM Lite allows deny-all and allow-all policies
Expand Down
Loading