Raw File
endpoint_status_test.go
// Copyright 2019-2020 Authors of Cilium
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// +build !privileged_tests

package endpoint

import (
	"context"
	"fmt"
	"reflect"
	"time"

	"github.com/cilium/cilium/api/v1/models"
	"github.com/cilium/cilium/pkg/checker"
	"github.com/cilium/cilium/pkg/controller"
	"github.com/cilium/cilium/pkg/identity"
	cilium_v2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2"
	"github.com/cilium/cilium/pkg/labels"
	"github.com/cilium/cilium/pkg/option"
	"github.com/cilium/cilium/pkg/policy"
	"github.com/cilium/cilium/pkg/policy/trafficdirection"

	"gopkg.in/check.v1"
)

var (
	allowAllIdentityList = cilium_v2.AllowedIdentityList{{}}
	denyAllIdentityList  = cilium_v2.AllowedIdentityList(nil)
)

type endpointGeneratorSpec struct {
	failingControllers       int
	logErrors                int
	allowedIngressIdentities int
	allowedEgressIdentities  int
	numPortsPerIdentity      int
	fakeControllerManager    bool
}

type endpointStatusConfiguration map[string]bool

func (e endpointStatusConfiguration) EndpointStatusIsEnabled(name string) bool {
	if e != nil {
		return e[string(name)]
	}
	return false
}

func (s *EndpointSuite) newEndpoint(c *check.C, spec endpointGeneratorSpec) *Endpoint {
	e, err := NewEndpointFromChangeModel(context.TODO(), s, &FakeEndpointProxy{}, s.mgr, &models.EndpointChangeRequest{
		Addressing: &models.AddressPair{},
		ID:         200,
		Labels: models.Labels{
			"k8s:io.cilium.k8s.policy.cluster=default",
			"k8s:io.cilium.k8s.policy.serviceaccount=default",
			"k8s:io.kubernetes.pod.namespace=default",
			"k8s:name=probe",
		},
		State: models.EndpointState("waiting-for-identity"),
	})
	c.Assert(err, check.IsNil)

	e.SecurityIdentity = &identity.Identity{
		ID: 100,
		Labels: labels.NewLabelsFromModel([]string{
			"k8s:io.cilium.k8s.policy.cluster=default",
			"k8s:io.cilium.k8s.policy.serviceaccount=default",
			"k8s:io.kubernetes.pod.namespace=default",
			"k8s:name=probe",
		}),
	}

	if spec.fakeControllerManager {
		e.controllers = controller.FakeManager(spec.failingControllers)
	}

	for i := 0; i < spec.logErrors; i++ {
		e.status.addStatusLog(&statusLogMsg{
			Status: Status{Code: Failure, Msg: "Failure", Type: BPF},
		})
	}

	e.desiredPolicy.PolicyMapState = policy.MapState{}

	if spec.numPortsPerIdentity == 0 {
		spec.numPortsPerIdentity = 1
	}

	for i := 0; i < spec.allowedIngressIdentities; i++ {
		for n := 0; n < spec.numPortsPerIdentity; n++ {
			key := policy.Key{
				Identity:         uint32(i),
				DestPort:         uint16(80 + n),
				TrafficDirection: trafficdirection.Ingress.Uint8(),
			}
			e.desiredPolicy.PolicyMapState[key] = policy.MapStateEntry{}
		}
	}

	for i := 0; i < spec.allowedIngressIdentities; i++ {
		for n := 0; n < spec.numPortsPerIdentity; n++ {
			key := policy.Key{
				Identity:         uint32(i + 30000),
				DestPort:         uint16(80 + n),
				TrafficDirection: trafficdirection.Egress.Uint8(),
			}
			e.desiredPolicy.PolicyMapState[key] = policy.MapStateEntry{}
		}
	}

	return e
}

func (s *EndpointSuite) TestGetCiliumEndpointStatusSuccessfulControllers(c *check.C) {
	e := s.newEndpoint(c, endpointGeneratorSpec{})
	cepA := e.GetCiliumEndpointStatus(&endpointStatusConfiguration{})

	// Run successful controllers in the background
	for i := 0; i < 50; i++ {
		e.controllers.UpdateController(fmt.Sprintf("controller-%d", i),
			controller.ControllerParams{
				DoFunc: func(ctx context.Context) error {
					return nil
				},
				RunInterval: 10 * time.Millisecond,
			},
		)
	}
	defer e.controllers.RemoveAll()

	// Generate EndpointStatus in quick interval while controllers are
	// succeeding in the background
	timeout := time.After(1 * time.Second)
	tick := time.Tick(10 * time.Millisecond)
	for {
		select {
		case <-timeout:
			return
		case <-tick:
			cepB := e.GetCiliumEndpointStatus(&endpointStatusConfiguration{})
			c.Assert(cepA, checker.DeepEquals, cepB)
		}
	}
}

func (s *EndpointSuite) TestGetCiliumEndpointStatusSuccessfulLog(c *check.C) {
	e := s.newEndpoint(c, endpointGeneratorSpec{})
	cepA := e.GetCiliumEndpointStatus(&endpointStatusConfiguration{})

	go func() {
		for i := 0; i < 1000; i++ {
			e.status.addStatusLog(&statusLogMsg{
				Status: Status{Code: OK, Msg: "Success", Type: BPF},
			})
			time.Sleep(time.Millisecond)
		}
	}()

	// Generate EndpointStatus in quick interval while state transitions
	// are succeeding in the background
	timeout := time.After(1 * time.Second)
	tick := time.Tick(10 * time.Millisecond)
	for {
		select {
		case <-timeout:
			return
		case <-tick:
			cepB := e.GetCiliumEndpointStatus(&endpointStatusConfiguration{})
			c.Assert(cepA, checker.DeepEquals, cepB)
		}
	}
}

func (s *EndpointSuite) TestGetCiliumEndpointStatusDeepEqual(c *check.C) {
	a := s.newEndpoint(c, endpointGeneratorSpec{
		fakeControllerManager:    true,
		failingControllers:       10,
		logErrors:                maxLogs,
		allowedIngressIdentities: 100,
		allowedEgressIdentities:  100,
		numPortsPerIdentity:      10,
	})

	b := s.newEndpoint(c, endpointGeneratorSpec{
		fakeControllerManager:    true,
		failingControllers:       10,
		logErrors:                maxLogs,
		allowedIngressIdentities: 100,
		allowedEgressIdentities:  100,
		numPortsPerIdentity:      10,
	})

	cepA := a.GetCiliumEndpointStatus(&endpointStatusConfiguration{})
	cepB := b.GetCiliumEndpointStatus(&endpointStatusConfiguration{})

	c.Assert(cepA, checker.DeepEquals, cepB)
}

func (s *EndpointSuite) TestGetCiliumEndpointStatusCorrectnes(c *check.C) {
	e := s.newEndpoint(c, endpointGeneratorSpec{
		fakeControllerManager:    true,
		failingControllers:       10,
		logErrors:                maxLogs,
		allowedIngressIdentities: 100,
		allowedEgressIdentities:  100,
		numPortsPerIdentity:      10,
	})

	cep := e.GetCiliumEndpointStatus(&endpointStatusConfiguration{
		option.EndpointStatusLog: true,
	})

	c.Assert(len(cep.Log), check.Equals, cilium_v2.EndpointStatusLogEntries)
}

// apiResult is an individual desired AllowedIdentityEntry test result entry.
type apiResult struct {
	labels   string
	identity uint64
	dport    uint16
	proto    uint8
}

func prepareExpectedList(want []apiResult) cilium_v2.AllowedIdentityList {
	expectedList := denyAllIdentityList
	if want != nil {
		expectedList = cilium_v2.AllowedIdentityList{}
		for _, w := range want {
			entry := cilium_v2.IdentityTuple{
				Identity: w.identity,
				DestPort: w.dport,
				Protocol: w.proto,
			}
			if w.labels != "" {
				entry.IdentityLabels = map[string]string{
					w.labels: "",
				}
			}
			expectedList = append(expectedList, entry)
		}
		expectedList.Sort()
	}

	return expectedList
}

func (s *EndpointSuite) TestgetEndpointPolicyMapState(c *check.C) {
	e := s.newEndpoint(c, endpointGeneratorSpec{
		fakeControllerManager:    true,
		failingControllers:       10,
		logErrors:                maxLogs,
		allowedIngressIdentities: 100,
		allowedEgressIdentities:  100,
		numPortsPerIdentity:      10,
	})
	// Policy not enabled; allow all.
	apiPolicy := e.getEndpointPolicy()
	c.Assert(apiPolicy.Ingress.Allowed, checker.DeepEquals, allowAllIdentityList)
	c.Assert(apiPolicy.Egress.Allowed, checker.DeepEquals, allowAllIdentityList)

	fooLbls := labels.Labels{"": labels.ParseLabel("foo")}
	fooIdentity, _, err := e.allocator.AllocateIdentity(context.Background(), fooLbls, false)
	c.Assert(err, check.Equals, nil)
	defer s.mgr.Release(context.Background(), fooIdentity, false)

	e.desiredPolicy = policy.NewEndpointPolicy(s.repo)
	e.desiredPolicy.IngressPolicyEnabled = true
	e.desiredPolicy.EgressPolicyEnabled = true

	type args struct {
		identity  uint32
		destPort  uint16
		nexthdr   uint8
		direction trafficdirection.TrafficDirection
	}

	tests := []struct {
		name          string
		args          []args
		egressResult  []apiResult
		ingressResult []apiResult
	}{
		{
			name: "Deny all",
		},
		{
			name: "Allow all ingress",
			args: []args{
				{0, 0, 0, trafficdirection.Ingress},
			},
			ingressResult: []apiResult{{}},
			egressResult:  nil,
		},
		{
			name: "Allow all egress",
			args: []args{
				{0, 0, 0, trafficdirection.Egress},
			},
			ingressResult: nil,
			egressResult:  []apiResult{{}},
		},
		{
			name: "Allow all both directions",
			args: []args{
				{0, 0, 0, trafficdirection.Ingress},
				{0, 0, 0, trafficdirection.Egress},
			},
			ingressResult: []apiResult{{}},
			egressResult:  []apiResult{{}},
		},
		{
			name: "Allow world ingress",
			args: []args{
				{uint32(identity.ReservedIdentityWorld), 0, 0, trafficdirection.Ingress},
			},
			ingressResult: []apiResult{
				{"reserved:world", uint64(identity.ReservedIdentityWorld), 0, 0},
			},
			egressResult: nil,
		},
		{
			name: "Allow world egress",
			args: []args{
				{uint32(identity.ReservedIdentityWorld), 0, 0, trafficdirection.Egress},
			},
			ingressResult: nil,
			egressResult: []apiResult{
				{"reserved:world", uint64(identity.ReservedIdentityWorld), 0, 0},
			},
		},
		{
			name: "Allow world both directions",
			args: []args{
				{uint32(identity.ReservedIdentityWorld), 0, 0, trafficdirection.Ingress},
				{uint32(identity.ReservedIdentityWorld), 0, 0, trafficdirection.Egress},
			},
			ingressResult: []apiResult{
				{"reserved:world", uint64(identity.ReservedIdentityWorld), 0, 0},
			},
			egressResult: []apiResult{
				{"reserved:world", uint64(identity.ReservedIdentityWorld), 0, 0},
			},
		},
		{
			name: "Ingress mix of L3, L4, L3-dependent L4",
			args: []args{
				{uint32(fooIdentity.ID), 0, 0, trafficdirection.Ingress},  // L3-only map state
				{0, 80, 6, trafficdirection.Ingress},                      // L4-only map state
				{uint32(fooIdentity.ID), 80, 6, trafficdirection.Ingress}, // L3-dependent L4 map state
			},
			ingressResult: []apiResult{
				{"unspec:foo", uint64(fooIdentity.ID), 0, 0},
				{"", 0, 80, 6},
				{"unspec:foo", uint64(fooIdentity.ID), 80, 6},
			},
			egressResult: nil,
		},
		{
			name: "Egress mix of L3, L4, L3-dependent L4",
			args: []args{
				{uint32(fooIdentity.ID), 0, 0, trafficdirection.Egress},  // L3-only map state
				{0, 80, 6, trafficdirection.Egress},                      // L4-only map state
				{uint32(fooIdentity.ID), 80, 6, trafficdirection.Egress}, // L3-dependent L4 map state
			},
			ingressResult: nil,
			egressResult: []apiResult{
				{"unspec:foo", uint64(fooIdentity.ID), 0, 0},
				{"", 0, 80, 6},
				{"unspec:foo", uint64(fooIdentity.ID), 80, 6},
			},
		},
		{
			name: "World shadows CIDR ingress",
			args: []args{
				{uint32(identity.ReservedIdentityWorld), 0, 0, trafficdirection.Ingress},
				{uint32(identity.LocalIdentityFlag), 0, 0, trafficdirection.Ingress},
			},
			ingressResult: []apiResult{
				{"reserved:world", uint64(identity.ReservedIdentityWorld), 0, 0},
			},
			egressResult: nil,
		},
		{
			name: "World shadows CIDR egress",
			args: []args{
				{uint32(identity.ReservedIdentityWorld), 0, 0, trafficdirection.Egress},
				{uint32(identity.LocalIdentityFlag), 0, 0, trafficdirection.Egress},
			},
			ingressResult: nil,
			egressResult: []apiResult{
				{"reserved:world", uint64(identity.ReservedIdentityWorld), 0, 0},
			},
		},
	}

	for _, tt := range tests {
		e.desiredPolicy.PolicyMapState = policy.MapState{}
		for _, arg := range tt.args {
			t := policy.Key{
				Identity:         arg.identity,
				DestPort:         arg.destPort,
				Nexthdr:          arg.nexthdr,
				TrafficDirection: arg.direction.Uint8(),
			}
			e.desiredPolicy.PolicyMapState[t] = policy.MapStateEntry{}
		}
		expectedIngressList := prepareExpectedList(tt.ingressResult)
		expectedEgressList := prepareExpectedList(tt.egressResult)

		apiPolicy = e.getEndpointPolicy()
		c.Assert(apiPolicy.Ingress.Allowed, checker.DeepEquals, expectedIngressList)
		c.Assert(apiPolicy.Egress.Allowed, checker.DeepEquals, expectedEgressList)
	}
}

func (s *EndpointSuite) TestEndpointPolicyStatus(c *check.C) {
	tcs := []struct {
		ingressEnabled bool
		egressEnabled  bool
		auditEnabled   bool
		status         models.EndpointPolicyEnabled
	}{
		{false, false, false, models.EndpointPolicyEnabledNone},
		{true, false, false, models.EndpointPolicyEnabledIngress},
		{false, true, false, models.EndpointPolicyEnabledEgress},
		{true, true, false, models.EndpointPolicyEnabledBoth},
		{false, false, true, models.EndpointPolicyEnabledNone},
		{true, false, true, models.EndpointPolicyEnabledAuditIngress},
		{false, true, true, models.EndpointPolicyEnabledAuditEgress},
		{true, true, true, models.EndpointPolicyEnabledAuditBoth},
	}

	e := s.newEndpoint(c, endpointGeneratorSpec{})
	for _, tc := range tcs {
		e.realizedPolicy.IngressPolicyEnabled = tc.ingressEnabled
		e.realizedPolicy.EgressPolicyEnabled = tc.egressEnabled
		e.Options.SetBool(option.PolicyAuditMode, tc.auditEnabled)
		c.Assert(e.policyStatus(), checker.Equals, tc.status)
	}
}

func (s *EndpointSuite) TestEndpointPolicy(c *check.C) {
	tcs := []struct {
		ingressEnabled   bool
		egressEnabled    bool
		auditEnabled     bool
		ingressEnforcing bool
		egressEnforcing  bool
	}{
		{false, false, false, false, false},
		{true, false, false, true, false},
		{false, true, false, false, true},
		{true, true, false, true, true},
		{false, false, true, false, false},
		{true, false, true, false, false},
		{false, true, true, false, false},
		{true, true, true, false, false},
	}

	e := s.newEndpoint(c, endpointGeneratorSpec{})
	for _, tc := range tcs {
		e.desiredPolicy.IngressPolicyEnabled = tc.ingressEnabled
		e.desiredPolicy.EgressPolicyEnabled = tc.egressEnabled
		e.Options.SetBool(option.PolicyAuditMode, tc.auditEnabled)
		policy := e.getEndpointPolicy()
		c.Assert(policy.Ingress.Enforcing, checker.Equals, tc.ingressEnforcing)
		c.Assert(policy.Egress.Enforcing, checker.Equals, tc.egressEnforcing)
	}
}

func (s *EndpointSuite) BenchmarkGetCiliumEndpointStatusDeepEqual(c *check.C) {
	a := s.newEndpoint(c, endpointGeneratorSpec{
		fakeControllerManager:    true,
		failingControllers:       10,
		logErrors:                maxLogs,
		allowedIngressIdentities: 100,
		allowedEgressIdentities:  100,
		numPortsPerIdentity:      10,
	})

	b := s.newEndpoint(c, endpointGeneratorSpec{
		fakeControllerManager:    true,
		failingControllers:       10,
		logErrors:                maxLogs,
		allowedIngressIdentities: 100,
		allowedEgressIdentities:  100,
		numPortsPerIdentity:      10,
	})

	c.ResetTimer()
	for i := 0; i < c.N; i++ {
		if !reflect.DeepEqual(a, b) {
			c.Errorf("DeepEqual failed")
		}
	}
	c.StopTimer()
}

func (s *EndpointSuite) BenchmarkGetCiliumEndpointStatus(c *check.C) {
	e := s.newEndpoint(c, endpointGeneratorSpec{
		failingControllers:       10,
		logErrors:                maxLogs,
		allowedIngressIdentities: 100,
		allowedEgressIdentities:  100,
		numPortsPerIdentity:      10,
	})

	c.ResetTimer()
	for i := 0; i < c.N; i++ {
		status := e.GetCiliumEndpointStatus(&endpointStatusConfiguration{})
		c.Assert(status, check.Not(check.IsNil))
	}
	c.StopTimer()
}
back to top