package agent import ( "context" "os/exec" "strings" ) // CollectFIB returns the device's reachable prefixes from the kernel FIB — which // includes FRR-installed routes — by shelling out to `ip route`. The control // plane uses these to scope which routers enforce a rule. On a host without // iproute2 it returns nil, and the control plane falls back to over-approximation. func CollectFIB(ctx context.Context) []string { var prefixes []string for _, spec := range []struct { args []string def string }{ {[]string{"route", "show"}, "0.0.0.0/0"}, {[]string{"-6", "route", "show"}, "::/0"}, } { out, err := exec.CommandContext(ctx, "ip", spec.args...).Output() if err != nil { continue } prefixes = append(prefixes, parseRoutes(string(out), spec.def)...) } return dedup(prefixes) } // routeTypeKeywords are leading tokens in `ip route` output that precede the // actual destination (e.g. "unreachable 10.0.0.0/8"). var routeTypeKeywords = map[string]bool{ "unreachable": true, "blackhole": true, "prohibit": true, "throw": true, "local": true, "broadcast": true, "multicast": true, "anycast": true, "nat": true, } // parseRoutes extracts destination prefixes from `ip route show` output. // defaultPrefix is substituted for a "default" route (family-specific). func parseRoutes(output, defaultPrefix string) []string { var out []string for _, line := range strings.Split(output, "\n") { fields := strings.Fields(line) if len(fields) == 0 { continue } dst := fields[0] // ECMP routes emit continuation "nexthop ..." lines with no destination. if dst == "nexthop" { continue } if routeTypeKeywords[dst] { if len(fields) < 2 { continue } dst = fields[1] } if dst == "default" { out = append(out, defaultPrefix) continue } out = append(out, normalizePrefix(dst)) } return out } // normalizePrefix turns a bare host address into a host prefix (/32 or /128). func normalizePrefix(dst string) string { if strings.Contains(dst, "/") { return dst } if strings.Contains(dst, ":") { return dst + "/128" } return dst + "/32" }