// Copyright (C) 2014 Nippon Telegraph and Telephone Corporation. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or // implied. // See the License for the specific language governing permissions and // limitations under the License. package table import ( "bytes" "encoding/binary" "fmt" log "github.com/Sirupsen/logrus" api "github.com/osrg/gobgp/api" "github.com/osrg/gobgp/config" "github.com/osrg/gobgp/packet/bgp" "net" "sort" ) var SelectionOptions config.RouteSelectionOptionsConfig var UseMultiplePaths config.UseMultiplePathsConfig type BestPathReason string const ( BPR_UNKNOWN BestPathReason = "Unknown" BPR_ONLY_PATH BestPathReason = "Only Path" BPR_REACHABLE_NEXT_HOP BestPathReason = "Reachable Next Hop" BPR_HIGHEST_WEIGHT BestPathReason = "Highest Weight" BPR_LOCAL_PREF BestPathReason = "Local Pref" BPR_LOCAL_ORIGIN BestPathReason = "Local Origin" BPR_ASPATH BestPathReason = "AS Path" BPR_ORIGIN BestPathReason = "Origin" BPR_MED BestPathReason = "MED" BPR_ASN BestPathReason = "ASN" BPR_IGP_COST BestPathReason = "IGP Cost" BPR_ROUTER_ID BestPathReason = "Router ID" BPR_OLDER BestPathReason = "Older" ) func IpToRadixkey(b []byte, max uint8) string { var buffer bytes.Buffer for i := 0; i < len(b) && i < int(max); i++ { buffer.WriteString(fmt.Sprintf("%08b", b[i])) } return buffer.String()[:max] } func CidrToRadixkey(cidr string) string { _, n, _ := net.ParseCIDR(cidr) ones, _ := n.Mask.Size() return IpToRadixkey(n.IP, uint8(ones)) } type PeerInfo struct { AS uint32 ID net.IP LocalAS uint32 LocalID net.IP Address net.IP LocalAddress net.IP RouteReflectorClient bool RouteReflectorClusterID net.IP } func (lhs *PeerInfo) Equal(rhs *PeerInfo) bool { if lhs == rhs { return true } if rhs == nil { return false } if (lhs.AS == rhs.AS) && lhs.ID.Equal(rhs.ID) && lhs.LocalID.Equal(rhs.LocalID) && lhs.Address.Equal(rhs.Address) { return true } return false } func (i *PeerInfo) String() string { if i.Address == nil { return "local" } s := bytes.NewBuffer(make([]byte, 0, 64)) s.WriteString(fmt.Sprintf("{ %s | ", i.Address)) s.WriteString(fmt.Sprintf("as: %d", i.AS)) s.WriteString(fmt.Sprintf(", id: %s", i.ID)) if i.RouteReflectorClient { s.WriteString(fmt.Sprintf(", cluster-id: %s", i.RouteReflectorClusterID)) } s.WriteString(" }") return s.String() } func NewPeerInfo(g *config.Global, p *config.Neighbor) *PeerInfo { id := net.ParseIP(string(p.RouteReflector.Config.RouteReflectorClusterId)).To4() return &PeerInfo{ AS: p.Config.PeerAs, LocalAS: g.Config.As, LocalID: net.ParseIP(g.Config.RouterId).To4(), Address: net.ParseIP(p.Config.NeighborAddress), RouteReflectorClient: p.RouteReflector.Config.RouteReflectorClient, RouteReflectorClusterID: id, } } type Destination struct { routeFamily bgp.RouteFamily nlri bgp.AddrPrefixInterface knownPathList paths withdrawList paths newPathList paths RadixKey string } func NewDestination(nlri bgp.AddrPrefixInterface) *Destination { d := &Destination{ routeFamily: bgp.AfiSafiToRouteFamily(nlri.AFI(), nlri.SAFI()), nlri: nlri, knownPathList: make([]*Path, 0), withdrawList: make([]*Path, 0), newPathList: make([]*Path, 0), } switch d.routeFamily { case bgp.RF_IPv4_UC, bgp.RF_IPv6_UC: d.RadixKey = CidrToRadixkey(nlri.String()) } return d } func (dd *Destination) ToApiStruct(id string) *api.Destination { prefix := dd.GetNlri().String() paths := func(arg []*Path) []*api.Path { ret := make([]*api.Path, 0, len(arg)) first := true for _, p := range arg { if p.Filtered(id) == POLICY_DIRECTION_NONE { pp := p.ToApiStruct(id) if first { pp.Best = true first = false } ret = append(ret, pp) } } return ret }(dd.knownPathList) if len(paths) == 0 { return nil } return &api.Destination{ Prefix: prefix, Paths: paths, } } func (dd *Destination) Family() bgp.RouteFamily { return dd.routeFamily } func (dd *Destination) setRouteFamily(routeFamily bgp.RouteFamily) { dd.routeFamily = routeFamily } func (dd *Destination) GetNlri() bgp.AddrPrefixInterface { return dd.nlri } func (dd *Destination) setNlri(nlri bgp.AddrPrefixInterface) { dd.nlri = nlri } func (dd *Destination) GetAllKnownPathList() []*Path { return dd.knownPathList } func (dd *Destination) GetKnownPathList(id string) []*Path { list := make([]*Path, 0, len(dd.knownPathList)) for _, p := range dd.knownPathList { if p.Filtered(id) == POLICY_DIRECTION_NONE { list = append(list, p) } } return list } func (dd *Destination) GetBestPath(id string) *Path { for _, p := range dd.knownPathList { if p.Filtered(id) == POLICY_DIRECTION_NONE { return p } } return nil } func (dd *Destination) addWithdraw(withdraw *Path) { dd.validatePath(withdraw) dd.withdrawList = append(dd.withdrawList, withdraw) } func (dd *Destination) addNewPath(newPath *Path) { dd.validatePath(newPath) dd.newPathList = append(dd.newPathList, newPath) } func (dd *Destination) validatePath(path *Path) { if path == nil || path.GetRouteFamily() != dd.routeFamily { log.WithFields(log.Fields{ "Topic": "Table", "Key": dd.GetNlri().String(), "Path": path, "ExpectedRF": dd.routeFamily, }).Error("path is nil or invalid route family") } } // Calculates best-path among known paths for this destination. // // Modifies destination's state related to stored paths. Removes withdrawn // paths from known paths. Also, adds new paths to known paths. func (dest *Destination) Calculate(ids []string) (map[string]*Path, []*Path, []*Path) { best := make(map[string]*Path, len(ids)) oldKnownPathList := dest.knownPathList // First remove the withdrawn paths. withdrawnList := dest.explicitWithdraw() // Do implicit withdrawal dest.implicitWithdraw() // Collect all new paths into known paths. dest.knownPathList = append(dest.knownPathList, dest.newPathList...) // Clear new paths as we copied them. dest.newPathList = make([]*Path, 0) // Compute new best path dest.computeKnownBestPath() f := func(id string) *Path { old := func() *Path { for _, p := range oldKnownPathList { if p.Filtered(id) == POLICY_DIRECTION_NONE { return p } } return nil }() best := dest.GetBestPath(id) if best != nil && best.Equal(old) { // RFC4684 3.2. Intra-AS VPN Route Distribution // When processing RT membership NLRIs received from internal iBGP // peers, it is necessary to consider all available iBGP paths for a // given RT prefix, for building the outbound route filter, and not just // the best path. if best.GetRouteFamily() == bgp.RF_RTC_UC { return best } return nil } if best == nil { if old == nil { return nil } return old.Clone(true) } return best } var multi []*Path for _, id := range ids { best[id] = f(id) if id == GLOBAL_RIB_NAME && UseMultiplePaths.Enabled { multipath := func(paths []*Path) []*Path { mp := make([]*Path, 0, len(paths)) var best *Path for _, path := range paths { if path.Filtered(id) == POLICY_DIRECTION_NONE { if best == nil { best = path mp = append(mp, path) } else if best.Compare(path) == 0 { mp = append(mp, path) } } } return mp } diff := func(lhs, rhs []*Path) bool { if len(lhs) != len(rhs) { return true } for idx, l := range lhs { if !l.Equal(rhs[idx]) { return true } } return false } oldM := multipath(oldKnownPathList) newM := multipath(dest.knownPathList) if diff(oldM, newM) { multi = newM if len(newM) == 0 { multi = []*Path{best[id]} } } } } return best, withdrawnList, multi } // Removes withdrawn paths. // // Note: // We may have disproportionate number of withdraws compared to know paths // since not all paths get installed into the table due to bgp policy and // we can receive withdraws for such paths and withdrawals may not be // stopped by the same policies. // func (dest *Destination) explicitWithdraw() paths { // If we have no withdrawals, we have nothing to do. if len(dest.withdrawList) == 0 { return nil } log.WithFields(log.Fields{ "Topic": "Table", "Key": dest.GetNlri().String(), "Length": len(dest.withdrawList), }).Debug("Removing withdrawals") // If we have some withdrawals and no know-paths, it means it is safe to // delete these withdraws. if len(dest.knownPathList) == 0 { log.WithFields(log.Fields{ "Topic": "Table", "Key": dest.GetNlri().String(), "Length": len(dest.withdrawList), }).Debug("Found withdrawals for path(s) that did not get installed") dest.withdrawList = []*Path{} return nil } // If we have some known paths and some withdrawals, we find matches and // delete them first. matches := make([]*Path, 0, len(dest.withdrawList)/2) newKnownPaths := make([]*Path, 0, len(dest.knownPathList)/2) // Match all withdrawals from destination paths. for _, withdraw := range dest.withdrawList { isFound := false for _, path := range dest.knownPathList { // We have a match if the source are same. if path.GetSource().Equal(withdraw.GetSource()) { isFound = true // this path is referenced in peer's adj-rib-in // when there was no policy modification applied. // we sould flag IsWithdraw down after use to avoid // a path with IsWithdraw flag exists in adj-rib-in path.IsWithdraw = true matches = append(matches, withdraw) } } // We do no have any match for this withdraw. if !isFound { log.WithFields(log.Fields{ "Topic": "Table", "Key": dest.GetNlri().String(), "Path": withdraw, }).Warn("No matching path for withdraw found, may be path was not installed into table") } } for _, path := range dest.knownPathList { if !path.IsWithdraw { newKnownPaths = append(newKnownPaths, path) } // here we flag IsWithdraw down path.IsWithdraw = false } dest.knownPathList = newKnownPaths dest.withdrawList = make([]*Path, 0) return matches } // Identifies which of known paths are old and removes them. // // Known paths will no longer have paths whose new version is present in // new paths. func (dest *Destination) implicitWithdraw() paths { newKnownPaths := make([]*Path, 0, len(dest.knownPathList)) implicitWithdrawn := make([]*Path, 0, len(dest.knownPathList)) for _, path := range dest.knownPathList { found := false for _, newPath := range dest.newPathList { if newPath.NoImplicitWithdraw() { continue } // Here we just check if source is same and not check if path // version num. as newPaths are implicit withdrawal of old // paths and when doing RouteRefresh (not EnhancedRouteRefresh) // we get same paths again. if newPath.GetSource().Equal(path.GetSource()) { log.WithFields(log.Fields{ "Topic": "Table", "Key": dest.GetNlri().String(), "Path": path, }).Debug("Implicit withdrawal of old path, since we have learned new path from the same peer") found = true break } } if found { implicitWithdrawn = append(implicitWithdrawn, path) } else { newKnownPaths = append(newKnownPaths, path) } } dest.knownPathList = newKnownPaths return implicitWithdrawn } func (dest *Destination) computeKnownBestPath() (*Path, BestPathReason, error) { // If we do not have any paths to this destination, then we do not have // new best path. if len(dest.knownPathList) == 0 { return nil, BPR_UNKNOWN, nil } log.Debugf("computeKnownBestPath known pathlist: %d", len(dest.knownPathList)) // We pick the first path as current best path. This helps in breaking // tie between two new paths learned in one cycle for which best-path // calculation steps lead to tie. if len(dest.knownPathList) == 1 { return dest.knownPathList[0], BPR_ONLY_PATH, nil } sort.Sort(dest.knownPathList) newBest := dest.knownPathList[0] return newBest, newBest.reason, nil } type paths []*Path func (p paths) Len() int { return len(p) } func (p paths) Swap(i, j int) { p[i], p[j] = p[j], p[i] } func (p paths) Less(i, j int) bool { //Compares given paths and returns best path. // //Parameters: // -`path1`: first path to compare // -`path2`: second path to compare // // Best path processing will involve following steps: // 1. Select a path with a reachable next hop. // 2. Select the path with the highest weight. // 3. If path weights are the same, select the path with the highest // local preference value. // 4. Prefer locally originated routes (network routes, redistributed // routes, or aggregated routes) over received routes. // 5. Select the route with the shortest AS-path length. // 6. If all paths have the same AS-path length, select the path based // on origin: IGP is preferred over EGP; EGP is preferred over // Incomplete. // 7. If the origins are the same, select the path with lowest MED // value. // 8. If the paths have the same MED values, select the path learned // via EBGP over one learned via IBGP. // 9. Select the route with the lowest IGP cost to the next hop. // 10. Select the route received from the peer with the lowest BGP // router ID. // // Returns None if best-path among given paths cannot be computed else best // path. // Assumes paths from NC has source equal to None. // path1 := p[i] path2 := p[j] var better *Path reason := BPR_UNKNOWN // Follow best path calculation algorithm steps. // compare by reachability if better == nil { better = compareByReachableNexthop(path1, path2) reason = BPR_REACHABLE_NEXT_HOP } if better == nil { better = compareByHighestWeight(path1, path2) reason = BPR_HIGHEST_WEIGHT } if better == nil { better = compareByLocalPref(path1, path2) reason = BPR_LOCAL_PREF } if better == nil { better = compareByLocalOrigin(path1, path2) reason = BPR_LOCAL_ORIGIN } if better == nil { better = compareByASPath(path1, path2) reason = BPR_ASPATH } if better == nil { better = compareByOrigin(path1, path2) reason = BPR_ORIGIN } if better == nil { better = compareByMED(path1, path2) reason = BPR_MED } if better == nil { better = compareByASNumber(path1, path2) reason = BPR_ASN } if better == nil { better = compareByIGPCost(path1, path2) reason = BPR_IGP_COST } if better == nil { better = compareByAge(path1, path2) reason = BPR_OLDER } if better == nil { var e error = nil better, e = compareByRouterID(path1, path2) if e != nil { log.Error(e) } reason = BPR_ROUTER_ID } if better == nil { reason = BPR_UNKNOWN better = path1 } better.reason = reason if better == path1 { return true } return false } func compareByReachableNexthop(path1, path2 *Path) *Path { // Compares given paths and selects best path based on reachable next-hop. // // If no path matches this criteria, return None. // However RouteServer doesn't need to check reachability, so return nil. log.Debugf("enter compareByReachableNexthop -- path1: %s, path2: %s", path1, path2) return nil } func compareByHighestWeight(path1, path2 *Path) *Path { // Selects a path with highest weight. // // Weight is BGPS specific parameter. It is local to the router on which it // is configured. // Return: // nil if best path among given paths cannot be decided, else best path. log.Debugf("enter compareByHighestWeight -- path1: %s, path2: %s", path1, path2) return nil } func compareByLocalPref(path1, path2 *Path) *Path { // Selects a path with highest local-preference. // // Unlike the weight attribute, which is only relevant to the local // router, local preference is an attribute that routers exchange in the // same AS. Highest local-pref is preferred. If we cannot decide, // we return None. // // # Default local-pref values is 100 log.Debugf("enter compareByLocalPref") localPref1, _ := path1.GetLocalPref() localPref2, _ := path2.GetLocalPref() // Highest local-preference value is preferred. if localPref1 > localPref2 { return path1 } else if localPref1 < localPref2 { return path2 } else { return nil } } func compareByLocalOrigin(path1, path2 *Path) *Path { // Select locally originating path as best path. // Locally originating routes are network routes, redistributed routes, // or aggregated routes. // Returns None if given paths have same source. // // If both paths are from same sources we cannot compare them here. log.Debugf("enter compareByLocalOrigin") if path1.GetSource().Equal(path2.GetSource()) { return nil } // Here we consider prefix from NC as locally originating static route. // Hence it is preferred. if path1.IsLocal() { return path1 } if path2.IsLocal() { return path2 } return nil } func compareByASPath(path1, path2 *Path) *Path { // Calculated the best-paths by comparing as-path lengths. // // Shortest as-path length is preferred. If both path have same lengths, // we return None. log.Debugf("enter compareByASPath") attribute1 := path1.getPathAttr(bgp.BGP_ATTR_TYPE_AS_PATH) attribute2 := path2.getPathAttr(bgp.BGP_ATTR_TYPE_AS_PATH) if attribute1 == nil || attribute2 == nil { log.WithFields(log.Fields{ "Topic": "Table", "Key": "compareByASPath", "ASPath1": attribute1, "ASPath2": attribute2, }).Warn("can't compare ASPath because it's not present") } l1 := path1.GetAsPathLen() l2 := path2.GetAsPathLen() log.Debugf("compareByASPath -- l1: %d, l2: %d", l1, l2) if l1 > l2 { return path2 } else if l1 < l2 { return path1 } else { return nil } } func compareByOrigin(path1, path2 *Path) *Path { // Select the best path based on origin attribute. // // IGP is preferred over EGP; EGP is preferred over Incomplete. // If both paths have same origin, we return None. log.Debugf("enter compareByOrigin") attribute1 := path1.getPathAttr(bgp.BGP_ATTR_TYPE_ORIGIN) attribute2 := path2.getPathAttr(bgp.BGP_ATTR_TYPE_ORIGIN) if attribute1 == nil || attribute2 == nil { log.WithFields(log.Fields{ "Topic": "Table", "Key": "compareByOrigin", "Origin1": attribute1, "Origin2": attribute2, }).Error("can't compare origin because it's not present") return nil } origin1, n1 := binary.Uvarint(attribute1.(*bgp.PathAttributeOrigin).Value) origin2, n2 := binary.Uvarint(attribute2.(*bgp.PathAttributeOrigin).Value) log.Debugf("compareByOrigin -- origin1: %d(%d), origin2: %d(%d)", origin1, n1, origin2, n2) // If both paths have same origins if origin1 == origin2 { return nil } else if origin1 < origin2 { return path1 } else { return path2 } } func compareByMED(path1, path2 *Path) *Path { // Select the path based with lowest MED value. // // If both paths have same MED, return None. // By default, a route that arrives with no MED value is treated as if it // had a MED of 0, the most preferred value. // RFC says lower MED is preferred over higher MED value. // compare MED among not only same AS path but also all path, // like bgp always-compare-med isInternal := func() bool { return path1.GetAsPathLen() == 0 && path2.GetAsPathLen() == 0 }() isSameAS := func() bool { firstAS := func(path *Path) uint32 { if aspath := path.GetAsPath(); aspath != nil { asPathParam := aspath.Value for i := 0; i < len(asPathParam); i++ { asPath := asPathParam[i].(*bgp.As4PathParam) if asPath.Num == 0 { continue } if asPath.Type == bgp.BGP_ASPATH_ATTR_TYPE_CONFED_SET || asPath.Type == bgp.BGP_ASPATH_ATTR_TYPE_CONFED_SEQ { continue } return asPath.AS[0] } } return 0 } return firstAS(path1) != 0 && firstAS(path1) == firstAS(path2) }() if SelectionOptions.AlwaysCompareMed || isInternal || isSameAS { log.Debugf("enter compareByMED") getMed := func(path *Path) uint32 { attribute := path.getPathAttr(bgp.BGP_ATTR_TYPE_MULTI_EXIT_DISC) if attribute == nil { return 0 } med := attribute.(*bgp.PathAttributeMultiExitDisc).Value return med } med1 := getMed(path1) med2 := getMed(path2) log.Debugf("compareByMED -- med1: %d, med2: %d", med1, med2) if med1 == med2 { return nil } else if med1 < med2 { return path1 } return path2 } else { log.Debugf("skip compareByMED %v %v %v", SelectionOptions.AlwaysCompareMed, isInternal, isSameAS) return nil } } func compareByASNumber(path1, path2 *Path) *Path { //Select the path based on source (iBGP/eBGP) peer. // //eBGP path is preferred over iBGP. If both paths are from same kind of //peers, return None. log.Debugf("enter compareByASNumber") log.Debugf("compareByASNumber -- p1Asn: %d, p2Asn: %d", path1.GetSource().AS, path2.GetSource().AS) // If one path is from ibgp peer and another is from ebgp peer, take the ebgp path if path1.IsIBGP() != path2.IsIBGP() { if path1.IsIBGP() { return path2 } return path1 } // If both paths are from ebgp or ibpg peers, we cannot decide. return nil } func compareByIGPCost(path1, path2 *Path) *Path { // Select the route with the lowest IGP cost to the next hop. // // Return None if igp cost is same. // Currently BGPS has no concept of IGP and IGP cost. log.Debugf("enter compareByIGPCost -- path1: %v, path2: %v", path1, path2) return nil } func compareByRouterID(path1, path2 *Path) (*Path, error) { // Select the route received from the peer with the lowest BGP router ID. // // If both paths are eBGP paths, then we do not do any tie breaking, i.e we do // not pick best-path based on this criteria. // RFC: http://tools.ietf.org/html/rfc5004 // We pick best path between two iBGP paths as usual. log.Debugf("enter compareByRouterID") // If both paths are from NC we have same router Id, hence cannot compare. if path1.IsLocal() && path2.IsLocal() { return nil, nil } // If both paths are from eBGP peers, then according to RFC we need // not tie break using router id. if !SelectionOptions.ExternalCompareRouterId && !path1.IsIBGP() && !path2.IsIBGP() { return nil, nil } if !SelectionOptions.ExternalCompareRouterId && path1.IsIBGP() != path2.IsIBGP() { return nil, fmt.Errorf("This method does not support comparing ebgp with ibgp path") } // At least one path is not coming from NC, so we get local bgp id. id1 := binary.BigEndian.Uint32(path1.GetSource().ID) id2 := binary.BigEndian.Uint32(path2.GetSource().ID) // If both router ids are same/equal we cannot decide. // This case is possible since router ids are arbitrary. if id1 == id2 { return nil, nil } else if id1 < id2 { return path1, nil } else { return path2, nil } } func compareByAge(path1, path2 *Path) *Path { if !path1.IsIBGP() && !path2.IsIBGP() && !SelectionOptions.ExternalCompareRouterId { age1 := path1.GetTimestamp().UnixNano() age2 := path2.GetTimestamp().UnixNano() if age1 == age2 { return nil } else if age1 < age2 { return path1 } return path2 } return nil } func (dest *Destination) String() string { return fmt.Sprintf("Destination NLRI: %s", dest.nlri.String()) } type destinations []*Destination func (d destinations) Len() int { return len(d) } func (d destinations) Swap(i, j int) { d[i], d[j] = d[j], d[i] } func (d destinations) Less(i, j int) bool { switch d[i].routeFamily { case bgp.RF_FS_IPv4_UC, bgp.RF_FS_IPv6_UC, bgp.RF_FS_IPv4_VPN, bgp.RF_FS_IPv6_VPN, bgp.RF_FS_L2_VPN: var s, t *bgp.FlowSpecNLRI switch d[i].routeFamily { case bgp.RF_FS_IPv4_UC: s = &d[i].nlri.(*bgp.FlowSpecIPv4Unicast).FlowSpecNLRI t = &d[j].nlri.(*bgp.FlowSpecIPv4Unicast).FlowSpecNLRI case bgp.RF_FS_IPv6_UC: s = &d[i].nlri.(*bgp.FlowSpecIPv6Unicast).FlowSpecNLRI t = &d[j].nlri.(*bgp.FlowSpecIPv6Unicast).FlowSpecNLRI case bgp.RF_FS_IPv4_VPN: s = &d[i].nlri.(*bgp.FlowSpecIPv4VPN).FlowSpecNLRI t = &d[j].nlri.(*bgp.FlowSpecIPv4VPN).FlowSpecNLRI case bgp.RF_FS_IPv6_VPN: s = &d[i].nlri.(*bgp.FlowSpecIPv6VPN).FlowSpecNLRI t = &d[j].nlri.(*bgp.FlowSpecIPv6VPN).FlowSpecNLRI case bgp.RF_FS_L2_VPN: s = &d[i].nlri.(*bgp.FlowSpecL2VPN).FlowSpecNLRI t = &d[j].nlri.(*bgp.FlowSpecL2VPN).FlowSpecNLRI } if r, _ := bgp.CompareFlowSpecNLRI(s, t); r >= 0 { return true } else { return false } default: strings := sort.StringSlice{d[i].nlri.String(), d[j].nlri.String()} return strings.Less(0, 1) } }