github.com/cilium/cilium@v1.16.2/pkg/datapath/linux/routing/migrate.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package linuxrouting
     5  
     6  import (
     7  	"fmt"
     8  
     9  	"github.com/vishvananda/netlink"
    10  
    11  	"github.com/cilium/cilium/pkg/datapath/linux/linux_defaults"
    12  	"github.com/cilium/cilium/pkg/revert"
    13  )
    14  
    15  // MigrateENIDatapath migrates the egress rules inside the Linux routing policy
    16  // database (RPDB) for ENI IPAM mode. It will return the number of rules that
    17  // were successfully migrated and the number of rules we've failed to migrated.
    18  // A -1 is returned for the failed number of rules if we couldn't even start
    19  // the migration.
    20  //
    21  // The compat flag will control what Cilium will do in the migration process.
    22  // If the flag is false, this instructs Cilium to ensure the datapath is newer
    23  // (or v2). If the flag is true, Cilium will ensure the original datapath (v1)
    24  // is in-place.
    25  //
    26  // Because this migration is on a best-effort basis, we ensure that each rule
    27  // (or endpoint), at the end, has either the new datapath or the original
    28  // in-place and serviceable. Otherwise we risk breaking connectivity.
    29  //
    30  // We rely on the ability to fetch the CiliumNode resource because we need to
    31  // fetch the number associated with the ENI device. The CiliumNode resource
    32  // contains this information in the Status field. This fetch is abstracted away
    33  // in the (*migrator).getter interface to avoid bringing in K8s logic to this
    34  // low-level datapath code.
    35  //
    36  // This function should be invoked before any endpoints are created.
    37  // Concretely, this function should be invoked before exposing the Cilium API
    38  // and health initialization logic because we want to ensure that no workloads
    39  // are scheduled while this modification is taking place. This migration is
    40  // related to a bug (https://github.com/cilium/cilium/issues/14336) where an
    41  // ENI has an ifindex that equals the main routing table number (253-255),
    42  // causing the rules and routes to be created using the wrong table ID, which
    43  // could end up blackholing most traffic on the node.
    44  func (m *migrator) MigrateENIDatapath(compat bool) (int, int) {
    45  	rules, err := m.rpdb.RuleList(netlink.FAMILY_V4)
    46  	if err != nil {
    47  		log.WithError(err).
    48  			Error("Failed to migrate ENI datapath due to a failure in listing the existing rules. " +
    49  				"The original datapath is still in-place, however it is recommended to retry the migration.")
    50  		return 0, -1
    51  	}
    52  
    53  	v1Rules := filterRulesByPriority(rules, linux_defaults.RulePriorityEgress)
    54  	v2Rules := filterRulesByPriority(rules, linux_defaults.RulePriorityEgressv2)
    55  
    56  	// (1) If compat=false and the current set of rules are under the older
    57  	// priority, then this is an upgrade migration.
    58  	//
    59  	// (2) If compat=false and the current set of rules are under the newer
    60  	// priority, then there is nothing to do.
    61  	//
    62  	// (3) If compat=true and the current set of rules are under the older
    63  	// priority, then there is nothing to do.
    64  	//
    65  	// (4) If compat=true and the current set of rules are under the newer
    66  	// priority, then this is a downgrade migration.
    67  
    68  	// Exit if there's nothing to do.
    69  	switch {
    70  	case !compat && len(v1Rules) == 0 && len(v2Rules) > 0: // 2
    71  		fallthrough
    72  	case compat && len(v1Rules) > 0 && len(v2Rules) == 0: // 3
    73  		return 0, 0
    74  	}
    75  
    76  	isUpgrade := !compat && len(v1Rules) > 0  // 1
    77  	isDowngrade := compat && len(v2Rules) > 0 // 4
    78  
    79  	// The following operation will be done on a per-rule basis (or
    80  	// per-endpoint, assuming that each egress rule has a unique IP addr
    81  	// associated with the endpoint).
    82  	//
    83  	// In both the upgrade and downgrade scenario, the following happens in a
    84  	// specific order to guarantee that any failure at any point won't cause
    85  	// connectivity disruption for the endpoint. Any errors encountered do not
    86  	// stop the migration process because we want to ensure that we conform to
    87  	// either the new state or the old state, and want to avoid being
    88  	// in-between datapath states.
    89  	//   1) Copy over new routes from the old routes
    90  	//   2) Insert new rule
    91  	//   3) Delete old rule
    92  	//   4) Delete old routes
    93  	// Doing (1) & (2) before (3) & (4) allows us to essentially perform an
    94  	// "atomic" swap-in for the new state.
    95  	//
    96  	// (4) is attempted separately outside the main loop because we want to
    97  	// avoid deleting routes for endpoints that share the same table ID. We
    98  	// will delete the routes, if and only if, all endpoints that share the
    99  	// same table ID succeeded in migrating. If an endpoint failed to migrate,
   100  	// then any routes that reference the table ID associated with the
   101  	// endpoint's egress rule will be skipped. This is to prevent disrupting
   102  	// endpoints who relying on the old state to be in-place.
   103  	//
   104  	// If a failure occurs at (1), then the old state can continue to service
   105  	// the endpoint. Similarly with (2) because routes without rules are likely
   106  	// to not have any effect.
   107  	//
   108  	// If a failure occurs at (3), we have already succeeded in getting the new
   109  	// state in-place to direct traffic for the endpoint. In any case of
   110  	// upgrade or downgrade, it is possible for both states to be in-place if
   111  	// there are any failures, especially if there were any failures in
   112  	// reverting. The datapath selected will depend on the rule priority.
   113  	//
   114  	// Concretely, for upgrades, the newer rule will have a lower priority, so
   115  	// the original datapath will be selected. The migration is deemed a
   116  	// failure because the original datapath (with a rule that has a higher
   117  	// priority) is being selected for the endpoint. It is necessary to attempt
   118  	// reverting the failed migration work [(1) & (2)], as leaving the state
   119  	// could block a user's from retrying this upgrade again.
   120  	//
   121  	// For downgrades, the newer rule will have a higher priority, so the newer
   122  	// datapath will be selected. The migration is deemed a success and we
   123  	// explicitly avoid reverting, because it's not necessary to revert this
   124  	// work merely because we failed to cleanup old, ineffectual state.
   125  	//
   126  	// In either case, no connectivity is affected for the endpoint.
   127  	//
   128  	// If we fail at (4), then the old rule will have been deleted and the new
   129  	// state is in-place, which would be servicing the endpoint. The old routes
   130  	// would just be leftover state to be cleaned up at a later point.
   131  	//
   132  	// It is also important to note that we only revert what we've done on a
   133  	// per-rule basis if we fail at (2) or (3). This is by design because we
   134  	// want to ensure that each iteration of the loop is atomic to each
   135  	// endpoint. Meaning, either the endpoint ends up with the new datapath or
   136  	// the original.
   137  
   138  	var (
   139  		// Number of rules (endpoints) successfully migrated and how many failed.
   140  		migrated, failed int
   141  		// Store the routes to cleanup in a set after successful migration
   142  		// because routes are only unique per table ID, meaning many endpoints
   143  		// share the same route table if the endpoint's IP is allocated from
   144  		// the same ENI device.
   145  		cleanup = make(map[netlink.Rule][]netlink.Route)
   146  		// Store the table IDs of the routes whose migration failed. This is
   147  		// important because to prevent deleting routes for endpoints that
   148  		// share the same table ID. An example: let's say we have 2 endpoints
   149  		// that have rules and routes that refer to the same table ID. If 1
   150  		// endpoint fails the migration and the other succeeded, we must not
   151  		// remove the routes for the endpoint that failed because it's still
   152  		// relying on them for connectivity.
   153  		failedTableIDs = make(map[int]struct{})
   154  	)
   155  
   156  	if isUpgrade {
   157  		for _, r := range v1Rules {
   158  			if routes, err := m.upgradeRule(r); err != nil {
   159  				log.WithError(err).WithField("rule", r).Warn("Failed to migrate endpoint to new ENI datapath. " +
   160  					"Previous datapath is still intact and endpoint connectivity is not affected.")
   161  				failedTableIDs[r.Table] = struct{}{}
   162  				failed++
   163  			} else {
   164  				if rs, found := cleanup[r]; found {
   165  					rs = append(rs, routes...)
   166  					cleanup[r] = rs
   167  				} else {
   168  					cleanup[r] = routes
   169  				}
   170  				migrated++
   171  			}
   172  		}
   173  	} else if isDowngrade {
   174  		for _, r := range v2Rules {
   175  			if routes, err := m.downgradeRule(r); err != nil {
   176  				log.WithError(err).WithField("rule", r).Warn("Failed to downgrade endpoint to original ENI datapath. " +
   177  					"Previous datapath is still intact and endpoint connectivity is not affected.")
   178  				failedTableIDs[r.Table] = struct{}{}
   179  				failed++
   180  			} else {
   181  				if rs, found := cleanup[r]; found {
   182  					rs = append(rs, routes...)
   183  					cleanup[r] = rs
   184  				} else {
   185  					cleanup[r] = routes
   186  				}
   187  				migrated++
   188  			}
   189  		}
   190  	}
   191  
   192  	// We store the routes that have already been deleted to de-duplicate and
   193  	// avoid netlink returning "no such process" for a route that has already
   194  	// been deleted. Note the map key is a string representation of a
   195  	// netlink.Route because netlink.Route is not a valid map key because it is
   196  	// incomparable due to containing a slice inside it.
   197  	deleted := make(map[string]struct{}, len(cleanup))
   198  
   199  	for rule, routes := range cleanup {
   200  		toDelete := make([]netlink.Route, 0, len(routes))
   201  		for _, ro := range routes {
   202  			if _, skip := failedTableIDs[rule.Table]; skip {
   203  				continue
   204  			}
   205  
   206  			if _, already := deleted[ro.String()]; !already {
   207  				// Declare the routes deleted here before the actual deletion
   208  				// below because we don't care if deletion succeeds or not. See
   209  				// comment below on why.
   210  				deleted[ro.String()] = struct{}{}
   211  				toDelete = append(toDelete, ro)
   212  			}
   213  		}
   214  
   215  		// This function does not return a revert stack unlike the others
   216  		// because this operation is best-effort. If we fail to delete old
   217  		// routes, then it simply means there is just leftover state left
   218  		// behind, but it has no impact on the datapath whatsoever. We can make
   219  		// that assumption because by the time we call this function, we'd have
   220  		// successfully deleted the old rule which would steer traffic towards
   221  		// these routes.
   222  		//
   223  		// We also don't want to revert here because at this point, the new
   224  		// datapath is in-place and it wouldn't make sense to risk reverting in
   225  		// case of a failure, just to merely cleanup the previous state. We'll
   226  		// live with the leftover state, however the user should be advised to
   227  		// eventually clean this up.
   228  		if err := m.deleteOldRoutes(toDelete); err != nil {
   229  			version := "new"
   230  			if rule.Priority == linux_defaults.RulePriorityEgressv2 {
   231  				version = "original"
   232  			}
   233  
   234  			scopedLog := log.WithField("rule", rule)
   235  			scopedLog.WithError(err).WithField("routes", routes).
   236  				Warnf("Failed to cleanup after successfully migrating endpoint to %s ENI datapath. "+
   237  					"It is recommended that these routes are cleaned up (by running `ip route del`), as it is possible in the future "+
   238  					"to collide with another endpoint with the same IP.", version)
   239  		}
   240  	}
   241  
   242  	return migrated, failed
   243  }
   244  
   245  // NewMigrator constructs a migrator object with the default implementation to
   246  // use the underlying upstream netlink library to manipulate the Linux RPDB.
   247  // It accepts a getter for retrieving the interface number by MAC address and
   248  // vice versa.
   249  func NewMigrator(getter interfaceDB) *migrator {
   250  	return &migrator{
   251  		rpdb:   defaultRPDB{},
   252  		getter: getter,
   253  	}
   254  }
   255  
   256  // upgradeRule migrates the given rule (and endpoint) to the new ENI datapath,
   257  // using the new table ID scheme derived from the ENI interface number. It
   258  // returns the old routes that the caller should remove at a later time, along
   259  // with an error.
   260  func (m *migrator) upgradeRule(rule netlink.Rule) ([]netlink.Route, error) {
   261  	// Let's say we have an ENI device attached to the node with ifindex 3 and
   262  	// interface number 2. The following rule will exist on the node _before_
   263  	// migration.
   264  	//   110:    from 192.168.11.171 to 192.168.0.0/16 lookup 3
   265  	// After the migration, this rule will become:
   266  	//   111:    from 192.168.11.171 to 192.168.0.0/16 lookup 12
   267  	// The priority has been updated to 111 and the table ID is 12 because the
   268  	// interface number is 2 plus the routing table offset
   269  	// (linux_defaults.RouteTableInterfacesOffset). See copyRoutes() for what
   270  	// happens with routes.
   271  
   272  	scopedLog := log.WithField("rule", rule)
   273  
   274  	routes, err := m.rpdb.RouteListFiltered(netlink.FAMILY_V4, &netlink.Route{
   275  		Table: rule.Table,
   276  	}, netlink.RT_FILTER_TABLE)
   277  	if err != nil {
   278  		return nil, fmt.Errorf("failed to list routes associated with rule: %w", err)
   279  	}
   280  
   281  	// If there are no routes under the same table as the rule, then
   282  	// skip.
   283  	if len(routes) == 0 {
   284  		scopedLog.Debug("Skipping migration of egress rule due to no routes found")
   285  		return nil, nil
   286  	}
   287  
   288  	// It is sufficient to grab the first route that matches because we
   289  	// are assuming all routes created under a rule will have the same
   290  	// ifindex (LinkIndex).
   291  	ifindex := routes[0].LinkIndex
   292  	newTable, err := m.retrieveTableIDFromIfIndex(ifindex)
   293  	if err != nil {
   294  		return nil, fmt.Errorf("failed to retrieve new table ID from ifindex %q: %w",
   295  			ifindex, err)
   296  	}
   297  
   298  	var (
   299  		stack revert.RevertStack
   300  
   301  		oldTable = rule.Table
   302  	)
   303  
   304  	revert, err := m.copyRoutes(routes, oldTable, newTable)
   305  	stack.Extend(revert)
   306  	if err != nil {
   307  		return nil, fmt.Errorf("failed to create new routes: %w", err)
   308  	}
   309  
   310  	revert, err = m.createNewRule(
   311  		rule,
   312  		linux_defaults.RulePriorityEgressv2,
   313  		newTable,
   314  	)
   315  	stack.Extend(revert)
   316  	if err != nil {
   317  		// We revert here because we want to ensure that the new routes
   318  		// are removed as they'd have no effect, but may conflict with
   319  		// others in the future.
   320  		if revErr := stack.Revert(); revErr != nil {
   321  			scopedLog.WithError(err).WithField("revertError", revErr).Warn(upgradeRevertWarning)
   322  		}
   323  
   324  		return nil, fmt.Errorf("failed to create new rule: %w", err)
   325  	}
   326  
   327  	if err := m.rpdb.RuleDel(&rule); err != nil {
   328  		// We revert here because we want to ensure that the new state that we
   329  		// just created above is reverted. See long comment describing the
   330  		// migration in MigrateENIDatapath().
   331  		if revErr := stack.Revert(); revErr != nil {
   332  			scopedLog.WithError(err).WithField("revertError", revErr).Warn(upgradeRevertWarning)
   333  		}
   334  
   335  		return nil, fmt.Errorf("failed to delete old rule: %w", err)
   336  	}
   337  
   338  	return routes, nil
   339  }
   340  
   341  // downgradeRule migrates the given rule (and endpoint) to the original ENI
   342  // datapath, using the old table ID scheme that was simply the ifindex of the
   343  // attached ENI device on the node. It returns the "old" routes (new datapath)
   344  // that the caller should remove at a later time, along with an error.
   345  func (m *migrator) downgradeRule(rule netlink.Rule) ([]netlink.Route, error) {
   346  	// Let's say we have an ENI device attached to the node with ifindex 9 and
   347  	// interface number 3. The following rule will exist on the node _before_
   348  	// migration.
   349  	//   111:    from 192.168.11.171 to 192.168.0.0/16 lookup 13
   350  	// After the migration, this rule will become:
   351  	//   110:    from 192.168.11.171 to 192.168.0.0/16 lookup 9
   352  	// The priority has been reverted back to 110 and the table ID back to 9
   353  	// because the ifindex is 9. See copyRoutes() for what happens with routes.
   354  
   355  	scopedLog := log.WithField("rule", rule)
   356  
   357  	oldTable := rule.Table
   358  	ifaceNumber := oldTable - linux_defaults.RouteTableInterfacesOffset
   359  
   360  	newTable, err := m.retrieveTableIDFromInterfaceNumber(ifaceNumber)
   361  	if err != nil {
   362  		return nil, fmt.Errorf("failed to retrieve new table ID from interface-number %q: %w",
   363  			ifaceNumber, err)
   364  	}
   365  
   366  	routes, err := m.rpdb.RouteListFiltered(netlink.FAMILY_V4, &netlink.Route{
   367  		Table: oldTable,
   368  	}, netlink.RT_FILTER_TABLE)
   369  	if err != nil {
   370  		return nil, fmt.Errorf("failed to list routes associated with rule: %w", err)
   371  	}
   372  
   373  	var stack revert.RevertStack
   374  
   375  	revert, err := m.copyRoutes(routes, oldTable, newTable)
   376  	stack.Extend(revert)
   377  	if err != nil {
   378  		return nil, fmt.Errorf("failed to create new routes: %w", err)
   379  	}
   380  
   381  	// We don't need the revert stack return value because the next operation
   382  	// to delete the rule will not revert the stack. See below comment on why.
   383  	_, err = m.createNewRule(
   384  		rule,
   385  		linux_defaults.RulePriorityEgress,
   386  		newTable,
   387  	)
   388  	if err != nil {
   389  		if revErr := stack.Revert(); revErr != nil {
   390  			scopedLog.WithError(err).WithField("revertError", revErr).Warn(downgradeRevertWarning)
   391  		}
   392  
   393  		return nil, fmt.Errorf("failed to create new rule: %w", err)
   394  	}
   395  
   396  	if err := m.rpdb.RuleDel(&rule); err != nil {
   397  		// We avoid reverting and returning an error here because the newer
   398  		// datapath is already in-place. See long comment describing the
   399  		// migration in MigrateENIDatapath().
   400  		scopedLog.WithError(err).Warn(downgradeFailedRuleDeleteWarning)
   401  		return nil, nil
   402  	}
   403  
   404  	return routes, nil
   405  }
   406  
   407  const (
   408  	upgradeRevertWarning = "Reverting the new ENI datapath failed. However, the previous datapath is still intact. " +
   409  		"Endpoint connectivity should not be affected. It is advised to retry the migration."
   410  	downgradeRevertWarning = "Reverting the new ENI datapath failed. However, both the new and previous datapaths are still intact. " +
   411  		"Endpoint connectivity should not be affected. It is advised to retry the migration."
   412  	downgradeFailedRuleDeleteWarning = "Downgrading the datapath has succeeded, but failed to cleanup the original datapath. " +
   413  		"It is advised to manually remove the old rule (priority 110)."
   414  )
   415  
   416  // retrieveTableIDFromIfIndex computes the correct table ID based on the
   417  // ifindex provided. The table ID is comprised of the number associated with an
   418  // ENI device that corresponds to the ifindex, plus the specific table offset
   419  // value.
   420  func (m *migrator) retrieveTableIDFromIfIndex(ifindex int) (int, error) {
   421  	link, err := m.rpdb.LinkByIndex(ifindex)
   422  	if err != nil {
   423  		return -1, fmt.Errorf("failed to find link by index: %w", err)
   424  	}
   425  
   426  	mac := link.Attrs().HardwareAddr.String()
   427  	ifaceNum, err := m.getter.GetInterfaceNumberByMAC(mac)
   428  	if err != nil {
   429  		return -1, fmt.Errorf("failed to get interface-number by MAC %q: %w", mac, err)
   430  	}
   431  
   432  	// This is guaranteed to avoid conflicting with the main routing table ID
   433  	// (253-255) because the maximum number of ENI devices on a node is 15 (see
   434  	// pkg/aws/eni/limits.go). Because the interface number is monotonically
   435  	// increasing and the lowest available number is reused when devices are
   436  	// added / removed. This means that the max possible table ID is 25.
   437  	return linux_defaults.RouteTableInterfacesOffset + ifaceNum, nil
   438  }
   439  
   440  // retrieveTableIDFromInterfaceNumber returns the table ID based on the
   441  // interface number. The table ID is the ifindex of the device corresponding to
   442  // the ENI with the given interface number. This is used for downgrading /
   443  // using the old ENI datapath.
   444  func (m *migrator) retrieveTableIDFromInterfaceNumber(ifaceNum int) (int, error) {
   445  	mac, err := m.getter.GetMACByInterfaceNumber(ifaceNum)
   446  	if err != nil {
   447  		return -1, fmt.Errorf("failed to get interface-number by MAC %q: %w", mac, err)
   448  	}
   449  
   450  	links, err := m.rpdb.LinkList()
   451  	if err != nil {
   452  		return -1, fmt.Errorf("failed to list links: %w", err)
   453  	}
   454  
   455  	var (
   456  		link  netlink.Link
   457  		found bool
   458  	)
   459  	for _, l := range links {
   460  		if l.Attrs().HardwareAddr.String() == mac {
   461  			link = l
   462  			found = true
   463  			break
   464  		}
   465  	}
   466  
   467  	if !found {
   468  		return -1, fmt.Errorf("could not find link with MAC %q by interface-number %q", mac, ifaceNum)
   469  	}
   470  
   471  	return link.Attrs().Index, nil
   472  }
   473  
   474  // copyRoutes upserts `routes` under the `from` table ID to `to` table ID. It
   475  // returns a RevertStack and an error. The RevertStack contains functions that
   476  // would revert all the successful operations that occurred in this function.
   477  // The caller of this function MUST revert the stack when this function returns
   478  // an error.
   479  func (m *migrator) copyRoutes(routes []netlink.Route, from, to int) (revert.RevertStack, error) {
   480  	var revertStack revert.RevertStack
   481  
   482  	// In ENI mode, we only expect two rules:
   483  	//   1) Link scoped route with a gateway IP
   484  	//   2) Default route via gateway IP
   485  	// We need to add the link-local scope route to the gateway first, then
   486  	// routes that depend on that as a next-hop later. If we didn't do this,
   487  	// then the kernel would complain with "Error: Nexthop has invalid
   488  	// gateway." with an errno of ENETUNREACH.
   489  	for _, r := range routes {
   490  		if r.Scope == netlink.SCOPE_LINK {
   491  			r.Table = to
   492  			if err := m.rpdb.RouteReplace(&r); err != nil {
   493  				return revertStack, fmt.Errorf("unable to replace link scoped route under table ID: %w", err)
   494  			}
   495  
   496  			revertStack.Push(func() error {
   497  				if err := m.rpdb.RouteDel(&r); err != nil {
   498  					return fmt.Errorf("failed to revert route upsert: %w", err)
   499  				}
   500  				return nil
   501  			})
   502  		}
   503  	}
   504  
   505  	for _, r := range routes {
   506  		if r.Scope == netlink.SCOPE_LINK {
   507  			// Skip over these because we already upserted it above.
   508  			continue
   509  		}
   510  
   511  		r.Table = to
   512  		if err := m.rpdb.RouteReplace(&r); err != nil {
   513  			return revertStack, fmt.Errorf("unable to replace route under table ID: %w", err)
   514  		}
   515  
   516  		revertStack.Push(func() error {
   517  			if err := m.rpdb.RouteDel(&r); err != nil {
   518  				return fmt.Errorf("failed to revert route upsert: %w", err)
   519  			}
   520  			return nil
   521  		})
   522  	}
   523  
   524  	return revertStack, nil
   525  }
   526  
   527  // createNewRule inserts `rule` with the table ID of `newTable` and a priority
   528  // of `toPrio`. It returns a RevertStack and an error. The RevertStack contains
   529  // functions that would revert all the successful operations that occurred in
   530  // this function. The caller of this function MUST revert the stack when this
   531  // function returns an error.
   532  func (m *migrator) createNewRule(rule netlink.Rule, toPrio, newTable int) (revert.RevertStack, error) {
   533  	var revertStack revert.RevertStack
   534  
   535  	r := rule
   536  	r.Priority = toPrio
   537  	r.Table = newTable
   538  	if err := m.rpdb.RuleAdd(&r); err != nil {
   539  		return revertStack, fmt.Errorf("unable to add new rule: %w", err)
   540  	}
   541  
   542  	revertStack.Push(func() error {
   543  		if err := m.rpdb.RuleDel(&r); err != nil {
   544  			return fmt.Errorf("failed to revert rule insert: %w", err)
   545  		}
   546  		return nil
   547  	})
   548  
   549  	return revertStack, nil
   550  }
   551  
   552  func (m *migrator) deleteOldRoutes(routes []netlink.Route) error {
   553  	for _, r := range routes {
   554  		if err := m.rpdb.RouteDel(&r); err != nil {
   555  			return fmt.Errorf("unable to delete old route: %w", err)
   556  		}
   557  	}
   558  
   559  	return nil
   560  }
   561  
   562  func filterRulesByPriority(rules []netlink.Rule, prio int) []netlink.Rule {
   563  	candidates := make([]netlink.Rule, 0, len(rules))
   564  	for _, r := range rules {
   565  		if r.Priority == prio {
   566  			candidates = append(candidates, r)
   567  		}
   568  	}
   569  
   570  	return candidates
   571  }
   572  
   573  type migrator struct {
   574  	rpdb   rpdb
   575  	getter interfaceDB
   576  }
   577  
   578  // defaultRPDB is a simple, default implementation of the rpdb interface which
   579  // forwards all RPDB operations to netlink.
   580  type defaultRPDB struct{}
   581  
   582  func (defaultRPDB) RuleList(family int) ([]netlink.Rule, error) { return netlink.RuleList(family) }
   583  func (defaultRPDB) RuleAdd(rule *netlink.Rule) error            { return netlink.RuleAdd(rule) }
   584  func (defaultRPDB) RuleDel(rule *netlink.Rule) error            { return netlink.RuleDel(rule) }
   585  func (defaultRPDB) RouteListFiltered(family int, filter *netlink.Route, mask uint64) ([]netlink.Route, error) {
   586  	return netlink.RouteListFiltered(family, filter, mask)
   587  }
   588  func (defaultRPDB) RouteAdd(route *netlink.Route) error     { return netlink.RouteAdd(route) }
   589  func (defaultRPDB) RouteDel(route *netlink.Route) error     { return netlink.RouteDel(route) }
   590  func (defaultRPDB) RouteReplace(route *netlink.Route) error { return netlink.RouteReplace(route) }
   591  func (defaultRPDB) LinkList() ([]netlink.Link, error)       { return netlink.LinkList() }
   592  func (defaultRPDB) LinkByIndex(ifindex int) (netlink.Link, error) {
   593  	return netlink.LinkByIndex(ifindex)
   594  }
   595  
   596  // rpdb abstracts the underlying Linux RPDB operations. This is an interface
   597  // mostly for testing purposes.
   598  type rpdb interface {
   599  	RuleList(int) ([]netlink.Rule, error)
   600  	RuleAdd(*netlink.Rule) error
   601  	RuleDel(*netlink.Rule) error
   602  
   603  	RouteListFiltered(int, *netlink.Route, uint64) ([]netlink.Route, error)
   604  	RouteAdd(*netlink.Route) error
   605  	RouteDel(*netlink.Route) error
   606  	RouteReplace(*netlink.Route) error
   607  
   608  	LinkList() ([]netlink.Link, error)
   609  	LinkByIndex(int) (netlink.Link, error)
   610  }
   611  
   612  type interfaceDB interface {
   613  	GetInterfaceNumberByMAC(mac string) (int, error)
   614  	GetMACByInterfaceNumber(ifaceNum int) (string, error)
   615  }