66265764df
The agent now collects the device's reachable prefixes from the kernel FIB
(including FRR-installed routes) via 'ip route show' / 'ip -6 route show' and
reports them to the control plane (POST /devices/{name}/routes) alongside its
status. tomswallapi uses these to scope which routers enforce a rule. Route
parsing (default routes, ECMP nexthop lines, route-type keywords, host routes,
v4/v6) is unit-tested; collection degrades to nil without iproute2.
77 lines
2.1 KiB
Go
77 lines
2.1 KiB
Go
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"
|
|
}
|