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 }