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 }