github.com/cilium/cilium@v1.16.2/pkg/redirectpolicy/redirectpolicy.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package redirectpolicy
     5  
     6  import (
     7  	"fmt"
     8  
     9  	"k8s.io/apimachinery/pkg/types"
    10  
    11  	"github.com/cilium/cilium/api/v1/models"
    12  	cmtypes "github.com/cilium/cilium/pkg/clustermesh/types"
    13  	"github.com/cilium/cilium/pkg/k8s"
    14  	v2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2"
    15  	slimcorev1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/api/core/v1"
    16  	"github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/labels"
    17  	k8sUtils "github.com/cilium/cilium/pkg/k8s/utils"
    18  	lb "github.com/cilium/cilium/pkg/loadbalancer"
    19  	"github.com/cilium/cilium/pkg/policy/api"
    20  )
    21  
    22  type lrpConfigType = int
    23  
    24  const (
    25  	lrpConfigTypeNone = iota
    26  	// LRP config is specified using IP/port tuple.
    27  	lrpConfigTypeAddr
    28  	// LRP config is specified using Kubernetes service name and namespace.
    29  	lrpConfigTypeSvc
    30  )
    31  
    32  type frontendConfigType = int
    33  
    34  const (
    35  	frontendTypeUnknown = iota
    36  	// Get frontends for service with clusterIP and all service ports.
    37  	svcFrontendAll
    38  	// Get frontends for service with clusterIP and parsed config named ports.
    39  	svcFrontendNamedPorts
    40  	// Get a frontend for service with clusterIP and parsed config port.
    41  	svcFrontendSinglePort
    42  	// Get a frontend with parsed config frontend IP and port.
    43  	addrFrontendSinglePort
    44  	// Get frontends with parsed config frontend IPs and named ports.
    45  	addrFrontendNamedPorts
    46  )
    47  
    48  // LRPConfig is the internal representation of Cilium Local Redirect Policy.
    49  type LRPConfig struct {
    50  	// id is the parsed config name and namespace
    51  	id k8s.ServiceID
    52  	// uid is the unique identifier assigned by Kubernetes
    53  	uid types.UID
    54  	// lrpType is the type of either address matcher or service matcher policy
    55  	lrpType lrpConfigType
    56  	// frontendType is the type for the parsed config frontend.
    57  	frontendType frontendConfigType
    58  	// frontendMappings is a slice of policy config frontend mappings that include
    59  	// frontend address, frontend port name, and a slice of its associated backends
    60  	frontendMappings []*feMapping
    61  	// serviceID is the parsed service name and namespace
    62  	serviceID *k8s.ServiceID
    63  	// backendSelector is an endpoint selector generated from the parsed policy selector
    64  	backendSelector api.EndpointSelector
    65  	// backendPorts is a slice of backend port and protocol along with the port name
    66  	backendPorts []bePortInfo
    67  	// backendPortsByPortName is a map indexed by port name with the value as
    68  	// a pointer to bePortInfo for easy lookup into backendPorts
    69  	backendPortsByPortName map[portName]*bePortInfo
    70  	// skipRedirectFromBackend is the flag that enables/disables redirection
    71  	// for traffic matching the policy frontend(s) from the backends selected by the policy
    72  	skipRedirectFromBackend bool
    73  }
    74  
    75  type frontend = lb.L3n4Addr
    76  
    77  // backend encapsulates loadbalancer.L3n4Addr for a pod along with podID (pod
    78  // name and namespace). There can be multiple backend pods for an LRP frontend,
    79  // in such cases, the podID will be used to keep track of updates related to
    80  // a pod.
    81  type backend struct {
    82  	lb.L3n4Addr
    83  	podID          podID
    84  	podNetnsCookie uint64
    85  }
    86  
    87  func (be *backend) GetModel() *models.LRPBackend {
    88  	ip := be.AddrCluster.String()
    89  	return &models.LRPBackend{
    90  		PodID: be.podID.String(),
    91  		BackendAddress: &models.BackendAddress{
    92  			IP:   &ip,
    93  			Port: be.Port,
    94  		},
    95  	}
    96  }
    97  
    98  type portName = string
    99  
   100  // feMapping stores frontend address and a list of associated backend addresses.
   101  type feMapping struct {
   102  	feAddr      *frontend
   103  	podBackends []backend
   104  	fePort      portName
   105  }
   106  
   107  func (feM *feMapping) GetModel() *models.FrontendMapping {
   108  	bes := make([]*models.LRPBackend, 0, len(feM.podBackends))
   109  	for _, be := range feM.podBackends {
   110  		bes = append(bes, be.GetModel())
   111  	}
   112  	return &models.FrontendMapping{
   113  		FrontendAddress: &models.FrontendAddress{
   114  			IP:       feM.feAddr.AddrCluster.String(),
   115  			Protocol: feM.feAddr.Protocol,
   116  			Port:     feM.feAddr.Port,
   117  		},
   118  		Backends: bes,
   119  	}
   120  }
   121  
   122  type bePortInfo struct {
   123  	// l4Addr is the port and protocol
   124  	l4Addr lb.L4Addr
   125  	// name is the port name
   126  	name string
   127  }
   128  
   129  // PolicyID includes policy name and namespace
   130  type policyID = k8s.ServiceID
   131  
   132  // Parse parses the specified cilium local redirect policy spec, and returns
   133  // a sanitized LRPConfig.
   134  func Parse(clrp *v2.CiliumLocalRedirectPolicy, sanitize bool) (*LRPConfig, error) {
   135  	name := clrp.ObjectMeta.Name
   136  	if name == "" {
   137  		return nil, fmt.Errorf("CiliumLocalRedirectPolicy must have a name")
   138  	}
   139  
   140  	namespace := k8sUtils.ExtractNamespace(&clrp.ObjectMeta)
   141  	if namespace == "" {
   142  		return nil, fmt.Errorf("CiliumLocalRedirectPolicy must have a non-empty namespace")
   143  	}
   144  
   145  	if sanitize {
   146  		return getSanitizedLRPConfig(name, namespace, clrp.UID, clrp.Spec)
   147  	} else {
   148  		return &LRPConfig{
   149  			id: k8s.ServiceID{
   150  				Name:      name,
   151  				Namespace: namespace,
   152  			},
   153  		}, nil
   154  	}
   155  }
   156  
   157  func getSanitizedLRPConfig(name, namespace string, uid types.UID, spec v2.CiliumLocalRedirectPolicySpec) (*LRPConfig, error) {
   158  
   159  	var (
   160  		addrMatcher    = spec.RedirectFrontend.AddressMatcher
   161  		svcMatcher     = spec.RedirectFrontend.ServiceMatcher
   162  		redirectTo     = spec.RedirectBackend
   163  		frontendType   = frontendTypeUnknown
   164  		checkNamedPort = false
   165  		lrpType        lrpConfigType
   166  		k8sSvc         *k8s.ServiceID
   167  		fe             *frontend
   168  		feMappings     []*feMapping
   169  		bePorts        []bePortInfo
   170  		bePortsMap     = make(map[portName]*bePortInfo)
   171  	)
   172  
   173  	// Parse frontend config
   174  	switch {
   175  	case addrMatcher == nil && svcMatcher == nil:
   176  		return nil, fmt.Errorf("both address and service" +
   177  			" matchers can not be empty")
   178  	case addrMatcher != nil && svcMatcher != nil:
   179  		return nil, fmt.Errorf("both address and service" +
   180  			" matchers can not be specified")
   181  	case addrMatcher != nil:
   182  		// LRP specifies IP/port tuple config for traffic that needs to be redirected.
   183  		addrCluster, err := cmtypes.ParseAddrCluster(addrMatcher.IP)
   184  		if err != nil {
   185  			return nil, fmt.Errorf("invalid address matcher IP %v: %w", addrMatcher.IP, err)
   186  		}
   187  		if len(addrMatcher.ToPorts) > 1 {
   188  			// If there are multiple ports, then the ports must be named.
   189  			checkNamedPort = true
   190  			frontendType = addrFrontendNamedPorts
   191  		} else if len(addrMatcher.ToPorts) == 1 {
   192  			frontendType = addrFrontendSinglePort
   193  		}
   194  		feMappings = make([]*feMapping, len(addrMatcher.ToPorts))
   195  		for i, portInfo := range addrMatcher.ToPorts {
   196  			p, pName, proto, err := portInfo.SanitizePortInfo(checkNamedPort)
   197  			if err != nil {
   198  				return nil, fmt.Errorf("invalid address matcher port: %w", err)
   199  			}
   200  			// Set the scope to ScopeExternal as the externalTrafficPolicy is set to Cluster.
   201  			fe = lb.NewL3n4Addr(proto, addrCluster, p, lb.ScopeExternal)
   202  			feM := &feMapping{
   203  				feAddr: fe,
   204  				fePort: pName,
   205  			}
   206  			feMappings[i] = feM
   207  		}
   208  		lrpType = lrpConfigTypeAddr
   209  	case svcMatcher != nil:
   210  		// LRP specifies service config for traffic that needs to be redirected.
   211  		k8sSvc = &k8s.ServiceID{
   212  			Name:      svcMatcher.Name,
   213  			Namespace: svcMatcher.Namespace,
   214  		}
   215  		if k8sSvc.Name == "" {
   216  			return nil, fmt.Errorf("kubernetes service name can" +
   217  				"not be empty")
   218  		}
   219  		if namespace != "" && k8sSvc.Namespace != namespace {
   220  			return nil, fmt.Errorf("kubernetes service namespace" +
   221  				"does not match with the CiliumLocalRedirectPolicy namespace")
   222  		}
   223  		switch len(svcMatcher.ToPorts) {
   224  		case 0:
   225  			frontendType = svcFrontendAll
   226  		case 1:
   227  			frontendType = svcFrontendSinglePort
   228  		default:
   229  			frontendType = svcFrontendNamedPorts
   230  			checkNamedPort = true
   231  		}
   232  		feMappings = make([]*feMapping, len(svcMatcher.ToPorts))
   233  		for i, portInfo := range svcMatcher.ToPorts {
   234  			p, pName, proto, err := portInfo.SanitizePortInfo(checkNamedPort)
   235  			if err != nil {
   236  				return nil, fmt.Errorf("invalid service matcher port: %w", err)
   237  			}
   238  			// Set the scope to ScopeExternal as the externalTrafficPolicy is set to Cluster.
   239  			// frontend ip will later be populated with the clusterIP of the service.
   240  			fe = lb.NewL3n4Addr(proto, cmtypes.AddrCluster{}, p, lb.ScopeExternal)
   241  			feM := &feMapping{
   242  				feAddr: fe,
   243  				fePort: pName,
   244  			}
   245  			feMappings[i] = feM
   246  		}
   247  		lrpType = lrpConfigTypeSvc
   248  	default:
   249  		return nil, fmt.Errorf("invalid local redirect policy %v", spec)
   250  	}
   251  
   252  	if lrpType == lrpConfigTypeNone || frontendType == frontendTypeUnknown {
   253  		return nil, fmt.Errorf("invalid local redirect policy %v", spec)
   254  	}
   255  
   256  	// Parse backend config
   257  	bePorts = make([]bePortInfo, len(redirectTo.ToPorts))
   258  	if len(redirectTo.ToPorts) > 1 {
   259  		// We check for backend named ports if either frontend/backend have
   260  		// multiple ports.
   261  		checkNamedPort = true
   262  	}
   263  	for i, portInfo := range redirectTo.ToPorts {
   264  		p, pName, proto, err := portInfo.SanitizePortInfo(checkNamedPort)
   265  		if err != nil {
   266  			return nil, fmt.Errorf("invalid backend port: %w", err)
   267  		}
   268  		beP := bePortInfo{
   269  			l4Addr: lb.L4Addr{
   270  				Protocol: proto,
   271  				Port:     p,
   272  			},
   273  			name: pName,
   274  		}
   275  		bePorts[i] = beP
   276  		if len(pName) > 0 {
   277  			// Port name is present.
   278  			bePortsMap[pName] = &bePorts[i]
   279  
   280  		}
   281  	}
   282  	// When a single port is specified in the LRP frontend, the protocol for frontend and
   283  	// backend must match.
   284  	if len(feMappings) == 1 {
   285  		if bePorts[0].l4Addr.Protocol != feMappings[0].feAddr.Protocol {
   286  			return nil, fmt.Errorf("backend protocol must match with " +
   287  				"frontend protocol")
   288  		}
   289  	}
   290  
   291  	// Get an EndpointSelector from the passed policy labelSelector for optimized matching.
   292  	selector := api.NewESFromK8sLabelSelector("", &redirectTo.LocalEndpointSelector)
   293  
   294  	return &LRPConfig{
   295  		uid:                     uid,
   296  		serviceID:               k8sSvc,
   297  		frontendMappings:        feMappings,
   298  		backendSelector:         selector,
   299  		backendPorts:            bePorts,
   300  		backendPortsByPortName:  bePortsMap,
   301  		lrpType:                 lrpType,
   302  		frontendType:            frontendType,
   303  		skipRedirectFromBackend: spec.SkipRedirectFromBackend,
   304  		id: k8s.ServiceID{
   305  			Name:      name,
   306  			Namespace: namespace,
   307  		},
   308  	}, nil
   309  }
   310  
   311  // policyConfigSelectsPod determines if the given pod is selected by the policy
   312  // config based on matching labels of config and pod.
   313  func (config *LRPConfig) policyConfigSelectsPod(pod *slimcorev1.Pod) bool {
   314  	return config.backendSelector.Matches(labels.Set(pod.GetLabels()))
   315  }
   316  
   317  // checkNamespace returns true if config namespace matches with the given namespace.
   318  // The namespace check isn't applicable for clusterwide LRPs.
   319  func (config *LRPConfig) checkNamespace(namespace string) bool {
   320  	if config.id.Namespace != "" {
   321  		return namespace == config.id.Namespace
   322  	}
   323  	return true
   324  }
   325  
   326  func (config *LRPConfig) GetModel() *models.LRPSpec {
   327  	if config == nil {
   328  		return nil
   329  	}
   330  
   331  	var feType, lrpType string
   332  	switch config.frontendType {
   333  	case frontendTypeUnknown:
   334  		feType = "unknown"
   335  	case svcFrontendAll:
   336  		feType = "clusterIP + all svc ports"
   337  	case svcFrontendNamedPorts:
   338  		feType = "clusterIP + named ports"
   339  	case svcFrontendSinglePort:
   340  		feType = "clusterIP + port"
   341  	case addrFrontendSinglePort:
   342  		feType = "IP + port"
   343  	case addrFrontendNamedPorts:
   344  		feType = "IP + named ports"
   345  	}
   346  
   347  	switch config.lrpType {
   348  	case lrpConfigTypeNone:
   349  		lrpType = "none"
   350  	case lrpConfigTypeAddr:
   351  		lrpType = "addr"
   352  	case lrpConfigTypeSvc:
   353  		lrpType = "svc"
   354  	}
   355  
   356  	feMappingModelArray := make([]*models.FrontendMapping, 0, len(config.frontendMappings))
   357  	for _, feM := range config.frontendMappings {
   358  		feMappingModelArray = append(feMappingModelArray, feM.GetModel())
   359  	}
   360  
   361  	var svcID string
   362  	if config.serviceID != nil {
   363  		svcID = config.serviceID.String()
   364  	}
   365  
   366  	return &models.LRPSpec{
   367  		UID:              string(config.uid),
   368  		Name:             config.id.Name,
   369  		Namespace:        config.id.Namespace,
   370  		FrontendType:     feType,
   371  		LrpType:          lrpType,
   372  		ServiceID:        svcID,
   373  		FrontendMappings: feMappingModelArray,
   374  	}
   375  }