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  }