github.com/cilium/cilium@v1.16.2/pkg/bgpv1/manager/reconcilerv2/policies.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package reconcilerv2
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"maps"
    10  	"net/netip"
    11  	"sort"
    12  	"strconv"
    13  	"strings"
    14  
    15  	"github.com/osrg/gobgp/v3/pkg/packet/bgp"
    16  	"github.com/sirupsen/logrus"
    17  	"k8s.io/apimachinery/pkg/util/sets"
    18  
    19  	"github.com/cilium/cilium/pkg/bgpv1/types"
    20  	"github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2alpha1"
    21  	"github.com/cilium/cilium/pkg/k8s/resource"
    22  )
    23  
    24  const (
    25  	MaxPrefixLenIPv4 = 32
    26  	MaxPrefixLenIPv6 = 128
    27  )
    28  
    29  // ResourceRoutePolicyMap holds the route policies per resource.
    30  type ResourceRoutePolicyMap map[resource.Key]RoutePolicyMap
    31  
    32  // RoutePolicyMap holds routing policies configured by the policy reconciler keyed by policy name.
    33  type RoutePolicyMap map[string]*types.RoutePolicy
    34  
    35  type ReconcileRoutePoliciesParams struct {
    36  	Logger          logrus.FieldLogger
    37  	Ctx             context.Context
    38  	Router          types.Router
    39  	DesiredPolicies RoutePolicyMap
    40  	CurrentPolicies RoutePolicyMap
    41  }
    42  
    43  // ReconcileRoutePolicies reconciles routing policies between the desired and the current state.
    44  // It returns the updated routing policies and an error if the reconciliation fails.
    45  func ReconcileRoutePolicies(rp *ReconcileRoutePoliciesParams) (RoutePolicyMap, error) {
    46  	runningPolicies := make(RoutePolicyMap)
    47  	maps.Copy(runningPolicies, rp.CurrentPolicies)
    48  
    49  	var toAdd, toRemove, toUpdate []*types.RoutePolicy
    50  
    51  	for _, p := range rp.DesiredPolicies {
    52  		if existing, found := rp.CurrentPolicies[p.Name]; found {
    53  			if !existing.DeepEqual(p) {
    54  				toUpdate = append(toUpdate, p)
    55  			}
    56  		} else {
    57  			toAdd = append(toAdd, p)
    58  		}
    59  	}
    60  	for _, p := range rp.CurrentPolicies {
    61  		if _, found := rp.DesiredPolicies[p.Name]; !found {
    62  			toRemove = append(toRemove, p)
    63  		}
    64  	}
    65  
    66  	// tracks which peers have to be reset because of policy change
    67  	resetPeers := sets.New[string]()
    68  
    69  	// add missing policies
    70  	for _, p := range toAdd {
    71  		rp.Logger.WithFields(logrus.Fields{
    72  			types.PolicyLogField: p.Name,
    73  		}).Debug("Adding route policy")
    74  
    75  		err := rp.Router.AddRoutePolicy(rp.Ctx, types.RoutePolicyRequest{
    76  			DefaultExportAction: types.RoutePolicyActionReject, // do not advertise routes by default
    77  			Policy:              p,
    78  		})
    79  		if err != nil {
    80  			return runningPolicies, err
    81  		}
    82  
    83  		runningPolicies[p.Name] = p
    84  		resetPeers.Insert(peerAddressFromPolicy(p))
    85  	}
    86  
    87  	// update modified policies
    88  	for _, p := range toUpdate {
    89  		// As proper implementation of an update operation for complex policies would be quite involved,
    90  		// we resort to recreating the policies that need an update here.
    91  		rp.Logger.WithFields(logrus.Fields{
    92  			types.PolicyLogField: p.Name,
    93  		}).Debug("Updating (re-creating) route policy")
    94  
    95  		existing := rp.CurrentPolicies[p.Name]
    96  		err := rp.Router.RemoveRoutePolicy(rp.Ctx, types.RoutePolicyRequest{Policy: existing})
    97  		if err != nil {
    98  			return runningPolicies, err
    99  		}
   100  		delete(runningPolicies, existing.Name)
   101  
   102  		err = rp.Router.AddRoutePolicy(rp.Ctx, types.RoutePolicyRequest{
   103  			DefaultExportAction: types.RoutePolicyActionReject, // do not advertise routes by default
   104  			Policy:              p,
   105  		})
   106  		if err != nil {
   107  			return runningPolicies, err
   108  		}
   109  
   110  		runningPolicies[p.Name] = p
   111  		resetPeers.Insert(peerAddressFromPolicy(p))
   112  	}
   113  
   114  	// remove old policies
   115  	for _, p := range toRemove {
   116  		rp.Logger.WithFields(logrus.Fields{
   117  			types.PolicyLogField: p.Name,
   118  		}).Debug("Removing route policy")
   119  
   120  		err := rp.Router.RemoveRoutePolicy(rp.Ctx, types.RoutePolicyRequest{Policy: p})
   121  		if err != nil {
   122  			return runningPolicies, err
   123  		}
   124  		delete(runningPolicies, p.Name)
   125  		resetPeers.Insert(peerAddressFromPolicy(p))
   126  	}
   127  
   128  	// soft-reset affected BGP peers to apply the changes on already advertised routes
   129  	for peer := range resetPeers {
   130  		_, err := netip.ParsePrefix(peer)
   131  		if err != nil {
   132  			continue
   133  		}
   134  
   135  		rp.Logger.WithFields(logrus.Fields{
   136  			types.PeerLogField: peer,
   137  		}).Debug("Resetting peer due to a routing policy change")
   138  
   139  		req := types.ResetNeighborRequest{
   140  			PeerAddress:        peer,
   141  			Soft:               true,
   142  			SoftResetDirection: types.SoftResetDirectionOut, // we are using only export policies
   143  		}
   144  
   145  		err = rp.Router.ResetNeighbor(rp.Ctx, req)
   146  		if err != nil {
   147  			// non-fatal error (may happen if the neighbor is not up), just log it
   148  			rp.Logger.WithFields(logrus.Fields{
   149  				types.PeerLogField: peer,
   150  			}).WithError(err).Debug("resetting peer failed after a routing policy change")
   151  		}
   152  	}
   153  
   154  	return runningPolicies, nil
   155  }
   156  
   157  // PolicyName returns a unique route policy name for the provided peer, family and advertisement type.
   158  // If there a is a need for multiple route policies per advertisement type, unique resourceID can be provided.
   159  func PolicyName(peer, family string, advertType v2alpha1.BGPAdvertisementType, resourceID string) string {
   160  	if resourceID == "" {
   161  		return fmt.Sprintf("%s-%s-%s", peer, family, advertType)
   162  	}
   163  	return fmt.Sprintf("%s-%s-%s-%s", peer, family, advertType, resourceID)
   164  }
   165  
   166  func CreatePolicy(name string, peerAddr netip.Addr, v4Prefixes, v6Prefixes types.PolicyPrefixMatchList, advert v2alpha1.BGPAdvertisement) (*types.RoutePolicy, error) {
   167  	policy := &types.RoutePolicy{
   168  		Name: name,
   169  		Type: types.RoutePolicyTypeExport,
   170  	}
   171  
   172  	// sort prefixes to have consistent order for DeepEqual
   173  	sort.Slice(v4Prefixes, v4Prefixes.Less)
   174  	sort.Slice(v6Prefixes, v6Prefixes.Less)
   175  
   176  	// get communities
   177  	communities, largeCommunities, err := getCommunities(advert)
   178  	if err != nil {
   179  		return nil, err
   180  	}
   181  
   182  	// get local preference
   183  	var localPref *int64
   184  	if advert.Attributes != nil {
   185  		localPref = advert.Attributes.LocalPreference
   186  	}
   187  
   188  	// Due to a GoBGP limitation, we need to generate a separate statement for v4 and v6 prefixes, as families
   189  	// can not be mixed in a single statement. Nevertheless, they can be both part of the same Policy.
   190  	if len(v4Prefixes) > 0 {
   191  		policy.Statements = append(policy.Statements, policyStatement(peerAddr, v4Prefixes, localPref, communities, largeCommunities))
   192  	}
   193  	if len(v6Prefixes) > 0 {
   194  		policy.Statements = append(policy.Statements, policyStatement(peerAddr, v6Prefixes, localPref, communities, largeCommunities))
   195  	}
   196  
   197  	return policy, nil
   198  }
   199  
   200  func getCommunities(advert v2alpha1.BGPAdvertisement) (standard, large []string, err error) {
   201  	standard, err = mergeAndDedupCommunities(advert)
   202  	if err != nil {
   203  		return nil, nil, err
   204  	}
   205  	large = dedupLargeCommunities(advert)
   206  
   207  	return standard, large, nil
   208  }
   209  
   210  // mergeAndDedupCommunities merges numeric standard community and well-known community strings,
   211  // deduplicated by their actual community values.
   212  func mergeAndDedupCommunities(advert v2alpha1.BGPAdvertisement) ([]string, error) {
   213  	var res []string
   214  
   215  	if advert.Attributes == nil || advert.Attributes.Communities == nil {
   216  		return res, nil
   217  	}
   218  
   219  	standard := advert.Attributes.Communities.Standard
   220  	wellKnown := advert.Attributes.Communities.WellKnown
   221  
   222  	existing := sets.New[uint32]()
   223  	for _, c := range standard {
   224  		val, err := parseCommunity(string(c))
   225  		if err != nil {
   226  			return nil, fmt.Errorf("failed to parse standard BGP community: %w", err)
   227  		}
   228  		if existing.Has(val) {
   229  			continue
   230  		}
   231  		existing.Insert(val)
   232  		res = append(res, string(c))
   233  	}
   234  
   235  	for _, c := range wellKnown {
   236  		val, ok := bgp.WellKnownCommunityValueMap[string(c)]
   237  		if !ok {
   238  			return nil, fmt.Errorf("invalid well-known community value '%s'", c)
   239  		}
   240  		if existing.Has(uint32(val)) {
   241  			continue
   242  		}
   243  		existing.Insert(uint32(val))
   244  		res = append(res, string(c))
   245  	}
   246  	return res, nil
   247  }
   248  
   249  func parseCommunity(communityStr string) (uint32, error) {
   250  	// parse as <0-65535>:<0-65535>
   251  	if elems := strings.Split(communityStr, ":"); len(elems) == 2 {
   252  		fst, err := strconv.ParseUint(elems[0], 10, 16)
   253  		if err != nil {
   254  			return 0, err
   255  		}
   256  		snd, err := strconv.ParseUint(elems[1], 10, 16)
   257  		if err != nil {
   258  			return 0, err
   259  		}
   260  		return uint32(fst<<16 | snd), nil
   261  	}
   262  	// parse as a single decimal number
   263  	c, err := strconv.ParseUint(communityStr, 10, 32)
   264  	return uint32(c), err
   265  }
   266  
   267  // dedupLargeCommunities returns deduplicated large communities as a string slice.
   268  func dedupLargeCommunities(advert v2alpha1.BGPAdvertisement) []string {
   269  	var res []string
   270  
   271  	if advert.Attributes == nil || advert.Attributes.Communities == nil {
   272  		return res
   273  	}
   274  
   275  	communities := advert.Attributes.Communities.Large
   276  
   277  	existing := sets.New[string]()
   278  	for _, c := range communities {
   279  		if existing.Has(string(c)) {
   280  			continue
   281  		}
   282  		existing.Insert(string(c))
   283  		res = append(res, string(c))
   284  	}
   285  	return res
   286  }
   287  
   288  func policyStatement(neighborAddr netip.Addr, prefixes []*types.RoutePolicyPrefixMatch, localPref *int64, communities, largeCommunities []string) *types.RoutePolicyStatement {
   289  	// create /32 or /128 neighbor prefix match
   290  	neighborPrefix := netip.PrefixFrom(neighborAddr, neighborAddr.BitLen())
   291  
   292  	return &types.RoutePolicyStatement{
   293  		Conditions: types.RoutePolicyConditions{
   294  			MatchNeighbors: []string{neighborPrefix.String()},
   295  			MatchPrefixes:  prefixes,
   296  		},
   297  		Actions: types.RoutePolicyActions{
   298  			RouteAction:         types.RoutePolicyActionAccept,
   299  			SetLocalPreference:  localPref,
   300  			AddCommunities:      communities,
   301  			AddLargeCommunities: largeCommunities,
   302  		},
   303  	}
   304  }
   305  
   306  // peerAddressFromPolicy returns the first neighbor address found in a routing policy.
   307  func peerAddressFromPolicy(p *types.RoutePolicy) string {
   308  	if p == nil {
   309  		return ""
   310  	}
   311  	for _, s := range p.Statements {
   312  		for _, m := range s.Conditions.MatchNeighbors {
   313  			return m
   314  		}
   315  	}
   316  	return ""
   317  }