github.com/cilium/cilium@v1.16.2/pkg/bgpv1/manager/reconciler/route_policy.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package reconciler 5 6 import ( 7 "context" 8 "crypto/sha256" 9 "fmt" 10 "net/netip" 11 "sort" 12 "strconv" 13 "strings" 14 15 "github.com/cilium/hive/cell" 16 "github.com/osrg/gobgp/v3/pkg/packet/bgp" 17 "github.com/sirupsen/logrus" 18 "k8s.io/apimachinery/pkg/util/sets" 19 20 "github.com/cilium/cilium/pkg/bgpv1/manager/instance" 21 "github.com/cilium/cilium/pkg/bgpv1/manager/store" 22 "github.com/cilium/cilium/pkg/bgpv1/types" 23 v2api "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2" 24 v2alpha1api "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2alpha1" 25 "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/labels" 26 slim_metav1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/meta/v1" 27 ) 28 29 const ( 30 maxPrefixLenIPv4 = 32 31 maxPrefixLenIPv6 = 128 32 ) 33 34 type RoutePolicyReconcilerOut struct { 35 cell.Out 36 37 Reconciler ConfigReconciler `group:"bgp-config-reconciler"` 38 } 39 40 type RoutePolicyReconciler struct { 41 lbPoolStore store.BGPCPResourceStore[*v2alpha1api.CiliumLoadBalancerIPPool] 42 podPoolStore store.BGPCPResourceStore[*v2alpha1api.CiliumPodIPPool] 43 } 44 45 // RoutePolicyReconcilerMetadata holds routing policies configured by the policy reconciler keyed by policy name. 46 type RoutePolicyReconcilerMetadata map[string]*types.RoutePolicy 47 48 func NewRoutePolicyReconciler( 49 lbStore store.BGPCPResourceStore[*v2alpha1api.CiliumLoadBalancerIPPool], 50 podStore store.BGPCPResourceStore[*v2alpha1api.CiliumPodIPPool], 51 ) RoutePolicyReconcilerOut { 52 return RoutePolicyReconcilerOut{ 53 Reconciler: &RoutePolicyReconciler{ 54 lbPoolStore: lbStore, 55 podPoolStore: podStore, 56 }, 57 } 58 } 59 60 func (r *RoutePolicyReconciler) Name() string { 61 return "RoutePolicy" 62 } 63 64 func (r *RoutePolicyReconciler) Priority() int { 65 // Should reconcile after the NeighborReconciler (so have higher priority number), 66 // as neighbor resets are performed from this reconciler. 67 // This is not a hard requirement, just to avoid some warnings. 68 return 70 69 } 70 71 func (r *RoutePolicyReconciler) Init(_ *instance.ServerWithConfig) error { 72 return nil 73 } 74 75 func (r *RoutePolicyReconciler) Cleanup(_ *instance.ServerWithConfig) {} 76 77 func (r *RoutePolicyReconciler) Reconcile(ctx context.Context, params ReconcileParams) error { 78 l := log.WithFields(logrus.Fields{"component": "RoutePolicyReconciler"}) 79 80 if params.DesiredConfig == nil { 81 return fmt.Errorf("attempted routing policy reconciliation with nil DesiredConfig") 82 } 83 if params.CurrentServer == nil { 84 return fmt.Errorf("attempted routing policy reconciliation with nil ServerWithConfig") 85 } 86 if params.CiliumNode == nil { 87 return fmt.Errorf("attempted routing policy reconciliation with nil local CiliumNode") 88 } 89 90 // take currently configured policies from cache 91 currentPolicies := r.getMetadata(params.CurrentServer) 92 93 // compile set of desired policies 94 // note: only per-neighbor export policies are supported at this time 95 desiredPolicies := make(map[string]*types.RoutePolicy) 96 for _, n := range params.DesiredConfig.Neighbors { 97 for _, routeAttrs := range n.AdvertisedPathAttributes { 98 exportPolicy, err := r.pathAttributesToPolicy(routeAttrs, n.PeerAddress, params) 99 if err != nil { 100 return fmt.Errorf("failed to convert BGP PathAttributes to a RoutePolicy: %w", err) 101 } 102 if len(exportPolicy.Statements) > 0 { 103 desiredPolicies[exportPolicy.Name] = exportPolicy 104 } 105 } 106 } 107 108 var toAdd, toRemove, toUpdate []*types.RoutePolicy 109 110 for _, p := range desiredPolicies { 111 if existing, found := currentPolicies[p.Name]; found { 112 if !existing.DeepEqual(p) { 113 toUpdate = append(toUpdate, p) 114 } 115 } else { 116 toAdd = append(toAdd, p) 117 } 118 } 119 for _, p := range currentPolicies { 120 if _, found := desiredPolicies[p.Name]; !found { 121 toRemove = append(toRemove, p) 122 } 123 } 124 125 resetPeers := make(map[string]bool) 126 127 // add missing policies 128 for _, p := range toAdd { 129 l.Infof("Adding route policy %s to vrouter %d", p.Name, params.DesiredConfig.LocalASN) 130 err := params.CurrentServer.Server.AddRoutePolicy(ctx, types.RoutePolicyRequest{ 131 DefaultExportAction: types.RoutePolicyActionNone, // no change to the default action 132 Policy: p, 133 }) 134 if err != nil { 135 return fmt.Errorf("failed adding route policy %v to vrouter %d: %w", p.Name, params.DesiredConfig.LocalASN, err) 136 } 137 resetPeers[peerAddressFromPolicy(p)] = true 138 } 139 // update modified policies 140 for _, p := range toUpdate { 141 // As proper implementation of an update operation for complex policies would be quite involved, 142 // we resort to recreating the policies that need an update here. 143 l.Infof("Updating (re-creating) route policy %s in vrouter %d", p.Name, params.DesiredConfig.LocalASN) 144 existing := currentPolicies[p.Name] 145 err := params.CurrentServer.Server.RemoveRoutePolicy(ctx, types.RoutePolicyRequest{Policy: existing}) 146 if err != nil { 147 return fmt.Errorf("failed removing route policy %v from vrouter %d: %w", existing.Name, params.DesiredConfig.LocalASN, err) 148 } 149 err = params.CurrentServer.Server.AddRoutePolicy(ctx, types.RoutePolicyRequest{ 150 DefaultExportAction: types.RoutePolicyActionNone, // no change to the default action 151 Policy: p, 152 }) 153 if err != nil { 154 return fmt.Errorf("failed adding route policy %v to vrouter %d: %w", p.Name, params.DesiredConfig.LocalASN, err) 155 } 156 resetPeers[peerAddressFromPolicy(p)] = true 157 } 158 // remove old policies 159 for _, p := range toRemove { 160 l.Infof("Removing route policy %s from vrouter %d", p.Name, params.DesiredConfig.LocalASN) 161 err := params.CurrentServer.Server.RemoveRoutePolicy(ctx, types.RoutePolicyRequest{Policy: p}) 162 if err != nil { 163 return fmt.Errorf("failed removing route policy %v from vrouter %d: %w", p.Name, params.DesiredConfig.LocalASN, err) 164 } 165 resetPeers[peerAddressFromPolicy(p)] = true 166 } 167 168 // soft-reset affected BGP peers to apply the changes on already advertised routes 169 for peer := range resetPeers { 170 l.Infof("Resetting peer %s on vrouter %d due to a routing policy change", peer, params.DesiredConfig.LocalASN) 171 req := types.ResetNeighborRequest{ 172 PeerAddress: peer, 173 Soft: true, 174 SoftResetDirection: types.SoftResetDirectionOut, // we are using only export policies 175 } 176 err := params.CurrentServer.Server.ResetNeighbor(ctx, req) 177 if err != nil { 178 // non-fatal error (may happen if the neighbor is not up), just log it 179 l.Warnf("error by resetting peer %s after a routing policy change: %v", peer, err) 180 } 181 } 182 183 // reconciliation successful, update the cache of configured policies which is now equal to desired polices 184 r.storeMetadata(params.CurrentServer, desiredPolicies) 185 return nil 186 } 187 188 func (r *RoutePolicyReconciler) getMetadata(sc *instance.ServerWithConfig) RoutePolicyReconcilerMetadata { 189 if _, found := sc.ReconcilerMetadata[r.Name()]; !found { 190 sc.ReconcilerMetadata[r.Name()] = make(RoutePolicyReconcilerMetadata) 191 } 192 return sc.ReconcilerMetadata[r.Name()].(RoutePolicyReconcilerMetadata) 193 } 194 195 func (r *RoutePolicyReconciler) storeMetadata(sc *instance.ServerWithConfig, meta RoutePolicyReconcilerMetadata) { 196 sc.ReconcilerMetadata[r.Name()] = meta 197 } 198 199 // pathAttributesToPolicy prepares an export policy configured by CRD using the Advertised Path Attributes feature 200 func (r *RoutePolicyReconciler) pathAttributesToPolicy(attrs v2alpha1api.CiliumBGPPathAttributes, neighborAddress string, params ReconcileParams) (*types.RoutePolicy, error) { 201 var v4Prefixes, v6Prefixes types.PolicyPrefixMatchList 202 203 policy := &types.RoutePolicy{ 204 Name: pathAttributesPolicyName(attrs, neighborAddress), 205 Type: types.RoutePolicyTypeExport, 206 } 207 208 labelSelector, err := slim_metav1.LabelSelectorAsSelector(attrs.Selector) 209 if err != nil { 210 return nil, fmt.Errorf("failed constructing LabelSelector: %w", err) 211 } 212 213 switch attrs.SelectorType { 214 case v2alpha1api.CPIPKindDefinition: 215 localPools := r.populateLocalPools(params.CiliumNode) 216 podPoolList, err := r.podPoolStore.List() 217 if err != nil { 218 return nil, fmt.Errorf("failed to list pod ip pools from store: %w", err) 219 } 220 for _, pool := range podPoolList { 221 if attrs.Selector != nil && !labelSelector.Matches(labels.Set(pool.Labels)) { 222 continue 223 } 224 // only include pool cidrs that have been allocated to the local node. 225 if cidrs, ok := localPools[pool.Name]; ok { 226 for _, cidr := range cidrs { 227 if cidr.Addr().Is4() { 228 prefixLen := int(pool.Spec.IPv4.MaskSize) 229 v4Prefixes = append(v4Prefixes, &types.RoutePolicyPrefixMatch{CIDR: cidr, PrefixLenMin: prefixLen, PrefixLenMax: prefixLen}) 230 } else { 231 prefixLen := int(pool.Spec.IPv6.MaskSize) 232 v6Prefixes = append(v6Prefixes, &types.RoutePolicyPrefixMatch{CIDR: cidr, PrefixLenMin: prefixLen, PrefixLenMax: prefixLen}) 233 } 234 } 235 } 236 } 237 case v2alpha1api.CiliumLoadBalancerIPPoolSelectorName: 238 lbPoolList, err := r.lbPoolStore.List() 239 if err != nil { 240 return nil, fmt.Errorf("failed to list lb ip pools from store: %w", err) 241 } 242 for _, pool := range lbPoolList { 243 if pool.Spec.Disabled { 244 continue 245 } 246 if attrs.Selector != nil && !labelSelector.Matches(labels.Set(pool.Labels)) { 247 continue 248 } 249 prefixesSeen := sets.New[netip.Prefix]() 250 for _, cidrBlock := range pool.Spec.Blocks { 251 cidr, err := netip.ParsePrefix(string(cidrBlock.Cidr)) 252 if err != nil { 253 return nil, fmt.Errorf("failed to parse IPAM pool CIDR %s: %w", cidrBlock.Cidr, err) 254 } 255 if cidr.Addr().Is4() { 256 v4Prefixes = append(v4Prefixes, &types.RoutePolicyPrefixMatch{CIDR: cidr, PrefixLenMin: maxPrefixLenIPv4, PrefixLenMax: maxPrefixLenIPv4}) 257 } else { 258 v6Prefixes = append(v6Prefixes, &types.RoutePolicyPrefixMatch{CIDR: cidr, PrefixLenMin: maxPrefixLenIPv6, PrefixLenMax: maxPrefixLenIPv6}) 259 } 260 prefixesSeen.Insert(cidr) 261 } 262 } 263 case v2alpha1api.PodCIDRSelectorName: 264 if attrs.Selector != nil && !labelSelector.Matches(labels.Set(params.CiliumNode.Labels)) { 265 break 266 } 267 for _, podCIDR := range params.CiliumNode.Spec.IPAM.PodCIDRs { 268 cidr, err := netip.ParsePrefix(podCIDR) 269 if err != nil { 270 return nil, fmt.Errorf("failed to parse PodCIDR %s: %w", podCIDR, err) 271 } 272 if cidr.Addr().Is4() { 273 v4Prefixes = append(v4Prefixes, &types.RoutePolicyPrefixMatch{CIDR: cidr, PrefixLenMin: cidr.Bits(), PrefixLenMax: cidr.Bits()}) 274 } else { 275 v6Prefixes = append(v6Prefixes, &types.RoutePolicyPrefixMatch{CIDR: cidr, PrefixLenMin: cidr.Bits(), PrefixLenMax: cidr.Bits()}) 276 } 277 } 278 default: 279 return nil, fmt.Errorf("invalid route policy SelectorType: %s", attrs.SelectorType) 280 } 281 282 // sort prefixes to have consistent order for DeepEqual 283 sort.Slice(v4Prefixes, v4Prefixes.Less) 284 sort.Slice(v6Prefixes, v6Prefixes.Less) 285 286 // dedup + sort communities to have consistent order for DeepEqual 287 var communities, largeCommunities []string 288 if attrs.Communities != nil { 289 communities, err = mergeAndDedupCommunities(attrs.Communities.Standard, attrs.Communities.WellKnown) 290 if err != nil { 291 return nil, err 292 } 293 largeCommunities = dedupLargeCommunities(attrs.Communities.Large) 294 sort.Strings(communities) 295 sort.Strings(largeCommunities) 296 } 297 298 // Due to a GoBGP limitation, we need to generate a separate statement for v4 and v6 prefixes, as families 299 // can not be mixed in a single statement. Nevertheless, they can be both part of the same Policy. 300 if len(v4Prefixes) > 0 { 301 policy.Statements = append(policy.Statements, policyStatement(neighborAddress, v4Prefixes, attrs.LocalPreference, communities, largeCommunities)) 302 } 303 if len(v6Prefixes) > 0 { 304 policy.Statements = append(policy.Statements, policyStatement(neighborAddress, v6Prefixes, attrs.LocalPreference, communities, largeCommunities)) 305 } 306 return policy, nil 307 } 308 309 // populateLocalPools returns a map of allocated multi-pool IPAM CIDRs of the local CiliumNode, 310 // keyed by the pool name. 311 func (r *RoutePolicyReconciler) populateLocalPools(localNode *v2api.CiliumNode) map[string][]netip.Prefix { 312 var ( 313 l = log.WithFields( 314 logrus.Fields{ 315 "component": "RoutePolicyReconciler", 316 }, 317 ) 318 ) 319 320 if localNode == nil { 321 return nil 322 } 323 324 lp := make(map[string][]netip.Prefix) 325 for _, pool := range localNode.Spec.IPAM.Pools.Allocated { 326 var prefixes []netip.Prefix 327 for _, cidr := range pool.CIDRs { 328 if p, err := cidr.ToPrefix(); err == nil { 329 prefixes = append(prefixes, *p) 330 } else { 331 l.Errorf("invalid ipam pool cidr %v: %v", cidr, err) 332 } 333 } 334 lp[pool.Pool] = prefixes 335 } 336 337 return lp 338 } 339 340 // pathAttributesPolicyName returns a policy name derived from the provided CiliumBGPPathAttributes 341 // (SelectorType and Selector) and NeighborAddress 342 func pathAttributesPolicyName(attrs v2alpha1api.CiliumBGPPathAttributes, neighborAddress string) string { 343 res := neighborAddress + "-" + attrs.SelectorType 344 if attrs.Selector != nil { 345 h := sha256.New() 346 selectorBytes, err := attrs.Selector.Marshal() 347 if err == nil { 348 h.Write(selectorBytes) 349 } 350 res += "-" + fmt.Sprintf("%x", h.Sum(nil)) 351 } 352 return res 353 } 354 355 func policyStatement(neighborAddr string, prefixes []*types.RoutePolicyPrefixMatch, localPref *int64, communities, largeCommunities []string) *types.RoutePolicyStatement { 356 return &types.RoutePolicyStatement{ 357 Conditions: types.RoutePolicyConditions{ 358 MatchNeighbors: []string{neighborAddr}, 359 MatchPrefixes: prefixes, 360 }, 361 Actions: types.RoutePolicyActions{ 362 RouteAction: types.RoutePolicyActionNone, // continue with the processing of the next statements / policies 363 SetLocalPreference: localPref, 364 AddCommunities: communities, 365 AddLargeCommunities: largeCommunities, 366 }, 367 } 368 } 369 370 // peerAddressFromPolicy returns the first neighbor address found in a routing policy. 371 func peerAddressFromPolicy(p *types.RoutePolicy) string { 372 if p == nil { 373 return "" 374 } 375 for _, s := range p.Statements { 376 for _, m := range s.Conditions.MatchNeighbors { 377 return m 378 } 379 } 380 return "" 381 } 382 383 // mergeAndDedupCommunities merges numeric standard community and well-known community strings, 384 // deduplicated by their actual community values. 385 func mergeAndDedupCommunities(standard []v2alpha1api.BGPStandardCommunity, wellKnown []v2alpha1api.BGPWellKnownCommunity) ([]string, error) { 386 var res []string 387 existing := sets.New[uint32]() 388 for _, c := range standard { 389 val, err := parseCommunity(string(c)) 390 if err != nil { 391 return nil, fmt.Errorf("failed to parse standard BGP community: %w", err) 392 } 393 if existing.Has(val) { 394 continue 395 } 396 existing.Insert(val) 397 res = append(res, string(c)) 398 } 399 for _, c := range wellKnown { 400 val, ok := bgp.WellKnownCommunityValueMap[string(c)] 401 if !ok { 402 return nil, fmt.Errorf("invalid well-known community value '%s'", c) 403 } 404 if existing.Has(uint32(val)) { 405 continue 406 } 407 existing.Insert(uint32(val)) 408 res = append(res, string(c)) 409 } 410 return res, nil 411 } 412 413 func parseCommunity(communityStr string) (uint32, error) { 414 // parse as <0-65535>:<0-65535> 415 if elems := strings.Split(communityStr, ":"); len(elems) == 2 { 416 fst, err := strconv.ParseUint(elems[0], 10, 16) 417 if err != nil { 418 return 0, err 419 } 420 snd, err := strconv.ParseUint(elems[1], 10, 16) 421 if err != nil { 422 return 0, err 423 } 424 return uint32(fst<<16 | snd), nil 425 } 426 // parse as a single decimal number 427 c, err := strconv.ParseUint(communityStr, 10, 32) 428 return uint32(c), err 429 } 430 431 // dedupLargeCommunities returns deduplicated large communities as a string slice. 432 func dedupLargeCommunities(communities []v2alpha1api.BGPLargeCommunity) []string { 433 var res []string 434 existing := sets.New[string]() 435 for _, c := range communities { 436 if existing.Has(string(c)) { 437 continue 438 } 439 existing.Insert(string(c)) 440 res = append(res, string(c)) 441 } 442 return res 443 }