mirror of
https://github.com/netbirdio/netbird.git
synced 2025-08-18 19:09:09 +02:00
[client] Add detailed routes and resolved IPs to debug bundle (#4141)
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/hashicorp/go-multierror"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/vishvananda/netlink"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
nberrors "github.com/netbirdio/netbird/client/errors"
|
||||
"github.com/netbirdio/netbird/client/internal/routemanager/sysctl"
|
||||
@@ -22,6 +23,25 @@ import (
|
||||
nbnet "github.com/netbirdio/netbird/util/net"
|
||||
)
|
||||
|
||||
// IPRule contains IP rule information for debugging
|
||||
type IPRule struct {
|
||||
Priority int
|
||||
From netip.Prefix
|
||||
To netip.Prefix
|
||||
IIF string
|
||||
OIF string
|
||||
Table string
|
||||
Action string
|
||||
Mark uint32
|
||||
Mask uint32
|
||||
TunID uint32
|
||||
Goto uint32
|
||||
Flow uint32
|
||||
SuppressPlen int
|
||||
SuppressIFL int
|
||||
Invert bool
|
||||
}
|
||||
|
||||
const (
|
||||
// NetbirdVPNTableID is the ID of the custom routing table used by Netbird.
|
||||
NetbirdVPNTableID = 0x1BD0
|
||||
@@ -37,6 +57,8 @@ const (
|
||||
|
||||
var ErrTableIDExists = errors.New("ID exists with different name")
|
||||
|
||||
const errParsePrefixMsg = "failed to parse prefix %s: %w"
|
||||
|
||||
// originalSysctl stores the original sysctl values before they are modified
|
||||
var originalSysctl map[string]int
|
||||
|
||||
@@ -209,6 +231,277 @@ func GetRoutesFromTable() ([]netip.Prefix, error) {
|
||||
return append(v4Routes, v6Routes...), nil
|
||||
}
|
||||
|
||||
// GetDetailedRoutesFromTable returns detailed route information from all routing tables
|
||||
func GetDetailedRoutesFromTable() ([]DetailedRoute, error) {
|
||||
tables := discoverRoutingTables()
|
||||
return collectRoutesFromTables(tables), nil
|
||||
}
|
||||
|
||||
func discoverRoutingTables() []int {
|
||||
tables, err := getAllRoutingTables()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to get all routing tables, using fallback list: %v", err)
|
||||
return []int{
|
||||
syscall.RT_TABLE_MAIN,
|
||||
syscall.RT_TABLE_LOCAL,
|
||||
NetbirdVPNTableID,
|
||||
}
|
||||
}
|
||||
return tables
|
||||
}
|
||||
|
||||
func collectRoutesFromTables(tables []int) []DetailedRoute {
|
||||
var allRoutes []DetailedRoute
|
||||
|
||||
for _, tableID := range tables {
|
||||
routes := collectRoutesFromTable(tableID)
|
||||
allRoutes = append(allRoutes, routes...)
|
||||
}
|
||||
|
||||
return allRoutes
|
||||
}
|
||||
|
||||
func collectRoutesFromTable(tableID int) []DetailedRoute {
|
||||
var routes []DetailedRoute
|
||||
|
||||
if v4Routes := getRoutesForFamily(tableID, netlink.FAMILY_V4); len(v4Routes) > 0 {
|
||||
routes = append(routes, v4Routes...)
|
||||
}
|
||||
|
||||
if v6Routes := getRoutesForFamily(tableID, netlink.FAMILY_V6); len(v6Routes) > 0 {
|
||||
routes = append(routes, v6Routes...)
|
||||
}
|
||||
|
||||
return routes
|
||||
}
|
||||
|
||||
func getRoutesForFamily(tableID, family int) []DetailedRoute {
|
||||
routes, err := getDetailedRoutes(tableID, family)
|
||||
if err != nil {
|
||||
log.Debugf("Failed to get routes from table %d family %d: %v", tableID, family, err)
|
||||
return nil
|
||||
}
|
||||
return routes
|
||||
}
|
||||
|
||||
func getAllRoutingTables() ([]int, error) {
|
||||
tablesMap := make(map[int]bool)
|
||||
families := []int{netlink.FAMILY_V4, netlink.FAMILY_V6}
|
||||
|
||||
// Use table 0 (RT_TABLE_UNSPEC) to discover all tables
|
||||
for _, family := range families {
|
||||
routes, err := netlink.RouteListFiltered(family, &netlink.Route{Table: 0}, netlink.RT_FILTER_TABLE)
|
||||
if err != nil {
|
||||
log.Debugf("Failed to list routes from table 0 for family %d: %v", family, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract unique table IDs from all routes
|
||||
for _, route := range routes {
|
||||
if route.Table > 0 {
|
||||
tablesMap[route.Table] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var tables []int
|
||||
for tableID := range tablesMap {
|
||||
tables = append(tables, tableID)
|
||||
}
|
||||
|
||||
standardTables := []int{syscall.RT_TABLE_MAIN, syscall.RT_TABLE_LOCAL, NetbirdVPNTableID}
|
||||
for _, table := range standardTables {
|
||||
if !tablesMap[table] {
|
||||
tables = append(tables, table)
|
||||
}
|
||||
}
|
||||
|
||||
return tables, nil
|
||||
}
|
||||
|
||||
// getDetailedRoutes fetches detailed routes from a specific routing table
|
||||
func getDetailedRoutes(tableID, family int) ([]DetailedRoute, error) {
|
||||
var detailedRoutes []DetailedRoute
|
||||
|
||||
routes, err := netlink.RouteListFiltered(family, &netlink.Route{Table: tableID}, netlink.RT_FILTER_TABLE)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list routes from table %d: %v", tableID, err)
|
||||
}
|
||||
|
||||
for _, route := range routes {
|
||||
detailed := buildDetailedRoute(route, tableID, family)
|
||||
if detailed != nil {
|
||||
detailedRoutes = append(detailedRoutes, *detailed)
|
||||
}
|
||||
}
|
||||
|
||||
return detailedRoutes, nil
|
||||
}
|
||||
|
||||
func buildDetailedRoute(route netlink.Route, tableID, family int) *DetailedRoute {
|
||||
detailed := DetailedRoute{
|
||||
Route: Route{},
|
||||
Metric: route.Priority,
|
||||
InterfaceMetric: -1, // Interface metrics not typically used on Linux
|
||||
InterfaceIndex: route.LinkIndex,
|
||||
Protocol: routeProtocolToString(int(route.Protocol)),
|
||||
Scope: routeScopeToString(route.Scope),
|
||||
Type: routeTypeToString(route.Type),
|
||||
Table: routeTableToString(tableID),
|
||||
Flags: "-",
|
||||
}
|
||||
|
||||
if !processRouteDestination(&detailed, route, family) {
|
||||
return nil
|
||||
}
|
||||
|
||||
processRouteGateway(&detailed, route)
|
||||
|
||||
processRouteInterface(&detailed, route)
|
||||
|
||||
return &detailed
|
||||
}
|
||||
|
||||
func processRouteDestination(detailed *DetailedRoute, route netlink.Route, family int) bool {
|
||||
if route.Dst != nil {
|
||||
addr, ok := netip.AddrFromSlice(route.Dst.IP)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
ones, _ := route.Dst.Mask.Size()
|
||||
prefix := netip.PrefixFrom(addr.Unmap(), ones)
|
||||
if prefix.IsValid() {
|
||||
detailed.Route.Dst = prefix
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if family == netlink.FAMILY_V4 {
|
||||
detailed.Route.Dst = netip.MustParsePrefix("0.0.0.0/0")
|
||||
} else {
|
||||
detailed.Route.Dst = netip.MustParsePrefix("::/0")
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func processRouteGateway(detailed *DetailedRoute, route netlink.Route) {
|
||||
if route.Gw != nil {
|
||||
if gateway, ok := netip.AddrFromSlice(route.Gw); ok {
|
||||
detailed.Route.Gw = gateway.Unmap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func processRouteInterface(detailed *DetailedRoute, route netlink.Route) {
|
||||
if route.LinkIndex > 0 {
|
||||
if link, err := netlink.LinkByIndex(route.LinkIndex); err == nil {
|
||||
detailed.Route.Interface = &net.Interface{
|
||||
Index: link.Attrs().Index,
|
||||
Name: link.Attrs().Name,
|
||||
}
|
||||
} else {
|
||||
detailed.Route.Interface = &net.Interface{
|
||||
Index: route.LinkIndex,
|
||||
Name: fmt.Sprintf("index-%d", route.LinkIndex),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions to convert netlink constants to strings
|
||||
func routeProtocolToString(protocol int) string {
|
||||
switch protocol {
|
||||
case syscall.RTPROT_UNSPEC:
|
||||
return "unspec"
|
||||
case syscall.RTPROT_REDIRECT:
|
||||
return "redirect"
|
||||
case syscall.RTPROT_KERNEL:
|
||||
return "kernel"
|
||||
case syscall.RTPROT_BOOT:
|
||||
return "boot"
|
||||
case syscall.RTPROT_STATIC:
|
||||
return "static"
|
||||
case syscall.RTPROT_DHCP:
|
||||
return "dhcp"
|
||||
case unix.RTPROT_RA:
|
||||
return "ra"
|
||||
case unix.RTPROT_ZEBRA:
|
||||
return "zebra"
|
||||
case unix.RTPROT_BIRD:
|
||||
return "bird"
|
||||
case unix.RTPROT_DNROUTED:
|
||||
return "dnrouted"
|
||||
case unix.RTPROT_XORP:
|
||||
return "xorp"
|
||||
case unix.RTPROT_NTK:
|
||||
return "ntk"
|
||||
default:
|
||||
return fmt.Sprintf("%d", protocol)
|
||||
}
|
||||
}
|
||||
|
||||
func routeScopeToString(scope netlink.Scope) string {
|
||||
switch scope {
|
||||
case netlink.SCOPE_UNIVERSE:
|
||||
return "global"
|
||||
case netlink.SCOPE_SITE:
|
||||
return "site"
|
||||
case netlink.SCOPE_LINK:
|
||||
return "link"
|
||||
case netlink.SCOPE_HOST:
|
||||
return "host"
|
||||
case netlink.SCOPE_NOWHERE:
|
||||
return "nowhere"
|
||||
default:
|
||||
return fmt.Sprintf("%d", scope)
|
||||
}
|
||||
}
|
||||
|
||||
func routeTypeToString(routeType int) string {
|
||||
switch routeType {
|
||||
case syscall.RTN_UNSPEC:
|
||||
return "unspec"
|
||||
case syscall.RTN_UNICAST:
|
||||
return "unicast"
|
||||
case syscall.RTN_LOCAL:
|
||||
return "local"
|
||||
case syscall.RTN_BROADCAST:
|
||||
return "broadcast"
|
||||
case syscall.RTN_ANYCAST:
|
||||
return "anycast"
|
||||
case syscall.RTN_MULTICAST:
|
||||
return "multicast"
|
||||
case syscall.RTN_BLACKHOLE:
|
||||
return "blackhole"
|
||||
case syscall.RTN_UNREACHABLE:
|
||||
return "unreachable"
|
||||
case syscall.RTN_PROHIBIT:
|
||||
return "prohibit"
|
||||
case syscall.RTN_THROW:
|
||||
return "throw"
|
||||
case syscall.RTN_NAT:
|
||||
return "nat"
|
||||
case syscall.RTN_XRESOLVE:
|
||||
return "xresolve"
|
||||
default:
|
||||
return fmt.Sprintf("%d", routeType)
|
||||
}
|
||||
}
|
||||
|
||||
func routeTableToString(tableID int) string {
|
||||
switch tableID {
|
||||
case syscall.RT_TABLE_MAIN:
|
||||
return "main"
|
||||
case syscall.RT_TABLE_LOCAL:
|
||||
return "local"
|
||||
case NetbirdVPNTableID:
|
||||
return "netbird"
|
||||
default:
|
||||
return fmt.Sprintf("%d", tableID)
|
||||
}
|
||||
}
|
||||
|
||||
// getRoutes fetches routes from a specific routing table identified by tableID.
|
||||
func getRoutes(tableID, family int) ([]netip.Prefix, error) {
|
||||
var prefixList []netip.Prefix
|
||||
@@ -237,6 +530,115 @@ func getRoutes(tableID, family int) ([]netip.Prefix, error) {
|
||||
return prefixList, nil
|
||||
}
|
||||
|
||||
// GetIPRules returns IP rules for debugging
|
||||
func GetIPRules() ([]IPRule, error) {
|
||||
v4Rules, err := getIPRules(netlink.FAMILY_V4)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get v4 rules: %w", err)
|
||||
}
|
||||
v6Rules, err := getIPRules(netlink.FAMILY_V6)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get v6 rules: %w", err)
|
||||
}
|
||||
return append(v4Rules, v6Rules...), nil
|
||||
}
|
||||
|
||||
// getIPRules fetches IP rules for the specified address family
|
||||
func getIPRules(family int) ([]IPRule, error) {
|
||||
rules, err := netlink.RuleList(family)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list rules for family %d: %w", family, err)
|
||||
}
|
||||
|
||||
var ipRules []IPRule
|
||||
for _, rule := range rules {
|
||||
ipRule := buildIPRule(rule)
|
||||
ipRules = append(ipRules, ipRule)
|
||||
}
|
||||
|
||||
return ipRules, nil
|
||||
}
|
||||
|
||||
func buildIPRule(rule netlink.Rule) IPRule {
|
||||
var mask uint32
|
||||
if rule.Mask != nil {
|
||||
mask = *rule.Mask
|
||||
}
|
||||
|
||||
ipRule := IPRule{
|
||||
Priority: rule.Priority,
|
||||
IIF: rule.IifName,
|
||||
OIF: rule.OifName,
|
||||
Table: ruleTableToString(rule.Table),
|
||||
Action: ruleActionToString(int(rule.Type)),
|
||||
Mark: rule.Mark,
|
||||
Mask: mask,
|
||||
TunID: uint32(rule.TunID),
|
||||
Goto: uint32(rule.Goto),
|
||||
Flow: uint32(rule.Flow),
|
||||
SuppressPlen: rule.SuppressPrefixlen,
|
||||
SuppressIFL: rule.SuppressIfgroup,
|
||||
Invert: rule.Invert,
|
||||
}
|
||||
|
||||
if rule.Src != nil {
|
||||
ipRule.From = parseRulePrefix(rule.Src)
|
||||
}
|
||||
|
||||
if rule.Dst != nil {
|
||||
ipRule.To = parseRulePrefix(rule.Dst)
|
||||
}
|
||||
|
||||
return ipRule
|
||||
}
|
||||
|
||||
func parseRulePrefix(ipNet *net.IPNet) netip.Prefix {
|
||||
if addr, ok := netip.AddrFromSlice(ipNet.IP); ok {
|
||||
ones, _ := ipNet.Mask.Size()
|
||||
prefix := netip.PrefixFrom(addr.Unmap(), ones)
|
||||
if prefix.IsValid() {
|
||||
return prefix
|
||||
}
|
||||
}
|
||||
return netip.Prefix{}
|
||||
}
|
||||
|
||||
func ruleTableToString(table int) string {
|
||||
switch table {
|
||||
case syscall.RT_TABLE_MAIN:
|
||||
return "main"
|
||||
case syscall.RT_TABLE_LOCAL:
|
||||
return "local"
|
||||
case syscall.RT_TABLE_DEFAULT:
|
||||
return "default"
|
||||
case NetbirdVPNTableID:
|
||||
return "netbird"
|
||||
default:
|
||||
return fmt.Sprintf("%d", table)
|
||||
}
|
||||
}
|
||||
|
||||
func ruleActionToString(action int) string {
|
||||
switch action {
|
||||
case unix.FR_ACT_UNSPEC:
|
||||
return "unspec"
|
||||
case unix.FR_ACT_TO_TBL:
|
||||
return "lookup"
|
||||
case unix.FR_ACT_GOTO:
|
||||
return "goto"
|
||||
case unix.FR_ACT_NOP:
|
||||
return "nop"
|
||||
case unix.FR_ACT_BLACKHOLE:
|
||||
return "blackhole"
|
||||
case unix.FR_ACT_UNREACHABLE:
|
||||
return "unreachable"
|
||||
case unix.FR_ACT_PROHIBIT:
|
||||
return "prohibit"
|
||||
default:
|
||||
return fmt.Sprintf("%d", action)
|
||||
}
|
||||
}
|
||||
|
||||
// addRoute adds a route to a specific routing table identified by tableID.
|
||||
func addRoute(prefix netip.Prefix, nexthop Nexthop, tableID int) error {
|
||||
route := &netlink.Route{
|
||||
@@ -247,7 +649,7 @@ func addRoute(prefix netip.Prefix, nexthop Nexthop, tableID int) error {
|
||||
|
||||
_, ipNet, err := net.ParseCIDR(prefix.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse prefix %s: %w", prefix, err)
|
||||
return fmt.Errorf(errParsePrefixMsg, prefix, err)
|
||||
}
|
||||
route.Dst = ipNet
|
||||
|
||||
@@ -268,7 +670,7 @@ func addRoute(prefix netip.Prefix, nexthop Nexthop, tableID int) error {
|
||||
func addUnreachableRoute(prefix netip.Prefix, tableID int) error {
|
||||
_, ipNet, err := net.ParseCIDR(prefix.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse prefix %s: %w", prefix, err)
|
||||
return fmt.Errorf(errParsePrefixMsg, prefix, err)
|
||||
}
|
||||
|
||||
route := &netlink.Route{
|
||||
@@ -288,7 +690,7 @@ func addUnreachableRoute(prefix netip.Prefix, tableID int) error {
|
||||
func removeUnreachableRoute(prefix netip.Prefix, tableID int) error {
|
||||
_, ipNet, err := net.ParseCIDR(prefix.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse prefix %s: %w", prefix, err)
|
||||
return fmt.Errorf(errParsePrefixMsg, prefix, err)
|
||||
}
|
||||
|
||||
route := &netlink.Route{
|
||||
@@ -313,7 +715,7 @@ func removeUnreachableRoute(prefix netip.Prefix, tableID int) error {
|
||||
func removeRoute(prefix netip.Prefix, nexthop Nexthop, tableID int) error {
|
||||
_, ipNet, err := net.ParseCIDR(prefix.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse prefix %s: %w", prefix, err)
|
||||
return fmt.Errorf(errParsePrefixMsg, prefix, err)
|
||||
}
|
||||
|
||||
route := &netlink.Route{
|
||||
|
Reference in New Issue
Block a user