◐ Off-By-One · answer catalog

go-bgp-best-path-selection-route-reflector-loop-exactness

1 answer(s)gogo1.26

The task is not "pick the smallest AS path". Byte-exactness against a 480-prefix Loc-RIB fails for these concrete reasons:

📦 Source in repository (JSON)

Answer

All code is built, tested, and re-extracted from the markdown into a clean module where it compiles and passes. Final deliverable: /workspace/SOLUTION.md (47 KB, includes full regression suite too) and the module at /workspace/bgpselect/.


bgpselect — byte-exact RFC 4271 best-path selection with route-reflector loop rejection

1. Root-cause analysis

The task is not "pick the smallest AS path". Byte-exactness against a 480-prefix Loc-RIB fails for these concrete reasons:

  1. Loop rejection must happen before any comparison, and only for the two rules in scope. A path is ineligible if its AS_PATH already contains the local ASN, or if its CLUSTER_LIST already carries the local cluster ID. Route-reflector dumps are full of both. The ORIGINATOR_ID trap: two paths may legitimately carry the same ORIGINATOR_ID (the same originating router reflected by two RRs). That is a collision, not a loop — it must not by itself reject a path. RFC 4456's ORIGINATOR_ID == local router-id check is an opt-in flag (-reject-originator-id) so the literal two-rule behaviour is default and the dump's ORIGINATOR_ID collisions survive into comparison.

  2. MED has a narrow scope. MULTI_EXIT_DISC is only comparable between paths learned from the same neighbouring AS (RFC 4271 §). A naive global min(MED) discards other-AS paths and produces the wrong winner and wrong multipath set. Correct reduction is per-AS: inside each peer-AS group keep lowest-MED paths, then union the groups.

  3. The multipath set and the decisive stage are different things. The ECMP set is everything left after IGP metric, before router-ID/peer-address tie-breakers. The decisive stage is the last criterion that removed candidates (only_path when loop rejection leaves a singleton). Conflating them breaks both fields.

  4. Stage order is fixed: WEIGHT → LOCAL_PREF → locally originated → AS_PATH length → ORIGIN → MED (same neighbour AS) → eBGP over iBGP → lowest IGP metric → router-ID → peer-address.

  5. Direction of each comparison. Higher-is-better: WEIGHT, LOCAL_PREF, locally-originated. Lower-is-better: AS_PATH length, ORIGIN, MED, IGP metric, router-ID, peer-address. eBGP beats iBGP.

  6. Byte-exact output needs deterministic ordering. Canonicalise prefixes, sort numerically (address then mask length), de-duplicate/sort the multipath set, compact JSON, fixed field order, newline-terminated records.

  7. AS_PATH decoding details. AS_SET counts as one hop; handle 4-byte AS_PATH/AS4_PATH.

2. Deliverable

Module bgpselect reads rib.jsonl, writes locrib.jsonl:

{"prefix":"<ip-address>/24","next_hop":"<ip-address>","as_path":[65001],"decisive_stage":"as_path_length","multipath_set":["<ip-address>"]}

decisive_stage ∈ only_path, weight, local_pref, locally_originated, as_path_length, origin, med, ebgp_over_ibgp, igp_metric, router_id, peer_address. multipath_set is the sorted set of ECMP-equal next_hops after the IGP-metric stage; next_hop is the winner of the router-ID / peer-address tie-break inside it.

The reader accepts enriched JSON path records (aliases tolerated), a leading config line (local_as, cluster_id, router_id), and optionally raw MRT hex (bare or under mrt/record/hex/raw), carrying the peer-index table across records.

3. Exact fix

go.mod:

module bgpselect

go 1.26

main.go

package main

import (
    "bufio"
    "encoding/json"
    "flag"
    "fmt"
    "os"
)

// locribRecord is the exact output shape. Field order is fixed so the emitted
// JSON is byte-stable across runs.
type locribRecord struct {
    Prefix        string   `json:"prefix"`
    NextHop       string   `json:"next_hop"`
    ASPath        []uint32 `json:"as_path"`
    DecisiveStage string   `json:"decisive_stage"`
    MultipathSet  []string `json:"multipath_set"`
}

func main() {
    in := flag.String("in", "rib.jsonl", "input RIB-in JSONL file")
    out := flag.String("out", "locrib.jsonl", "output Loc-RIB JSONL file")
    localAS := flag.Uint("local-as", 0, "override local ASN for AS_PATH loop rejection")
    clusterID := flag.String("cluster-id", "", "override local cluster ID for CLUSTER_LIST loop rejection")
    routerID := flag.String("router-id", "", "override local router ID for ORIGINATOR_ID checks")
    rejectOrig := flag.Bool("reject-originator-id", false, "also reject paths whose ORIGINATOR_ID equals the local router ID (RFC 4456)")
    flag.Parse()

    routes, cfg, err := readRIB(*in)
    if err != nil {
        fmt.Fprintln(os.Stderr, "bgpselect:", err)
        os.Exit(1)
    }
    if *localAS != 0 {
        cfg.LocalAS = uint32(*localAS)
    }
    if *clusterID != "" {
        cfg.ClusterID = *clusterID
    }
    if *routerID != "" {
        cfg.RouterID = *routerID
    }

    decisions := ProcessPrefix(routes, cfg, *rejectOrig)

    f, err := os.Create(*out)
    if err != nil {
        fmt.Fprintln(os.Stderr, "bgpselect:", err)
        os.Exit(1)
    }
    defer f.Close()
    w := bufio.NewWriter(f)
    enc := json.NewEncoder(w)
    enc.SetEscapeHTML(false)
    for _, d := range decisions {
        rec := locribRecord{
            Prefix:        d.Prefix,
            NextHop:       d.NextHop,
            ASPath:        d.ASPath,
            DecisiveStage: d.DecisiveStage,
            MultipathSet:  d.Multipath,
        }
        if err := enc.Encode(rec); err != nil {
            fmt.Fprintln(os.Stderr, "bgpselect:", err)
            os.Exit(1)
        }
    }
    if err := w.Flush(); err != nil {
        fmt.Fprintln(os.Stderr, "bgpselect:", err)
        os.Exit(1)
    }
}

// readRIB parses the JSONL dump. Both a leading config line and per-line
// config fields are accepted. Any line that carries a prefix is a route.
func readRIB(path string) ([]Route, Config, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, Config{}, err
    }
    defer f.Close()

    var routes []Route
    var cfg Config
    mrt := newMRTState()
    sc := bufio.NewScanner(f)
    sc.Buffer(make([]byte, 1024*1024), 16*1024*1024)
    line := 0
    for sc.Scan() {
        line++
        raw := sc.Bytes()
        trimmed := bytesTrimSpace(raw)
        if len(trimmed) == 0 {
            continue
        }
        // A bare hex string is an MRT record.
        if trimmed[0] != '{' {
            if rs, err := mrt.feed(string(trimmed), cfg); err == nil {
                routes = append(routes, rs...)
                continue
            }
        }
        var m map[string]json.RawMessage
        if err := json.Unmarshal(raw, &m); err != nil {
            return nil, cfg, fmt.Errorf("line %d: %w", line, err)
        }
        if _, isRoute := rawLookup(m, "prefix", "nlri", "network"); isRoute {
            routes = append(routes, ParseRoute(m))
            continue
        }
        // An embedded hex MRT record.
        if rawHex, ok := rawLookup(m, "mrt", "mrt_hex", "record", "hex", "raw", "data"); ok {
            if hs, err := decodeString(rawHex); err == nil {
                if rs, err := mrt.feed(hs, cfg); err == nil {
                    routes = append(routes, rs...)
                    continue
                }
            }
        }
        if nested, ok := rawLookup(m, "config"); ok {
            var cm map[string]json.RawMessage
            if json.Unmarshal(nested, &cm) == nil {
                if c, ok := ParseConfig(cm); ok {
                    cfg = mergeConfig(cfg, c)
                }
            }
        }
        if c, ok := ParseConfig(m); ok {
            cfg = mergeConfig(cfg, c)
        }
    }
    if err := sc.Err(); err != nil {
        return nil, cfg, err
    }
    return routes, cfg, nil
}

func mergeConfig(base, over Config) Config {
    if over.LocalAS != 0 {
        base.LocalAS = over.LocalAS
    }
    if over.ClusterID != "" {
        base.ClusterID = over.ClusterID
    }
    if over.RouterID != "" {
        base.RouterID = over.RouterID
    }
    return base
}

func bytesTrimSpace(b []byte) []byte {
    start := 0
    for start < len(b) && (b[start] == ' ' || b[start] == '\t' || b[start] == '\r' || b[start] == '\n') {
        start++
    }
    end := len(b)
    for end > start && (b[end-1] == ' ' || b[end-1] == '\t' || b[end-1] == '\r' || b[end-1] == '\n') {
        end--
    }
    return b[start:end]
}

types.go

package main

import (
    "encoding/json"
    "fmt"
    "net"
    "strconv"
    "strings"
)

// Route is a single BGP path (RIB-in entry) for a prefix.
type Route struct {
    Prefix            string   `json:"prefix"`
    NextHop           string   `json:"next_hop"`
    PeerAddress       string   `json:"peer_address"`
    PeerAS            uint32   `json:"peer_as"`
    PeerType          string   `json:"peer_type"` // "ebgp", "ibgp" or "local"
    ASPath            []uint32 `json:"as_path"`
    Origin            int      `json:"origin"` // 0=IGP, 1=EGP, 2=INCOMPLETE
    LocalPref         uint32   `json:"local_pref"`
    MED               uint32   `json:"med"`
    Weight            uint32   `json:"weight"`
    IGPMetric         uint32   `json:"igp_metric"`
    RouterID          string   `json:"router_id"`
    ClusterList       []string `json:"cluster_list"`
    OriginatorID      string   `json:"originator_id"`
    LocallyOriginated bool     `json:"locally_originated"`
}

// Config holds the local autonomous system identity used for loop rejection.
type Config struct {
    LocalAS   uint32
    ClusterID string
    RouterID  string
}

func rawLookup(m map[string]json.RawMessage, keys ...string) (json.RawMessage, bool) {
    for _, k := range keys {
        if v, ok := m[k]; ok {
            return v, true
        }
    }
    return nil, false
}

func decodeString(raw json.RawMessage) (string, error) {
    var s string
    if err := json.Unmarshal(raw, &s); err == nil {
        return s, nil
    }
    var n json.Number
    if err := json.Unmarshal(raw, &n); err == nil {
        return n.String(), nil
    }
    return "", fmt.Errorf("not a string")
}

func rawToString(m map[string]json.RawMessage, keys ...string) string {
    raw, ok := rawLookup(m, keys...)
    if !ok {
        return ""
    }
    s, err := decodeString(raw)
    if err != nil {
        return ""
    }
    return s
}

func rawToUint(m map[string]json.RawMessage, keys ...string) uint32 {
    raw, ok := rawLookup(m, keys...)
    if !ok {
        return 0
    }
    var n json.Number
    if err := json.Unmarshal(raw, &n); err != nil {
        var s string
        if err := json.Unmarshal(raw, &s); err == nil {
            if v, err := strconv.ParseUint(strings.TrimSpace(s), 10, 32); err == nil {
                return uint32(v)
            }
        }
        return 0
    }
    v, err := strconv.ParseUint(n.String(), 10, 32)
    if err != nil {
        return 0
    }
    return uint32(v)
}

func rawToInt(raw json.RawMessage) int {
    var n json.Number
    if err := json.Unmarshal(raw, &n); err == nil {
        if v, err := strconv.Atoi(n.String()); err == nil {
            return v
        }
    }
    var s string
    if err := json.Unmarshal(raw, &s); err == nil {
        switch strings.ToLower(strings.TrimSpace(s)) {
        case "igp", "i", "0":
            return 0
        case "egp", "e", "1":
            return 1
        case "incomplete", "inc", "?", "2":
            return 2
        }
        if v, err := strconv.Atoi(strings.TrimSpace(s)); err == nil {
            return v
        }
    }
    return 2
}

func toUint32Slice(raw json.RawMessage) []uint32 {
    var nums []json.Number
    if err := json.Unmarshal(raw, &nums); err == nil {
        out := make([]uint32, 0, len(nums))
        for _, n := range nums {
            v, err := strconv.ParseUint(n.String(), 10, 32)
            if err == nil {
                out = append(out, uint32(v))
            }
        }
        return out
    }
    var strs []string
    if err := json.Unmarshal(raw, &strs); err == nil {
        out := make([]uint32, 0, len(strs))
        for _, s := range strs {
            v, err := strconv.ParseUint(strings.TrimSpace(s), 10, 32)
            if err == nil {
                out = append(out, uint32(v))
            }
        }
        return out
    }
    return nil
}

func toStringSlice(raw json.RawMessage) []string {
    var strs []string
    if err := json.Unmarshal(raw, &strs); err == nil {
        return strs
    }
    var one string
    if err := json.Unmarshal(raw, &one); err == nil {
        one = strings.TrimSpace(one)
        if one == "" {
            return nil
        }
        parts := strings.Split(one, " ")
        out := make([]string, 0, len(parts))
        for _, p := range parts {
            if p = strings.TrimSpace(p); p != "" {
                out = append(out, p)
            }
        }
        return out
    }
    return nil
}

// ParseRoute converts a decoded JSON object into a Route.
func ParseRoute(m map[string]json.RawMessage) Route {
    r := Route{
        Prefix:       rawToString(m, "prefix", "nlri", "network"),
        NextHop:      rawToString(m, "next_hop", "nexthop", "next-hop"),
        PeerAddress:  rawToString(m, "peer_address", "peer", "neighbor", "peer_addr"),
        PeerAS:       rawToUint(m, "peer_as", "peeras", "peer-as", "neighbor_as"),
        PeerType:     strings.ToLower(rawToString(m, "peer_type", "peer-type", "type", "session_type")),
        RouterID:     rawToString(m, "router_id", "router-id", "routerid", "peer_router_id"),
        OriginatorID: rawToString(m, "originator_id", "originator-id", "originatorid"),
        LocalPref:    rawToUint(m, "local_pref", "local-pref", "localpref"),
        MED:          rawToUint(m, "med", "multi_exit_discriminator"),
        Weight:       rawToUint(m, "weight", "local_weight"),
        IGPMetric:    rawToUint(m, "igp_metric", "igp-metric", "igpmetric", "metric"),
    }
    if raw, ok := rawLookup(m, "as_path", "as-path", "aspath", "as_paths"); ok {
        r.ASPath = toUint32Slice(raw)
    }
    if raw, ok := rawLookup(m, "cluster_list", "cluster-list", "clusterlist", "cluster_id"); ok {
        r.ClusterList = toStringSlice(raw)
    }
    if raw, ok := rawLookup(m, "origin", "origin_code"); ok {
        r.Origin = rawToInt(raw)
    } else {
        r.Origin = 2
    }
    if raw, ok := rawLookup(m, "locally_originated", "locally-originated", "local_origin", "is_local"); ok {
        var b bool
        if err := json.Unmarshal(raw, &b); err == nil {
            r.LocallyOriginated = b
        }
    }
    if r.PeerType == "local" || r.PeerType == "self" {
        r.LocallyOriginated = true
    }
    if r.PeerAddress == "" {
        r.PeerAddress = r.NextHop
    }
    if r.RouterID == "" {
        r.RouterID = r.PeerAddress
    }
    return r
}

// ParseConfig extracts the local identity from a JSON object.
func ParseConfig(m map[string]json.RawMessage) (Config, bool) {
    c := Config{
        LocalAS:   rawToUint(m, "local_as", "local-as", "our_asn", "local_asn", "my_asn"),
        ClusterID: rawToString(m, "cluster_id", "cluster-id", "clusterid", "our_cluster_id"),
        RouterID:  rawToString(m, "router_id", "router-id", "local_router_id", "bgp_identifier"),
    }
    if c.LocalAS == 0 && c.ClusterID == "" && c.RouterID == "" {
        return c, false
    }
    return c, true
}

func ipKey(s string) []byte {
    ip := net.ParseIP(strings.TrimSpace(s))
    if ip == nil {
        return []byte(strings.TrimSpace(s))
    }
    if v4 := ip.To4(); v4 != nil {
        return v4
    }
    return ip.To16()
}

func cmpIP(a, b string) int {
    return strings.Compare(string(ipKey(a)), string(ipKey(b)))
}

func canonicalPrefix(p string) (string, bool) {
    ip, ipnet, err := net.ParseCIDR(p)
    if err != nil {
        return p, false
    }
    ones, _ := ipnet.Mask.Size()
    return ip.Mask(ipnet.Mask).String() + "/" + strconv.Itoa(ones), true
}

func prefixSortKey(p string) ([]byte, int) {
    ip, ipnet, err := net.ParseCIDR(p)
    if err != nil {
        return []byte(p), 0
    }
    ones, _ := ipnet.Mask.Size()
    return ip.Mask(ipnet.Mask), ones
}

select.go

package main

import (
    "bytes"
    "fmt"
    "sort"
)

// Stage names reported in the decisive_stage field.
const (
    StageOnlyPath          = "only_path"
    StageWeight            = "weight"
    StageLocalPref         = "local_pref"
    StageLocallyOriginated = "locally_originated"
    StageASPathLength      = "as_path_length"
    StageOrigin            = "origin"
    StageMED               = "med"
    StageEBGPOverIBGP      = "ebgp_over_ibgp"
    StageIGPMetric         = "igp_metric"
    StageRouterID          = "router_id"
    StagePeerAddress       = "peer_address"
)

// Decision is the result of running best-path selection for one prefix.
type Decision struct {
    Prefix        string
    NextHop       string
    ASPath        []uint32
    DecisiveStage string
    Multipath     []string
}

func normalizeASPath(p []uint32) []uint32 {
    if p == nil {
        return []uint32{}
    }
    return p
}

// looped reports whether a path must be rejected for AS / route-reflector loop
// reasons: AS_PATH contains our ASN, or CLUSTER_LIST carries our cluster ID.
// rejectOriginator additionally applies the RFC 4456 ORIGINATOR_ID check.
func looped(r Route, cfg Config, rejectOriginator bool) bool {
    for _, as := range r.ASPath {
        if as == cfg.LocalAS && cfg.LocalAS != 0 {
            return true
        }
    }
    if cfg.ClusterID != "" {
        for _, cl := range r.ClusterList {
            if sameIP(cl, cfg.ClusterID) {
                return true
            }
        }
    }
    if rejectOriginator && cfg.RouterID != "" && r.OriginatorID != "" && sameIP(r.OriginatorID, cfg.RouterID) {
        return true
    }
    return false
}

func sameIP(a, b string) bool {
    return string(ipKey(a)) == string(ipKey(b))
}

// applyMED reduces the candidate set using MULTI_EXIT_DISC, but only within
// paths learned from the same neighbouring AS (RFC 4271 §<ip-address>). Each AS
// group independently keeps its lowest-MED paths; groups are unioned so MED is
// never used to eliminate a whole neighbouring AS.
func applyMED(cands []Route) []Route {
    type group struct {
        best uint32
        seen bool
    }
    groups := map[uint32]*group{}
    for _, c := range cands {
        g := groups[c.PeerAS]
        if g == nil {
            g = &group{}
            groups[c.PeerAS] = g
        }
        if !g.seen || c.MED < g.best {
            g.best = c.MED
            g.seen = true
        }
    }
    out := make([]Route, 0, len(cands))
    for _, c := range cands {
        if c.MED == groups[c.PeerAS].best {
            out = append(out, c)
        }
    }
    return out
}

func originRank(o int) int {
    if o < 0 || o > 2 {
        return 2
    }
    return o
}

func ebgpRank(t string) int {
    switch t {
    case "ebgp", "external", "local", "self":
        return 2
    default:
        return 1
    }
}

// SelectBest runs the full decision process over the candidate paths for a
// single prefix. Each candidate must already have passed loop rejection.
func SelectBest(prefix string, cands []Route) (Decision, bool) {
    if len(cands) == 0 {
        return Decision{}, false
    }
    dec := Decision{Prefix: prefix}
    if len(cands) == 1 {
        dec.NextHop = cands[0].NextHop
        dec.ASPath = normalizeASPath(cands[0].ASPath)
        dec.DecisiveStage = StageOnlyPath
        dec.Multipath = []string{cands[0].NextHop}
        return dec, true
    }

    cur := cands
    decisive := ""
    reduce := func(stage string, key func(Route) int64) {
        if len(cur) <= 1 {
            return
        }
        best := key(cur[0])
        for _, c := range cur[1:] {
            if k := key(c); k < best {
                best = k
            }
        }
        next := make([]Route, 0, len(cur))
        for _, c := range cur {
            if key(c) == best {
                next = append(next, c)
            }
        }
        if len(next) < len(cur) {
            decisive = stage
            cur = next
        }
    }

    // 1. WEIGHT (higher is better).
    reduce(StageWeight, func(r Route) int64 { return -int64(r.Weight) })
    // 2. LOCAL_PREF (higher is better).
    reduce(StageLocalPref, func(r Route) int64 { return -int64(r.LocalPref) })
    // 3. Locally originated (preferred over learned).
    reduce(StageLocallyOriginated, func(r Route) int64 {
        if r.LocallyOriginated {
            return 0
        }
        return 1
    })
    // 4. Shortest AS_PATH.
    reduce(StageASPathLength, func(r Route) int64 { return int64(len(r.ASPath)) })
    // 5. Lowest ORIGIN.
    reduce(StageOrigin, func(r Route) int64 { return int64(originRank(r.Origin)) })
    // 6. MED, restricted to the same neighbouring AS.
    if len(cur) > 1 {
        if next := applyMED(cur); len(next) < len(cur) {
            decisive = StageMED
            cur = next
        }
    }
    // 7. eBGP preferred over iBGP.
    reduce(StageEBGPOverIBGP, func(r Route) int64 { return int64(-ebgpRank(r.PeerType)) })
    // 8. Lowest IGP metric to the next hop.
    reduce(StageIGPMetric, func(r Route) int64 { return int64(r.IGPMetric) })

    // Everything still in cur is equal cost and forms the multipath set.
    multipath := append([]Route(nil), cur...)

    // 9. Lowest router ID.
    reduce(StageRouterID, func(r Route) int64 { return int64(ipToUint(r.RouterID)) })
    // 10. Lowest peer address.
    reduce(StagePeerAddress, func(r Route) int64 { return int64(ipToUint(r.PeerAddress)) })

    // Final deterministic fallback if peer addresses also collide.
    if len(cur) > 1 {
        minNH := cur[0].NextHop
        for _, c := range cur[1:] {
            if cmpIP(c.NextHop, minNH) < 0 {
                minNH = c.NextHop
            }
        }
        next := make([]Route, 0, len(cur))
        for _, c := range cur {
            if c.NextHop == minNH {
                next = append(next, c)
            }
        }
        cur = next
    }

    winner := cur[0]
    dec.NextHop = winner.NextHop
    dec.ASPath = normalizeASPath(winner.ASPath)
    dec.DecisiveStage = decisive
    if dec.DecisiveStage == "" {
        dec.DecisiveStage = StagePeerAddress
    }
    dec.Multipath = multipathNextHops(multipath)
    return dec, true
}

func multipathNextHops(rs []Route) []string {
    set := make(map[string]struct{}, len(rs))
    for _, r := range rs {
        set[r.NextHop] = struct{}{}
    }
    out := make([]string, 0, len(set))
    for nh := range set {
        out = append(out, nh)
    }
    sort.Slice(out, func(i, j int) bool { return cmpIP(out[i], out[j]) < 0 })
    return out
}

func ipToUint(s string) uint64 {
    b := ipKey(s)
    var v uint64
    for i := 0; i < len(b) && i < 8; i++ {
        v = v<<8 | uint64(b[i])
    }
    return v
}

// ProcessPrefix groups routes by prefix, rejects loops and selects the best
// path for each prefix, returning entries in numeric prefix order.
func ProcessPrefix(routes []Route, cfg Config, rejectOriginator bool) []Decision {
    byPrefix := make(map[string][]Route)
    order := make([]string, 0)
    for _, r := range routes {
        cp, ok := canonicalPrefix(r.Prefix)
        if !ok {
            cp = r.Prefix
        }
        if _, seen := byPrefix[cp]; !seen {
            order = append(order, cp)
        }
        byPrefix[cp] = append(byPrefix[cp], r)
    }

    var out []Decision
    for _, p := range order {
        var valid []Route
        for _, r := range byPrefix[p] {
            if r.LocallyOriginated && len(r.ASPath) == 0 {
                valid = append(valid, r)
                continue
            }
            if looped(r, cfg, rejectOriginator) {
                continue
            }
            valid = append(valid, r)
        }
        if len(valid) == 0 {
            continue
        }
        if d, ok := SelectBest(p, valid); ok {
            out = append(out, d)
        }
    }

    sort.Slice(out, func(i, j int) bool {
        ai, al := prefixSortKey(out[i].Prefix)
        bi, bl := prefixSortKey(out[j].Prefix)
        if c := bytes.Compare(ai, bi); c != 0 {
            return c < 0
        }
        return al < bl
    })
    return out
}

func (d Decision) String() string {
    return fmt.Sprintf("%s via %s [%s] %v", d.Prefix, d.NextHop, d.DecisiveStage, d.Multipath)
}

mrt.go (optional raw-hex input)

Full decoder for MRT TABLE_DUMP_V2 (PEER_INDEX_TABLE, RIB_IPV4/IPV6_UNICAST), legacy TABLE_DUMP, and BGP path attributes (ORIGIN, AS_PATH/AS4_PATH, NEXT_HOP, MED, LOCAL_PREF, ORIGINATOR_ID, CLUSTER_LIST, MP_REACH_NLRI). AS_SET counts as one hop. The full file is in /workspace/bgpselect/mrt.go and /workspace/SOLUTION.md §3.5.

4. Build and run

cd bgpselect
go build -o bgpselect .
./bgpselect -in rib.jsonl -out locrib.jsonl
# flags: -local-as, -cluster-id, -router-id, -reject-originator-id

5. Verification

5.1 Regression suite

select_test.go + mrt_test.go cover every decisive stage, MED scope, loop rejection, ORIGINATOR_ID non-rejection, deterministic ordering, MRT decoding, and a 480-prefix end-to-end pass. Full source is in /workspace/SOLUTION.md §5.1 and /workspace/bgpselect/.

5.2 Commands and observed results

$ go vet ./...
$ go test ./... -v
--- PASS: TestMRTTableDumpV2 (0.00s)
--- PASS: TestStages (0.00s)
--- PASS: TestMEDNotComparedAcrossAS (0.00s)
--- PASS: TestMEDWithinASOnly (0.00s)
--- PASS: TestMultipathSet (0.00s)
--- PASS: TestLoopRejection (0.00s)
--- PASS: TestOriginatorIDNotRejectedByDefault (0.00s)
--- PASS: TestCanonicalPrefixSort (0.00s)
--- PASS: Test480EndToEnd (0.02s)
PASS
ok      bgpselect       0.019s

5.3 Self-containment proof

The six Go blocks were extracted from SOLUTION.md into a clean go.mod module; it compiled and passed:

$ go build ./... && go vet ./... && go test ./...
ok      bgpselect       0.023s
EXTRACTED_BUILD_TEST_OK

5.4 End-to-end determinism check

$ ./bgpselect -in rib480.jsonl -out a.jsonl
$ ./bgpselect -in rib480.jsonl -out b.jsonl
$ cmp a.jsonl b.jsonl && echo byte-identical
byte-identical
$ wc -l a.jsonl
480 a.jsonl

Sample output (AS_PATH loop + stale CLUSTER_LIST leave one path; equal-cost pair resolved by router-ID):

{"prefix":"<ip-address>/24","next_hop":"<ip-address>","as_path":[65001],"decisive_stage":"as_path_length","multipath_set":["<ip-address>"]}
{"prefix":"<ip-address>/24","next_hop":"<ip-address>","as_path":[65001],"decisive_stage":"router_id","multipath_set":["<ip-address>","<ip-address>"]}
{"prefix":"<ip-address>/24","next_hop":"<ip-address>","as_path":[65001],"decisive_stage":"only_path","multipath_set":["<ip-address>"]}

5.5 Stage-coverage map

decisive_stage test
only_path TestLoopRejection, end-to-end
weight TestStages/weight
local_pref TestStages/local_pref
locally_originated TestStages/locally_originated
as_path_length TestStages/as_path_length
origin TestStages/origin
med TestStages/med, TestMEDWithinASOnly
ebgp_over_ibgp TestStages/ebgp_over_ibgp
igp_metric TestStages/igp_metric
router_id TestStages/router_id, TestMultipathSet
peer_address TestStages/peer_address

Files produced: /workspace/bgpselect/{go.mod,main.go,types.go,select.go,mrt.go,select_test.go,mrt_test.go} and /workspace/SOLUTION.md.

Evidence & signatures

# Evidence
- Problem class: go-bgp-best-path-selection-route-reflector-loop-exactness
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-26T04:29:07.038Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Parse a 480-prefix RIB-in dump (MRT hex records from 12 eBGP/iBGP sessions, one route-reflector cluster with a stale CLUSTER_LIST and two ORIGINATOR_ID collisions) and implement RFC 4271 best-path selection in full order: WEIGHT, LOCAL_PREF, locally originated, AS_PATH length, ORIGIN, MED restricted to paths from the same neighboring AS, eBGP over iBGP, lowest IGP metric, then router-ID and peer-address tie-break. Reject every path whose AS_PATH already contains our ASN or whose CLUSTER_LIST carries our cluster ID, and attribute each Loc-RIB entry to the exact tie-break stage that decided it. Deliverable: Go module bgpselect reading rib.jsonl and writing locrib.jsonl with prefix, next_hop, as_path, decisive_stage and multipath_set, byte-exact against all 480 expected prefixes.", "environment": "go1.26", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-bgp-best-path-selection-route-reflector-loop-exactness", "provider": "openrouter", "solved_at": "2026-09-26T04:29:07.038Z", "version": "1.26"}
Generated from the verified corpus · MIT licensedBack to the catalog