[client] Add detailed routes and resolved IPs to debug bundle (#4141)

This commit is contained in:
Viktor Liu
2025-07-25 15:31:06 +02:00
committed by GitHub
parent 2c4ac33b38
commit e0d9306b05
15 changed files with 1501 additions and 165 deletions

View File

@ -40,10 +40,12 @@ status.txt: Anonymized status information of the NetBird client.
client.log: Most recent, anonymized client log file of the NetBird client. client.log: Most recent, anonymized client log file of the NetBird client.
netbird.err: Most recent, anonymized stderr log file of the NetBird client. netbird.err: Most recent, anonymized stderr log file of the NetBird client.
netbird.out: Most recent, anonymized stdout log file of the NetBird client. netbird.out: Most recent, anonymized stdout log file of the NetBird client.
routes.txt: Anonymized system routes, if --system-info flag was provided. routes.txt: Detailed system routing table in tabular format including destination, gateway, interface, metrics, and protocol information, if --system-info flag was provided.
interfaces.txt: Anonymized network interface information, if --system-info flag was provided. interfaces.txt: Anonymized network interface information, if --system-info flag was provided.
ip_rules.txt: Detailed IP routing rules in tabular format including priority, source, destination, interfaces, table, and action information (Linux only), if --system-info flag was provided.
iptables.txt: Anonymized iptables rules with packet counters, if --system-info flag was provided. iptables.txt: Anonymized iptables rules with packet counters, if --system-info flag was provided.
nftables.txt: Anonymized nftables rules with packet counters, if --system-info flag was provided. nftables.txt: Anonymized nftables rules with packet counters, if --system-info flag was provided.
resolved_domains.txt: Anonymized resolved domain IP addresses from the status recorder.
config.txt: Anonymized configuration information of the NetBird client. config.txt: Anonymized configuration information of the NetBird client.
network_map.json: Anonymized network map containing peer configurations, routes, DNS settings, and firewall rules. network_map.json: Anonymized network map containing peer configurations, routes, DNS settings, and firewall rules.
state.json: Anonymized client state dump containing netbird states. state.json: Anonymized client state dump containing netbird states.
@ -107,7 +109,29 @@ go tool pprof -http=:8088 heap.prof
This will open a web browser tab with the profiling information. This will open a web browser tab with the profiling information.
Routes Routes
For anonymized routes, the IP addresses are replaced as described above. The prefix length remains unchanged. Note that for prefixes, the anonymized IP might not be a network address, but the prefix length is still correct. The routes.txt file contains detailed routing table information in a tabular format:
- Destination: Network prefix (IP_ADDRESS/PREFIX_LENGTH)
- Gateway: Next hop IP address (or "-" if direct)
- Interface: Network interface name
- Metric: Route priority/metric (lower values preferred)
- Protocol: Routing protocol (kernel, static, dhcp, etc.)
- Scope: Route scope (global, link, host, etc.)
- Type: Route type (unicast, local, broadcast, etc.)
- Table: Routing table name (main, local, netbird, etc.)
The table format provides a comprehensive view of the system's routing configuration, including information from multiple routing tables on Linux systems. This is valuable for troubleshooting routing issues and understanding traffic flow.
For anonymized routes, IP addresses are replaced as described above. The prefix length remains unchanged. Note that for prefixes, the anonymized IP might not be a network address, but the prefix length is still correct. Interface names are anonymized using string anonymization.
Resolved Domains
The resolved_domains.txt file contains information about domain names that have been resolved to IP addresses by NetBird's DNS resolver. This includes:
- Original domain patterns that were configured for routing
- Resolved domain names that matched those patterns
- IP address prefixes that were resolved for each domain
- Parent domain associations showing which original pattern each resolved domain belongs to
All domain names and IP addresses in this file follow the same anonymization rules as described above. This information is valuable for troubleshooting DNS resolution and routing issues.
Network Interfaces Network Interfaces
The interfaces.txt file contains information about network interfaces, including: The interfaces.txt file contains information about network interfaces, including:
@ -145,6 +169,22 @@ nftables.txt:
- Shows packet and byte counters for each rule - Shows packet and byte counters for each rule
- All IP addresses are anonymized - All IP addresses are anonymized
- Chain names, table names, and other non-sensitive information remain unchanged - Chain names, table names, and other non-sensitive information remain unchanged
IP Rules (Linux only)
The ip_rules.txt file contains detailed IP routing rule information:
- Priority: Rule priority number (lower values processed first)
- From: Source IP prefix or "all" if unspecified
- To: Destination IP prefix or "all" if unspecified
- IIF: Input interface name or "-" if unspecified
- OIF: Output interface name or "-" if unspecified
- Table: Target routing table name (main, local, netbird, etc.)
- Action: Rule action (lookup, goto, blackhole, etc.)
- Mark: Firewall mark value in hex format or "-" if unspecified
The table format provides comprehensive visibility into the IP routing decision process, including how traffic is directed to different routing tables based on various criteria. This is valuable for troubleshooting advanced routing configurations and policy-based routing.
For anonymized rules, IP addresses and prefixes are replaced as described above. Interface names are anonymized using string anonymization. Table names, actions, and other non-sensitive information remain unchanged.
` `
const ( const (
@ -159,13 +199,11 @@ const (
type BundleGenerator struct { type BundleGenerator struct {
anonymizer *anonymize.Anonymizer anonymizer *anonymize.Anonymizer
// deps
internalConfig *internal.Config internalConfig *internal.Config
statusRecorder *peer.Status statusRecorder *peer.Status
networkMap *mgmProto.NetworkMap networkMap *mgmProto.NetworkMap
logFile string logFile string
// config
anonymize bool anonymize bool
clientStatus string clientStatus string
includeSystemInfo bool includeSystemInfo bool
@ -258,7 +296,11 @@ func (g *BundleGenerator) createArchive() error {
} }
if err := g.addConfig(); err != nil { if err := g.addConfig(); err != nil {
log.Errorf("Failed to add config to debug bundle: %v", err) log.Errorf("failed to add config to debug bundle: %v", err)
}
if err := g.addResolvedDomains(); err != nil {
log.Errorf("failed to add resolved domains to debug bundle: %v", err)
} }
if g.includeSystemInfo { if g.includeSystemInfo {
@ -266,7 +308,7 @@ func (g *BundleGenerator) createArchive() error {
} }
if err := g.addProf(); err != nil { if err := g.addProf(); err != nil {
log.Errorf("Failed to add profiles to debug bundle: %v", err) log.Errorf("failed to add profiles to debug bundle: %v", err)
} }
if err := g.addNetworkMap(); err != nil { if err := g.addNetworkMap(); err != nil {
@ -274,26 +316,26 @@ func (g *BundleGenerator) createArchive() error {
} }
if err := g.addStateFile(); err != nil { if err := g.addStateFile(); err != nil {
log.Errorf("Failed to add state file to debug bundle: %v", err) log.Errorf("failed to add state file to debug bundle: %v", err)
} }
if err := g.addCorruptedStateFiles(); err != nil { if err := g.addCorruptedStateFiles(); err != nil {
log.Errorf("Failed to add corrupted state files to debug bundle: %v", err) log.Errorf("failed to add corrupted state files to debug bundle: %v", err)
} }
if err := g.addWgShow(); err != nil { if err := g.addWgShow(); err != nil {
log.Errorf("Failed to add wg show output: %v", err) log.Errorf("failed to add wg show output: %v", err)
} }
if g.logFile != "" && !slices.Contains(util.SpecialLogs, g.logFile) { if g.logFile != "" && !slices.Contains(util.SpecialLogs, g.logFile) {
if err := g.addLogfile(); err != nil { if err := g.addLogfile(); err != nil {
log.Errorf("Failed to add log file to debug bundle: %v", err) log.Errorf("failed to add log file to debug bundle: %v", err)
if err := g.trySystemdLogFallback(); err != nil { if err := g.trySystemdLogFallback(); err != nil {
log.Errorf("Failed to add systemd logs as fallback: %v", err) log.Errorf("failed to add systemd logs as fallback: %v", err)
} }
} }
} else if err := g.trySystemdLogFallback(); err != nil { } else if err := g.trySystemdLogFallback(); err != nil {
log.Errorf("Failed to add systemd logs: %v", err) log.Errorf("failed to add systemd logs: %v", err)
} }
return nil return nil
@ -301,15 +343,19 @@ func (g *BundleGenerator) createArchive() error {
func (g *BundleGenerator) addSystemInfo() { func (g *BundleGenerator) addSystemInfo() {
if err := g.addRoutes(); err != nil { if err := g.addRoutes(); err != nil {
log.Errorf("Failed to add routes to debug bundle: %v", err) log.Errorf("failed to add routes to debug bundle: %v", err)
} }
if err := g.addInterfaces(); err != nil { if err := g.addInterfaces(); err != nil {
log.Errorf("Failed to add interfaces to debug bundle: %v", err) log.Errorf("failed to add interfaces to debug bundle: %v", err)
}
if err := g.addIPRules(); err != nil {
log.Errorf("failed to add IP rules to debug bundle: %v", err)
} }
if err := g.addFirewallRules(); err != nil { if err := g.addFirewallRules(); err != nil {
log.Errorf("Failed to add firewall rules to debug bundle: %v", err) log.Errorf("failed to add firewall rules to debug bundle: %v", err)
} }
} }
@ -364,7 +410,6 @@ func (g *BundleGenerator) addConfig() error {
} }
} }
// Add config content to zip file
configReader := strings.NewReader(configContent.String()) configReader := strings.NewReader(configContent.String())
if err := g.addFileToZip(configReader, "config.txt"); err != nil { if err := g.addFileToZip(configReader, "config.txt"); err != nil {
return fmt.Errorf("add config file to zip: %w", err) return fmt.Errorf("add config file to zip: %w", err)
@ -376,7 +421,6 @@ func (g *BundleGenerator) addConfig() error {
func (g *BundleGenerator) addCommonConfigFields(configContent *strings.Builder) { func (g *BundleGenerator) addCommonConfigFields(configContent *strings.Builder) {
configContent.WriteString("NetBird Client Configuration:\n\n") configContent.WriteString("NetBird Client Configuration:\n\n")
// Add non-sensitive fields
configContent.WriteString(fmt.Sprintf("WgIface: %s\n", g.internalConfig.WgIface)) configContent.WriteString(fmt.Sprintf("WgIface: %s\n", g.internalConfig.WgIface))
configContent.WriteString(fmt.Sprintf("WgPort: %d\n", g.internalConfig.WgPort)) configContent.WriteString(fmt.Sprintf("WgPort: %d\n", g.internalConfig.WgPort))
if g.internalConfig.NetworkMonitor != nil { if g.internalConfig.NetworkMonitor != nil {
@ -461,6 +505,27 @@ func (g *BundleGenerator) addInterfaces() error {
return nil return nil
} }
func (g *BundleGenerator) addResolvedDomains() error {
if g.statusRecorder == nil {
log.Debugf("skipping resolved domains in debug bundle: no status recorder")
return nil
}
resolvedDomains := g.statusRecorder.GetResolvedDomainsStates()
if len(resolvedDomains) == 0 {
log.Debugf("skipping resolved domains in debug bundle: no resolved domains")
return nil
}
resolvedDomainsContent := formatResolvedDomains(resolvedDomains, g.anonymize, g.anonymizer)
resolvedDomainsReader := strings.NewReader(resolvedDomainsContent)
if err := g.addFileToZip(resolvedDomainsReader, "resolved_domains.txt"); err != nil {
return fmt.Errorf("add resolved domains file to zip: %w", err)
}
return nil
}
func (g *BundleGenerator) addNetworkMap() error { func (g *BundleGenerator) addNetworkMap() error {
if g.networkMap == nil { if g.networkMap == nil {
log.Debugf("skipping empty network map in debug bundle") log.Debugf("skipping empty network map in debug bundle")
@ -572,7 +637,6 @@ func (g *BundleGenerator) addLogfile() error {
return fmt.Errorf("add client log file to zip: %w", err) return fmt.Errorf("add client log file to zip: %w", err)
} }
// add rotated log files based on logFileCount
g.addRotatedLogFiles(logDir) g.addRotatedLogFiles(logDir)
stdErrLogPath := filepath.Join(logDir, errorLogFile) stdErrLogPath := filepath.Join(logDir, errorLogFile)
@ -601,7 +665,7 @@ func (g *BundleGenerator) addSingleLogfile(logPath, targetName string) error {
} }
defer func() { defer func() {
if err := logFile.Close(); err != nil { if err := logFile.Close(); err != nil {
log.Errorf("Failed to close log file %s: %v", targetName, err) log.Errorf("failed to close log file %s: %v", targetName, err)
} }
}() }()
@ -625,13 +689,21 @@ func (g *BundleGenerator) addSingleLogFileGz(logPath, targetName string) error {
if err != nil { if err != nil {
return fmt.Errorf("open gz log file %s: %w", targetName, err) return fmt.Errorf("open gz log file %s: %w", targetName, err)
} }
defer f.Close() defer func() {
if err := f.Close(); err != nil {
log.Errorf("failed to close gz file %s: %v", targetName, err)
}
}()
gzr, err := gzip.NewReader(f) gzr, err := gzip.NewReader(f)
if err != nil { if err != nil {
return fmt.Errorf("create gzip reader: %w", err) return fmt.Errorf("create gzip reader: %w", err)
} }
defer gzr.Close() defer func() {
if err := gzr.Close(); err != nil {
log.Errorf("failed to close gzip reader %s: %v", targetName, err)
}
}()
var logReader io.Reader = gzr var logReader io.Reader = gzr
if g.anonymize { if g.anonymize {
@ -689,7 +761,6 @@ func (g *BundleGenerator) addRotatedLogFiles(logDir string) {
return fi.ModTime().After(fj.ModTime()) return fi.ModTime().After(fj.ModTime())
}) })
// include up to logFileCount rotated files
maxFiles := int(g.logFileCount) maxFiles := int(g.logFileCount)
if maxFiles > len(files) { if maxFiles > len(files) {
maxFiles = len(files) maxFiles = len(files)
@ -717,7 +788,7 @@ func (g *BundleGenerator) addFileToZip(reader io.Reader, filename string) error
// If the reader is a file, we can get more accurate information // If the reader is a file, we can get more accurate information
if f, ok := reader.(*os.File); ok { if f, ok := reader.(*os.File); ok {
if stat, err := f.Stat(); err != nil { if stat, err := f.Stat(); err != nil {
log.Tracef("Failed to get file stat for %s: %v", filename, err) log.Tracef("failed to get file stat for %s: %v", filename, err)
} else { } else {
header.Modified = stat.ModTime() header.Modified = stat.ModTime()
} }
@ -765,89 +836,6 @@ func seedFromStatus(a *anonymize.Anonymizer, status *peer.FullStatus) {
} }
} }
func formatRoutes(routes []netip.Prefix, anonymize bool, anonymizer *anonymize.Anonymizer) string {
var ipv4Routes, ipv6Routes []netip.Prefix
// Separate IPv4 and IPv6 routes
for _, route := range routes {
if route.Addr().Is4() {
ipv4Routes = append(ipv4Routes, route)
} else {
ipv6Routes = append(ipv6Routes, route)
}
}
// Sort IPv4 and IPv6 routes separately
sort.Slice(ipv4Routes, func(i, j int) bool {
return ipv4Routes[i].Bits() > ipv4Routes[j].Bits()
})
sort.Slice(ipv6Routes, func(i, j int) bool {
return ipv6Routes[i].Bits() > ipv6Routes[j].Bits()
})
var builder strings.Builder
// Format IPv4 routes
builder.WriteString("IPv4 Routes:\n")
for _, route := range ipv4Routes {
formatRoute(&builder, route, anonymize, anonymizer)
}
// Format IPv6 routes
builder.WriteString("\nIPv6 Routes:\n")
for _, route := range ipv6Routes {
formatRoute(&builder, route, anonymize, anonymizer)
}
return builder.String()
}
func formatRoute(builder *strings.Builder, route netip.Prefix, anonymize bool, anonymizer *anonymize.Anonymizer) {
if anonymize {
anonymizedIP := anonymizer.AnonymizeIP(route.Addr())
builder.WriteString(fmt.Sprintf("%s/%d\n", anonymizedIP, route.Bits()))
} else {
builder.WriteString(fmt.Sprintf("%s\n", route))
}
}
func formatInterfaces(interfaces []net.Interface, anonymize bool, anonymizer *anonymize.Anonymizer) string {
sort.Slice(interfaces, func(i, j int) bool {
return interfaces[i].Name < interfaces[j].Name
})
var builder strings.Builder
builder.WriteString("Network Interfaces:\n")
for _, iface := range interfaces {
builder.WriteString(fmt.Sprintf("\nInterface: %s\n", iface.Name))
builder.WriteString(fmt.Sprintf(" Index: %d\n", iface.Index))
builder.WriteString(fmt.Sprintf(" MTU: %d\n", iface.MTU))
builder.WriteString(fmt.Sprintf(" Flags: %v\n", iface.Flags))
addrs, err := iface.Addrs()
if err != nil {
builder.WriteString(fmt.Sprintf(" Addresses: Error retrieving addresses: %v\n", err))
} else {
builder.WriteString(" Addresses:\n")
for _, addr := range addrs {
prefix, err := netip.ParsePrefix(addr.String())
if err != nil {
builder.WriteString(fmt.Sprintf(" Error parsing address: %v\n", err))
continue
}
ip := prefix.Addr()
if anonymize {
ip = anonymizer.AnonymizeIP(ip)
}
builder.WriteString(fmt.Sprintf(" %s/%d\n", ip, prefix.Bits()))
}
}
}
return builder.String()
}
func anonymizeLog(reader io.Reader, writer *io.PipeWriter, anonymizer *anonymize.Anonymizer) { func anonymizeLog(reader io.Reader, writer *io.PipeWriter, anonymizer *anonymize.Anonymizer) {
defer func() { defer func() {
// always nil // always nil
@ -954,7 +942,6 @@ func anonymizeRemotePeer(peer *mgmProto.RemotePeerConfig, anonymizer *anonymize.
} }
for i, ip := range peer.AllowedIps { for i, ip := range peer.AllowedIps {
// Try to parse as prefix first (CIDR)
if prefix, err := netip.ParsePrefix(ip); err == nil { if prefix, err := netip.ParsePrefix(ip); err == nil {
anonIP := anonymizer.AnonymizeIP(prefix.Addr()) anonIP := anonymizer.AnonymizeIP(prefix.Addr())
peer.AllowedIps[i] = fmt.Sprintf("%s/%d", anonIP, prefix.Bits()) peer.AllowedIps[i] = fmt.Sprintf("%s/%d", anonIP, prefix.Bits())
@ -1033,7 +1020,7 @@ func anonymizeRecords(records []*mgmProto.SimpleRecord, anonymizer *anonymize.An
func anonymizeRData(record *mgmProto.SimpleRecord, anonymizer *anonymize.Anonymizer) { func anonymizeRData(record *mgmProto.SimpleRecord, anonymizer *anonymize.Anonymizer) {
switch record.Type { switch record.Type {
case 1, 28: // A or AAAA record case 1, 28:
if addr, err := netip.ParseAddr(record.RData); err == nil { if addr, err := netip.ParseAddr(record.RData); err == nil {
record.RData = anonymizer.AnonymizeIP(addr).String() record.RData = anonymizer.AnonymizeIP(addr).String()
} }

View File

@ -17,8 +17,27 @@ import (
"github.com/google/nftables" "github.com/google/nftables"
"github.com/google/nftables/expr" "github.com/google/nftables/expr"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
) )
// addIPRules collects and adds IP rules to the archive
func (g *BundleGenerator) addIPRules() error {
log.Info("Collecting IP rules")
ipRules, err := systemops.GetIPRules()
if err != nil {
return fmt.Errorf("get IP rules: %w", err)
}
rulesContent := formatIPRulesTable(ipRules, g.anonymize, g.anonymizer)
rulesReader := strings.NewReader(rulesContent)
if err := g.addFileToZip(rulesReader, "ip_rules.txt"); err != nil {
return fmt.Errorf("add IP rules file to zip: %w", err)
}
return nil
}
const ( const (
maxLogEntries = 100000 maxLogEntries = 100000
maxLogAge = 7 * 24 * time.Hour // Last 7 days maxLogAge = 7 * 24 * time.Hour // Last 7 days
@ -136,7 +155,6 @@ func (g *BundleGenerator) addFirewallRules() error {
func collectIPTablesRules() (string, error) { func collectIPTablesRules() (string, error) {
var builder strings.Builder var builder strings.Builder
// First try using iptables-save
saveOutput, err := collectIPTablesSave() saveOutput, err := collectIPTablesSave()
if err != nil { if err != nil {
log.Warnf("Failed to collect iptables rules using iptables-save: %v", err) log.Warnf("Failed to collect iptables rules using iptables-save: %v", err)
@ -146,7 +164,6 @@ func collectIPTablesRules() (string, error) {
builder.WriteString("\n") builder.WriteString("\n")
} }
// Collect ipset information
ipsetOutput, err := collectIPSets() ipsetOutput, err := collectIPSets()
if err != nil { if err != nil {
log.Warnf("Failed to collect ipset information: %v", err) log.Warnf("Failed to collect ipset information: %v", err)
@ -232,11 +249,9 @@ func getTableStatistics(table string) (string, error) {
// collectNFTablesRules attempts to collect nftables rules using either nft command or netlink // collectNFTablesRules attempts to collect nftables rules using either nft command or netlink
func collectNFTablesRules() (string, error) { func collectNFTablesRules() (string, error) {
// First try using nft command
rules, err := collectNFTablesFromCommand() rules, err := collectNFTablesFromCommand()
if err != nil { if err != nil {
log.Debugf("Failed to collect nftables rules using nft command: %v, falling back to netlink", err) log.Debugf("Failed to collect nftables rules using nft command: %v, falling back to netlink", err)
// Fall back to netlink
rules, err = collectNFTablesFromNetlink() rules, err = collectNFTablesFromNetlink()
if err != nil { if err != nil {
return "", fmt.Errorf("collect nftables rules using both nft and netlink failed: %w", err) return "", fmt.Errorf("collect nftables rules using both nft and netlink failed: %w", err)
@ -451,7 +466,6 @@ func formatRule(rule *nftables.Rule) string {
func formatExprSequence(builder *strings.Builder, exprs []expr.Any, i int) int { func formatExprSequence(builder *strings.Builder, exprs []expr.Any, i int) int {
curr := exprs[i] curr := exprs[i]
// Handle Meta + Cmp sequence
if meta, ok := curr.(*expr.Meta); ok && i+1 < len(exprs) { if meta, ok := curr.(*expr.Meta); ok && i+1 < len(exprs) {
if cmp, ok := exprs[i+1].(*expr.Cmp); ok { if cmp, ok := exprs[i+1].(*expr.Cmp); ok {
if formatted := formatMetaWithCmp(meta, cmp); formatted != "" { if formatted := formatMetaWithCmp(meta, cmp); formatted != "" {
@ -461,7 +475,6 @@ func formatExprSequence(builder *strings.Builder, exprs []expr.Any, i int) int {
} }
} }
// Handle Payload + Cmp sequence
if payload, ok := curr.(*expr.Payload); ok && i+1 < len(exprs) { if payload, ok := curr.(*expr.Payload); ok && i+1 < len(exprs) {
if cmp, ok := exprs[i+1].(*expr.Cmp); ok { if cmp, ok := exprs[i+1].(*expr.Cmp); ok {
builder.WriteString(formatPayloadWithCmp(payload, cmp)) builder.WriteString(formatPayloadWithCmp(payload, cmp))
@ -493,13 +506,13 @@ func formatMetaWithCmp(meta *expr.Meta, cmp *expr.Cmp) string {
func formatPayloadWithCmp(p *expr.Payload, cmp *expr.Cmp) string { func formatPayloadWithCmp(p *expr.Payload, cmp *expr.Cmp) string {
if p.Base == expr.PayloadBaseNetworkHeader { if p.Base == expr.PayloadBaseNetworkHeader {
switch p.Offset { switch p.Offset {
case 12: // Source IP case 12:
if p.Len == 4 { if p.Len == 4 {
return fmt.Sprintf("ip saddr %s %s", formatCmpOp(cmp.Op), formatIPBytes(cmp.Data)) return fmt.Sprintf("ip saddr %s %s", formatCmpOp(cmp.Op), formatIPBytes(cmp.Data))
} else if p.Len == 2 { } else if p.Len == 2 {
return fmt.Sprintf("ip saddr %s %s", formatCmpOp(cmp.Op), formatIPBytes(cmp.Data)) return fmt.Sprintf("ip saddr %s %s", formatCmpOp(cmp.Op), formatIPBytes(cmp.Data))
} }
case 16: // Destination IP case 16:
if p.Len == 4 { if p.Len == 4 {
return fmt.Sprintf("ip daddr %s %s", formatCmpOp(cmp.Op), formatIPBytes(cmp.Data)) return fmt.Sprintf("ip daddr %s %s", formatCmpOp(cmp.Op), formatIPBytes(cmp.Data))
} else if p.Len == 2 { } else if p.Len == 2 {
@ -580,7 +593,6 @@ func formatExpr(exp expr.Any) string {
} }
func formatImmediateData(data []byte) string { func formatImmediateData(data []byte) string {
// For IP addresses (4 bytes)
if len(data) == 4 { if len(data) == 4 {
return fmt.Sprintf("%d.%d.%d.%d", data[0], data[1], data[2], data[3]) return fmt.Sprintf("%d.%d.%d.%d", data[0], data[1], data[2], data[3])
} }
@ -588,26 +600,21 @@ func formatImmediateData(data []byte) string {
} }
func formatMeta(e *expr.Meta) string { func formatMeta(e *expr.Meta) string {
// Handle source register case first (meta mark set)
if e.SourceRegister { if e.SourceRegister {
return fmt.Sprintf("meta %s set reg %d", formatMetaKey(e.Key), e.Register) return fmt.Sprintf("meta %s set reg %d", formatMetaKey(e.Key), e.Register)
} }
// For interface names, handle register load operation
switch e.Key { switch e.Key {
case expr.MetaKeyIIFNAME, case expr.MetaKeyIIFNAME,
expr.MetaKeyOIFNAME, expr.MetaKeyOIFNAME,
expr.MetaKeyBRIIIFNAME, expr.MetaKeyBRIIIFNAME,
expr.MetaKeyBRIOIFNAME: expr.MetaKeyBRIOIFNAME:
// Simply the key name with no register reference
return formatMetaKey(e.Key) return formatMetaKey(e.Key)
case expr.MetaKeyMARK: case expr.MetaKeyMARK:
// For mark operations, we want just "mark"
return "mark" return "mark"
} }
// For other meta keys, show as loading into register
return fmt.Sprintf("meta %s => reg %d", formatMetaKey(e.Key), e.Register) return fmt.Sprintf("meta %s => reg %d", formatMetaKey(e.Key), e.Register)
} }

View File

@ -12,3 +12,8 @@ func (g *BundleGenerator) trySystemdLogFallback() error {
// TODO: Add BSD support // TODO: Add BSD support
return nil return nil
} }
func (g *BundleGenerator) addIPRules() error {
// IP rules are only supported on Linux
return nil
}

View File

@ -10,16 +10,16 @@ import (
) )
func (g *BundleGenerator) addRoutes() error { func (g *BundleGenerator) addRoutes() error {
routes, err := systemops.GetRoutesFromTable() detailedRoutes, err := systemops.GetDetailedRoutesFromTable()
if err != nil { if err != nil {
return fmt.Errorf("get routes: %w", err) return fmt.Errorf("get detailed routes: %w", err)
} }
// TODO: get routes including nexthop routesContent := formatRoutesTable(detailedRoutes, g.anonymize, g.anonymizer)
routesContent := formatRoutes(routes, g.anonymize, g.anonymizer)
routesReader := strings.NewReader(routesContent) routesReader := strings.NewReader(routesContent)
if err := g.addFileToZip(routesReader, "routes.txt"); err != nil { if err := g.addFileToZip(routesReader, "routes.txt"); err != nil {
return fmt.Errorf("add routes file to zip: %w", err) return fmt.Errorf("add routes file to zip: %w", err)
} }
return nil return nil
} }

View File

@ -0,0 +1,206 @@
package debug
import (
"fmt"
"net"
"net/netip"
"sort"
"strings"
"github.com/netbirdio/netbird/client/anonymize"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
"github.com/netbirdio/netbird/management/domain"
)
func formatInterfaces(interfaces []net.Interface, anonymize bool, anonymizer *anonymize.Anonymizer) string {
sort.Slice(interfaces, func(i, j int) bool {
return interfaces[i].Name < interfaces[j].Name
})
var builder strings.Builder
builder.WriteString("Network Interfaces:\n")
for _, iface := range interfaces {
builder.WriteString(fmt.Sprintf("\nInterface: %s\n", iface.Name))
builder.WriteString(fmt.Sprintf(" Index: %d\n", iface.Index))
builder.WriteString(fmt.Sprintf(" MTU: %d\n", iface.MTU))
builder.WriteString(fmt.Sprintf(" Flags: %v\n", iface.Flags))
addrs, err := iface.Addrs()
if err != nil {
builder.WriteString(fmt.Sprintf(" Addresses: Error retrieving addresses: %v\n", err))
} else {
builder.WriteString(" Addresses:\n")
for _, addr := range addrs {
prefix, err := netip.ParsePrefix(addr.String())
if err != nil {
builder.WriteString(fmt.Sprintf(" Error parsing address: %v\n", err))
continue
}
ip := prefix.Addr()
if anonymize {
ip = anonymizer.AnonymizeIP(ip)
}
builder.WriteString(fmt.Sprintf(" %s/%d\n", ip, prefix.Bits()))
}
}
}
return builder.String()
}
func formatResolvedDomains(resolvedDomains map[domain.Domain]peer.ResolvedDomainInfo, anonymize bool, anonymizer *anonymize.Anonymizer) string {
if len(resolvedDomains) == 0 {
return "No resolved domains found.\n"
}
var builder strings.Builder
builder.WriteString("Resolved Domains:\n")
builder.WriteString("=================\n\n")
var sortedParents []domain.Domain
for parentDomain := range resolvedDomains {
sortedParents = append(sortedParents, parentDomain)
}
sort.Slice(sortedParents, func(i, j int) bool {
return sortedParents[i].SafeString() < sortedParents[j].SafeString()
})
for _, parentDomain := range sortedParents {
info := resolvedDomains[parentDomain]
parentKey := parentDomain.SafeString()
if anonymize {
parentKey = anonymizer.AnonymizeDomain(parentKey)
}
builder.WriteString(fmt.Sprintf("%s:\n", parentKey))
var sortedIPs []string
for _, prefix := range info.Prefixes {
ipStr := prefix.String()
if anonymize {
anonymizedIP := anonymizer.AnonymizeIP(prefix.Addr())
ipStr = fmt.Sprintf("%s/%d", anonymizedIP, prefix.Bits())
}
sortedIPs = append(sortedIPs, ipStr)
}
sort.Strings(sortedIPs)
for _, ipStr := range sortedIPs {
builder.WriteString(fmt.Sprintf(" %s\n", ipStr))
}
builder.WriteString("\n")
}
return builder.String()
}
func formatRoutesTable(detailedRoutes []systemops.DetailedRoute, anonymize bool, anonymizer *anonymize.Anonymizer) string {
if len(detailedRoutes) == 0 {
return "No routes found.\n"
}
sort.Slice(detailedRoutes, func(i, j int) bool {
if detailedRoutes[i].Table != detailedRoutes[j].Table {
return detailedRoutes[i].Table < detailedRoutes[j].Table
}
return detailedRoutes[i].Route.Dst.String() < detailedRoutes[j].Route.Dst.String()
})
headers, rows := buildPlatformSpecificRouteTable(detailedRoutes, anonymize, anonymizer)
return formatTable("Routing Table:", headers, rows)
}
func formatRouteDestination(destination netip.Prefix, anonymize bool, anonymizer *anonymize.Anonymizer) string {
if anonymize {
anonymizedDestIP := anonymizer.AnonymizeIP(destination.Addr())
return fmt.Sprintf("%s/%d", anonymizedDestIP, destination.Bits())
}
return destination.String()
}
func formatRouteGateway(gateway netip.Addr, anonymize bool, anonymizer *anonymize.Anonymizer) string {
if gateway.IsValid() {
if anonymize {
return anonymizer.AnonymizeIP(gateway).String()
}
return gateway.String()
}
return "-"
}
func formatRouteInterface(iface *net.Interface) string {
if iface != nil {
return iface.Name
}
return "-"
}
func formatInterfaceIndex(index int) string {
if index <= 0 {
return "-"
}
return fmt.Sprintf("%d", index)
}
func formatRouteMetric(metric int) string {
if metric < 0 {
return "-"
}
return fmt.Sprintf("%d", metric)
}
func formatTable(title string, headers []string, rows [][]string) string {
widths := make([]int, len(headers))
for i, header := range headers {
widths[i] = len(header)
}
for _, row := range rows {
for i, cell := range row {
if len(cell) > widths[i] {
widths[i] = len(cell)
}
}
}
for i := range widths {
widths[i] += 2
}
var formatParts []string
for _, width := range widths {
formatParts = append(formatParts, fmt.Sprintf("%%-%ds", width))
}
formatStr := strings.Join(formatParts, "") + "\n"
var builder strings.Builder
builder.WriteString(title + "\n")
builder.WriteString(strings.Repeat("=", len(title)) + "\n\n")
headerArgs := make([]interface{}, len(headers))
for i, header := range headers {
headerArgs[i] = header
}
builder.WriteString(fmt.Sprintf(formatStr, headerArgs...))
separatorArgs := make([]interface{}, len(headers))
for i, width := range widths {
separatorArgs[i] = strings.Repeat("-", width-2)
}
builder.WriteString(fmt.Sprintf(formatStr, separatorArgs...))
for _, row := range rows {
rowArgs := make([]interface{}, len(row))
for i, cell := range row {
rowArgs[i] = cell
}
builder.WriteString(fmt.Sprintf(formatStr, rowArgs...))
}
return builder.String()
}

View File

@ -0,0 +1,185 @@
//go:build linux && !android
package debug
import (
"fmt"
"net/netip"
"sort"
"github.com/netbirdio/netbird/client/anonymize"
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
)
func formatIPRulesTable(ipRules []systemops.IPRule, anonymize bool, anonymizer *anonymize.Anonymizer) string {
if len(ipRules) == 0 {
return "No IP rules found.\n"
}
sort.Slice(ipRules, func(i, j int) bool {
return ipRules[i].Priority < ipRules[j].Priority
})
columnConfig := detectIPRuleColumns(ipRules)
headers := buildIPRuleHeaders(columnConfig)
rows := buildIPRuleRows(ipRules, columnConfig, anonymize, anonymizer)
return formatTable("IP Rules:", headers, rows)
}
type ipRuleColumnConfig struct {
hasInvert, hasTo, hasMark, hasIIF, hasOIF, hasSuppressPlen bool
}
func detectIPRuleColumns(ipRules []systemops.IPRule) ipRuleColumnConfig {
var config ipRuleColumnConfig
for _, rule := range ipRules {
if rule.Invert {
config.hasInvert = true
}
if rule.To.IsValid() {
config.hasTo = true
}
if rule.Mark != 0 {
config.hasMark = true
}
if rule.IIF != "" {
config.hasIIF = true
}
if rule.OIF != "" {
config.hasOIF = true
}
if rule.SuppressPlen >= 0 {
config.hasSuppressPlen = true
}
}
return config
}
func buildIPRuleHeaders(config ipRuleColumnConfig) []string {
var headers []string
headers = append(headers, "Priority")
if config.hasInvert {
headers = append(headers, "Not")
}
headers = append(headers, "From")
if config.hasTo {
headers = append(headers, "To")
}
if config.hasMark {
headers = append(headers, "FWMark")
}
if config.hasIIF {
headers = append(headers, "IIF")
}
if config.hasOIF {
headers = append(headers, "OIF")
}
headers = append(headers, "Table")
headers = append(headers, "Action")
if config.hasSuppressPlen {
headers = append(headers, "SuppressPlen")
}
return headers
}
func buildIPRuleRows(ipRules []systemops.IPRule, config ipRuleColumnConfig, anonymize bool, anonymizer *anonymize.Anonymizer) [][]string {
var rows [][]string
for _, rule := range ipRules {
row := buildSingleIPRuleRow(rule, config, anonymize, anonymizer)
rows = append(rows, row)
}
return rows
}
func buildSingleIPRuleRow(rule systemops.IPRule, config ipRuleColumnConfig, anonymize bool, anonymizer *anonymize.Anonymizer) []string {
var row []string
row = append(row, fmt.Sprintf("%d", rule.Priority))
if config.hasInvert {
row = append(row, formatIPRuleInvert(rule.Invert))
}
row = append(row, formatIPRuleAddress(rule.From, "all", anonymize, anonymizer))
if config.hasTo {
row = append(row, formatIPRuleAddress(rule.To, "-", anonymize, anonymizer))
}
if config.hasMark {
row = append(row, formatIPRuleMark(rule.Mark, rule.Mask))
}
if config.hasIIF {
row = append(row, formatIPRuleInterface(rule.IIF))
}
if config.hasOIF {
row = append(row, formatIPRuleInterface(rule.OIF))
}
row = append(row, rule.Table)
row = append(row, formatIPRuleAction(rule.Action))
if config.hasSuppressPlen {
row = append(row, formatIPRuleSuppressPlen(rule.SuppressPlen))
}
return row
}
func formatIPRuleInvert(invert bool) string {
if invert {
return "not"
}
return "-"
}
func formatIPRuleAction(action string) string {
if action == "unspec" {
return "lookup"
}
return action
}
func formatIPRuleSuppressPlen(suppressPlen int) string {
if suppressPlen >= 0 {
return fmt.Sprintf("%d", suppressPlen)
}
return "-"
}
func formatIPRuleAddress(prefix netip.Prefix, defaultVal string, anonymize bool, anonymizer *anonymize.Anonymizer) string {
if !prefix.IsValid() {
return defaultVal
}
if anonymize {
anonymizedIP := anonymizer.AnonymizeIP(prefix.Addr())
return fmt.Sprintf("%s/%d", anonymizedIP, prefix.Bits())
}
return prefix.String()
}
func formatIPRuleMark(mark, mask uint32) string {
if mark == 0 {
return "-"
}
if mask != 0 {
return fmt.Sprintf("0x%x/0x%x", mark, mask)
}
return fmt.Sprintf("0x%x", mark)
}
func formatIPRuleInterface(iface string) string {
if iface == "" {
return "-"
}
return iface
}

View File

@ -0,0 +1,27 @@
//go:build !windows
package debug
import (
"github.com/netbirdio/netbird/client/anonymize"
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
)
// buildPlatformSpecificRouteTable builds headers and rows for non-Windows platforms
func buildPlatformSpecificRouteTable(detailedRoutes []systemops.DetailedRoute, anonymize bool, anonymizer *anonymize.Anonymizer) ([]string, [][]string) {
headers := []string{"Destination", "Gateway", "Interface", "Idx", "Metric", "Protocol", "Scope", "Type", "Table", "Flags"}
var rows [][]string
for _, route := range detailedRoutes {
destStr := formatRouteDestination(route.Route.Dst, anonymize, anonymizer)
gatewayStr := formatRouteGateway(route.Route.Gw, anonymize, anonymizer)
interfaceStr := formatRouteInterface(route.Route.Interface)
indexStr := formatInterfaceIndex(route.InterfaceIndex)
metricStr := formatRouteMetric(route.Metric)
row := []string{destStr, gatewayStr, interfaceStr, indexStr, metricStr, route.Protocol, route.Scope, route.Type, route.Table, route.Flags}
rows = append(rows, row)
}
return headers, rows
}

View File

@ -0,0 +1,37 @@
//go:build windows
package debug
import (
"fmt"
"github.com/netbirdio/netbird/client/anonymize"
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
)
// buildPlatformSpecificRouteTable builds headers and rows for Windows with interface metrics
func buildPlatformSpecificRouteTable(detailedRoutes []systemops.DetailedRoute, anonymize bool, anonymizer *anonymize.Anonymizer) ([]string, [][]string) {
headers := []string{"Destination", "Gateway", "Interface", "Idx", "Metric", "If Metric", "Protocol", "Age", "Origin"}
var rows [][]string
for _, route := range detailedRoutes {
destStr := formatRouteDestination(route.Route.Dst, anonymize, anonymizer)
gatewayStr := formatRouteGateway(route.Route.Gw, anonymize, anonymizer)
interfaceStr := formatRouteInterface(route.Route.Interface)
indexStr := formatInterfaceIndex(route.InterfaceIndex)
metricStr := formatRouteMetric(route.Metric)
ifMetricStr := formatInterfaceMetric(route.InterfaceMetric)
row := []string{destStr, gatewayStr, interfaceStr, indexStr, metricStr, ifMetricStr, route.Protocol, route.Scope, route.Type}
rows = append(rows, row)
}
return headers, rows
}
func formatInterfaceMetric(metric int) string {
if metric < 0 {
return "-"
}
return fmt.Sprintf("%d", metric)
}

View File

@ -2,9 +2,12 @@
package systemops package systemops
import "syscall" import (
"strings"
"syscall"
)
// filterRoutesByFlags - return true if need to ignore such route message because it consists specific flags. // filterRoutesByFlags returns true if the route message should be ignored based on its flags.
func filterRoutesByFlags(routeMessageFlags int) bool { func filterRoutesByFlags(routeMessageFlags int) bool {
if routeMessageFlags&syscall.RTF_UP == 0 { if routeMessageFlags&syscall.RTF_UP == 0 {
return true return true
@ -16,3 +19,50 @@ func filterRoutesByFlags(routeMessageFlags int) bool {
return false return false
} }
// formatBSDFlags formats route flags for BSD systems (excludes FreeBSD-specific handling)
func formatBSDFlags(flags int) string {
var flagStrs []string
if flags&syscall.RTF_UP != 0 {
flagStrs = append(flagStrs, "U")
}
if flags&syscall.RTF_GATEWAY != 0 {
flagStrs = append(flagStrs, "G")
}
if flags&syscall.RTF_HOST != 0 {
flagStrs = append(flagStrs, "H")
}
if flags&syscall.RTF_REJECT != 0 {
flagStrs = append(flagStrs, "R")
}
if flags&syscall.RTF_DYNAMIC != 0 {
flagStrs = append(flagStrs, "D")
}
if flags&syscall.RTF_MODIFIED != 0 {
flagStrs = append(flagStrs, "M")
}
if flags&syscall.RTF_STATIC != 0 {
flagStrs = append(flagStrs, "S")
}
if flags&syscall.RTF_LLINFO != 0 {
flagStrs = append(flagStrs, "L")
}
if flags&syscall.RTF_LOCAL != 0 {
flagStrs = append(flagStrs, "l")
}
if flags&syscall.RTF_BLACKHOLE != 0 {
flagStrs = append(flagStrs, "B")
}
if flags&syscall.RTF_CLONING != 0 {
flagStrs = append(flagStrs, "C")
}
if flags&syscall.RTF_WASCLONED != 0 {
flagStrs = append(flagStrs, "W")
}
if len(flagStrs) == 0 {
return "-"
}
return strings.Join(flagStrs, "")
}

View File

@ -1,19 +1,64 @@
//go:build: freebsd //go:build freebsd
package systemops package systemops
import "syscall" import (
"strings"
"syscall"
)
// filterRoutesByFlags - return true if need to ignore such route message because it consists specific flags. // filterRoutesByFlags returns true if the route message should be ignored based on its flags.
func filterRoutesByFlags(routeMessageFlags int) bool { func filterRoutesByFlags(routeMessageFlags int) bool {
if routeMessageFlags&syscall.RTF_UP == 0 { if routeMessageFlags&syscall.RTF_UP == 0 {
return true return true
} }
// NOTE: syscall.RTF_WASCLONED deprecated in FreeBSD 8.0 (https://www.freebsd.org/releases/8.0R/relnotes-detailed/) // NOTE: syscall.RTF_WASCLONED deprecated in FreeBSD 8.0
// a concept of cloned route (a route generated by an entry with RTF_CLONING flag) is deprecated.
if routeMessageFlags&(syscall.RTF_REJECT|syscall.RTF_BLACKHOLE) != 0 { if routeMessageFlags&(syscall.RTF_REJECT|syscall.RTF_BLACKHOLE) != 0 {
return true return true
} }
return false return false
} }
// formatBSDFlags formats route flags for FreeBSD (excludes deprecated RTF_CLONING and RTF_WASCLONED)
func formatBSDFlags(flags int) string {
var flagStrs []string
if flags&syscall.RTF_UP != 0 {
flagStrs = append(flagStrs, "U")
}
if flags&syscall.RTF_GATEWAY != 0 {
flagStrs = append(flagStrs, "G")
}
if flags&syscall.RTF_HOST != 0 {
flagStrs = append(flagStrs, "H")
}
if flags&syscall.RTF_REJECT != 0 {
flagStrs = append(flagStrs, "R")
}
if flags&syscall.RTF_DYNAMIC != 0 {
flagStrs = append(flagStrs, "D")
}
if flags&syscall.RTF_MODIFIED != 0 {
flagStrs = append(flagStrs, "M")
}
if flags&syscall.RTF_STATIC != 0 {
flagStrs = append(flagStrs, "S")
}
if flags&syscall.RTF_LLINFO != 0 {
flagStrs = append(flagStrs, "L")
}
if flags&syscall.RTF_LOCAL != 0 {
flagStrs = append(flagStrs, "l")
}
if flags&syscall.RTF_BLACKHOLE != 0 {
flagStrs = append(flagStrs, "B")
}
// Note: RTF_CLONING and RTF_WASCLONED deprecated in FreeBSD 8.0
if len(flagStrs) == 0 {
return "-"
}
return strings.Join(flagStrs, "")
}

View File

@ -19,6 +19,26 @@ type Nexthop struct {
Intf *net.Interface Intf *net.Interface
} }
// Route represents a basic network route with core routing information
type Route struct {
Dst netip.Prefix
Gw netip.Addr
Interface *net.Interface
}
// DetailedRoute extends Route with additional metadata for display and debugging
type DetailedRoute struct {
Route
Metric int
InterfaceMetric int
InterfaceIndex int
Protocol string
Scope string
Type string
Table string
Flags string
}
// Equal checks if two nexthops are equal. // Equal checks if two nexthops are equal.
func (n Nexthop) Equal(other Nexthop) bool { func (n Nexthop) Equal(other Nexthop) bool {
return n.IP == other.IP && (n.Intf == nil && other.Intf == nil || return n.IP == other.IP && (n.Intf == nil && other.Intf == nil ||

View File

@ -16,12 +16,6 @@ import (
"golang.org/x/net/route" "golang.org/x/net/route"
) )
type Route struct {
Dst netip.Prefix
Gw netip.Addr
Interface *net.Interface
}
func GetRoutesFromTable() ([]netip.Prefix, error) { func GetRoutesFromTable() ([]netip.Prefix, error) {
tab, err := retryFetchRIB() tab, err := retryFetchRIB()
if err != nil { if err != nil {
@ -47,25 +41,134 @@ func GetRoutesFromTable() ([]netip.Prefix, error) {
continue continue
} }
route, err := MsgToRoute(m) r, err := MsgToRoute(m)
if err != nil { if err != nil {
log.Warnf("Failed to parse route message: %v", err) log.Warnf("Failed to parse route message: %v", err)
continue continue
} }
if route.Dst.IsValid() { if r.Dst.IsValid() {
prefixList = append(prefixList, route.Dst) prefixList = append(prefixList, r.Dst)
} }
} }
return prefixList, nil return prefixList, nil
} }
func GetDetailedRoutesFromTable() ([]DetailedRoute, error) {
tab, err := retryFetchRIB()
if err != nil {
return nil, fmt.Errorf("fetch RIB: %v", err)
}
msgs, err := route.ParseRIB(route.RIBTypeRoute, tab)
if err != nil {
return nil, fmt.Errorf("parse RIB: %v", err)
}
return processRouteMessages(msgs)
}
func processRouteMessages(msgs []route.Message) ([]DetailedRoute, error) {
var detailedRoutes []DetailedRoute
for _, msg := range msgs {
m := msg.(*route.RouteMessage)
if !isValidRouteMessage(m) {
continue
}
if filterRoutesByFlags(m.Flags) {
continue
}
detailed, err := buildDetailedRouteFromMessage(m)
if err != nil {
log.Warnf("Failed to parse route message: %v", err)
continue
}
if detailed != nil {
detailedRoutes = append(detailedRoutes, *detailed)
}
}
return detailedRoutes, nil
}
func isValidRouteMessage(m *route.RouteMessage) bool {
if m.Version < 3 || m.Version > 5 {
log.Warnf("Unexpected RIB message version: %d", m.Version)
return false
}
if m.Type != syscall.RTM_GET {
log.Warnf("Unexpected RIB message type: %d", m.Type)
return false
}
return true
}
func buildDetailedRouteFromMessage(m *route.RouteMessage) (*DetailedRoute, error) {
routeMsg, err := MsgToRoute(m)
if err != nil {
return nil, err
}
if !routeMsg.Dst.IsValid() {
return nil, errors.New("invalid destination")
}
detailed := DetailedRoute{
Route: Route{
Dst: routeMsg.Dst,
Gw: routeMsg.Gw,
Interface: routeMsg.Interface,
},
Metric: extractBSDMetric(m),
Protocol: extractBSDProtocol(m.Flags),
Scope: "global",
Type: "unicast",
Table: "main",
Flags: formatBSDFlags(m.Flags),
}
return &detailed, nil
}
func buildLinkInterface(t *route.LinkAddr) *net.Interface {
interfaceName := fmt.Sprintf("link#%d", t.Index)
if t.Name != "" {
interfaceName = t.Name
}
return &net.Interface{
Index: t.Index,
Name: interfaceName,
}
}
func extractBSDMetric(m *route.RouteMessage) int {
return -1
}
func extractBSDProtocol(flags int) string {
if flags&syscall.RTF_STATIC != 0 {
return "static"
}
if flags&syscall.RTF_DYNAMIC != 0 {
return "dynamic"
}
if flags&syscall.RTF_LOCAL != 0 {
return "local"
}
return "kernel"
}
func retryFetchRIB() ([]byte, error) { func retryFetchRIB() ([]byte, error) {
var out []byte var out []byte
operation := func() error { operation := func() error {
var err error var err error
out, err = route.FetchRIB(syscall.AF_UNSPEC, route.RIBTypeRoute, 0) out, err = route.FetchRIB(syscall.AF_UNSPEC, route.RIBTypeRoute, 0)
if errors.Is(err, syscall.ENOMEM) { if errors.Is(err, syscall.ENOMEM) {
log.Debug("~etrying fetchRIB due to 'cannot allocate memory' error") log.Debug("Retrying fetchRIB due to 'cannot allocate memory' error")
return err return err
} else if err != nil { } else if err != nil {
return backoff.Permanent(err) return backoff.Permanent(err)
@ -100,7 +203,6 @@ func toNetIP(a route.Addr) netip.Addr {
} }
} }
// ones returns the number of leading ones in the mask.
func ones(a route.Addr) (int, error) { func ones(a route.Addr) (int, error) {
switch t := a.(type) { switch t := a.(type) {
case *route.Inet4Addr: case *route.Inet4Addr:
@ -114,7 +216,6 @@ func ones(a route.Addr) (int, error) {
} }
} }
// MsgToRoute converts a route message to a Route.
func MsgToRoute(msg *route.RouteMessage) (*Route, error) { func MsgToRoute(msg *route.RouteMessage) (*Route, error) {
dstIP, nexthop, dstMask := msg.Addrs[0], msg.Addrs[1], msg.Addrs[2] dstIP, nexthop, dstMask := msg.Addrs[0], msg.Addrs[1], msg.Addrs[2]
@ -127,10 +228,7 @@ func MsgToRoute(msg *route.RouteMessage) (*Route, error) {
case *route.Inet4Addr, *route.Inet6Addr: case *route.Inet4Addr, *route.Inet6Addr:
nexthopAddr = toNetIP(t) nexthopAddr = toNetIP(t)
case *route.LinkAddr: case *route.LinkAddr:
nexthopIntf = &net.Interface{ nexthopIntf = buildLinkInterface(t)
Index: t.Index,
Name: t.Name,
}
default: default:
return nil, fmt.Errorf("unexpected next hop type: %T", t) return nil, fmt.Errorf("unexpected next hop type: %T", t)
} }
@ -156,5 +254,4 @@ func MsgToRoute(msg *route.RouteMessage) (*Route, error) {
Gw: nexthopAddr, Gw: nexthopAddr,
Interface: nexthopIntf, Interface: nexthopIntf,
}, nil }, nil
} }

View File

@ -14,6 +14,7 @@ import (
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/vishvananda/netlink" "github.com/vishvananda/netlink"
"golang.org/x/sys/unix"
nberrors "github.com/netbirdio/netbird/client/errors" nberrors "github.com/netbirdio/netbird/client/errors"
"github.com/netbirdio/netbird/client/internal/routemanager/sysctl" "github.com/netbirdio/netbird/client/internal/routemanager/sysctl"
@ -22,6 +23,25 @@ import (
nbnet "github.com/netbirdio/netbird/util/net" 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 ( const (
// NetbirdVPNTableID is the ID of the custom routing table used by Netbird. // NetbirdVPNTableID is the ID of the custom routing table used by Netbird.
NetbirdVPNTableID = 0x1BD0 NetbirdVPNTableID = 0x1BD0
@ -37,6 +57,8 @@ const (
var ErrTableIDExists = errors.New("ID exists with different name") 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 // originalSysctl stores the original sysctl values before they are modified
var originalSysctl map[string]int var originalSysctl map[string]int
@ -209,6 +231,277 @@ func GetRoutesFromTable() ([]netip.Prefix, error) {
return append(v4Routes, v6Routes...), nil 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. // getRoutes fetches routes from a specific routing table identified by tableID.
func getRoutes(tableID, family int) ([]netip.Prefix, error) { func getRoutes(tableID, family int) ([]netip.Prefix, error) {
var prefixList []netip.Prefix var prefixList []netip.Prefix
@ -237,6 +530,115 @@ func getRoutes(tableID, family int) ([]netip.Prefix, error) {
return prefixList, nil 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. // addRoute adds a route to a specific routing table identified by tableID.
func addRoute(prefix netip.Prefix, nexthop Nexthop, tableID int) error { func addRoute(prefix netip.Prefix, nexthop Nexthop, tableID int) error {
route := &netlink.Route{ route := &netlink.Route{
@ -247,7 +649,7 @@ func addRoute(prefix netip.Prefix, nexthop Nexthop, tableID int) error {
_, ipNet, err := net.ParseCIDR(prefix.String()) _, ipNet, err := net.ParseCIDR(prefix.String())
if err != nil { if err != nil {
return fmt.Errorf("parse prefix %s: %w", prefix, err) return fmt.Errorf(errParsePrefixMsg, prefix, err)
} }
route.Dst = ipNet 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 { func addUnreachableRoute(prefix netip.Prefix, tableID int) error {
_, ipNet, err := net.ParseCIDR(prefix.String()) _, ipNet, err := net.ParseCIDR(prefix.String())
if err != nil { if err != nil {
return fmt.Errorf("parse prefix %s: %w", prefix, err) return fmt.Errorf(errParsePrefixMsg, prefix, err)
} }
route := &netlink.Route{ route := &netlink.Route{
@ -288,7 +690,7 @@ func addUnreachableRoute(prefix netip.Prefix, tableID int) error {
func removeUnreachableRoute(prefix netip.Prefix, tableID int) error { func removeUnreachableRoute(prefix netip.Prefix, tableID int) error {
_, ipNet, err := net.ParseCIDR(prefix.String()) _, ipNet, err := net.ParseCIDR(prefix.String())
if err != nil { if err != nil {
return fmt.Errorf("parse prefix %s: %w", prefix, err) return fmt.Errorf(errParsePrefixMsg, prefix, err)
} }
route := &netlink.Route{ 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 { func removeRoute(prefix netip.Prefix, nexthop Nexthop, tableID int) error {
_, ipNet, err := net.ParseCIDR(prefix.String()) _, ipNet, err := net.ParseCIDR(prefix.String())
if err != nil { if err != nil {
return fmt.Errorf("parse prefix %s: %w", prefix, err) return fmt.Errorf(errParsePrefixMsg, prefix, err)
} }
route := &netlink.Route{ route := &netlink.Route{

View File

@ -10,6 +10,25 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
// 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
}
func (r *SysOps) AddVPNRoute(prefix netip.Prefix, intf *net.Interface) error { func (r *SysOps) AddVPNRoute(prefix netip.Prefix, intf *net.Interface) error {
if err := r.validateRoute(prefix); err != nil { if err := r.validateRoute(prefix); err != nil {
return err return err
@ -32,3 +51,9 @@ func EnableIPForwarding() error {
func hasSeparateRouting() ([]netip.Prefix, error) { func hasSeparateRouting() ([]netip.Prefix, error) {
return GetRoutesFromTable() return GetRoutesFromTable()
} }
// GetIPRules returns IP rules for debugging (not supported on non-Linux platforms)
func GetIPRules() ([]IPRule, error) {
log.Infof("IP rules collection is not supported on %s", runtime.GOOS)
return []IPRule{}, nil
}

View File

@ -40,13 +40,6 @@ type RouteMonitor struct {
done chan struct{} done chan struct{}
} }
// Route represents a single routing table entry.
type Route struct {
Destination netip.Prefix
Nexthop netip.Addr
Interface *net.Interface
}
type MSFT_NetRoute struct { type MSFT_NetRoute struct {
DestinationPrefix string DestinationPrefix string
NextHop string NextHop string
@ -78,6 +71,12 @@ type MIB_IPFORWARD_ROW2 struct {
Origin uint32 Origin uint32
} }
// MIB_IPFORWARD_TABLE2 represents a table of IP forward entries
type MIB_IPFORWARD_TABLE2 struct {
NumEntries uint32
Table [1]MIB_IPFORWARD_ROW2 // Flexible array member
}
// IP_ADDRESS_PREFIX is defined in https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ns-netioapi-ip_address_prefix // IP_ADDRESS_PREFIX is defined in https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ns-netioapi-ip_address_prefix
type IP_ADDRESS_PREFIX struct { type IP_ADDRESS_PREFIX struct {
Prefix SOCKADDR_INET Prefix SOCKADDR_INET
@ -108,6 +107,45 @@ type SOCKADDR_INET_NEXTHOP struct {
// MIB_NOTIFICATION_TYPE is defined in https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ne-netioapi-mib_notification_type // MIB_NOTIFICATION_TYPE is defined in https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ne-netioapi-mib_notification_type
type MIB_NOTIFICATION_TYPE int32 type MIB_NOTIFICATION_TYPE int32
// MIB_IPINTERFACE_ROW is defined in https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ns-netioapi-mib_ipinterface_row
type MIB_IPINTERFACE_ROW struct {
Family uint16
InterfaceLuid luid
InterfaceIndex uint32
MaxReassemblySize uint32
InterfaceIdentifier uint64
MinRouterAdvertisementInterval uint32
MaxRouterAdvertisementInterval uint32
AdvertisingEnabled uint8
ForwardingEnabled uint8
WeakHostSend uint8
WeakHostReceive uint8
UseAutomaticMetric uint8
UseNeighborUnreachabilityDetection uint8
ManagedAddressConfigurationSupported uint8
OtherStatefulConfigurationSupported uint8
AdvertiseDefaultRoute uint8
RouterDiscoveryBehavior uint32
DadTransmits uint32
BaseReachableTime uint32
RetransmitTime uint32
PathMtuDiscoveryTimeout uint32
LinkLocalAddressBehavior uint32
LinkLocalAddressTimeout uint32
ZoneIndices [16]uint32
SitePrefixLength uint32
Metric uint32
NlMtu uint32
Connected uint8
SupportsWakeUpPatterns uint8
SupportsNeighborDiscovery uint8
SupportsRouterDiscovery uint8
ReachableTime uint32
TransmitOffload uint32
ReceiveOffload uint32
DisableDefaultRoutes uint8
}
var ( var (
modiphlpapi = windows.NewLazyDLL("iphlpapi.dll") modiphlpapi = windows.NewLazyDLL("iphlpapi.dll")
procNotifyRouteChange2 = modiphlpapi.NewProc("NotifyRouteChange2") procNotifyRouteChange2 = modiphlpapi.NewProc("NotifyRouteChange2")
@ -115,8 +153,11 @@ var (
procCreateIpForwardEntry2 = modiphlpapi.NewProc("CreateIpForwardEntry2") procCreateIpForwardEntry2 = modiphlpapi.NewProc("CreateIpForwardEntry2")
procDeleteIpForwardEntry2 = modiphlpapi.NewProc("DeleteIpForwardEntry2") procDeleteIpForwardEntry2 = modiphlpapi.NewProc("DeleteIpForwardEntry2")
procGetIpForwardEntry2 = modiphlpapi.NewProc("GetIpForwardEntry2") procGetIpForwardEntry2 = modiphlpapi.NewProc("GetIpForwardEntry2")
procGetIpForwardTable2 = modiphlpapi.NewProc("GetIpForwardTable2")
procInitializeIpForwardEntry = modiphlpapi.NewProc("InitializeIpForwardEntry") procInitializeIpForwardEntry = modiphlpapi.NewProc("InitializeIpForwardEntry")
procConvertInterfaceIndexToLuid = modiphlpapi.NewProc("ConvertInterfaceIndexToLuid") procConvertInterfaceIndexToLuid = modiphlpapi.NewProc("ConvertInterfaceIndexToLuid")
procGetIpInterfaceEntry = modiphlpapi.NewProc("GetIpInterfaceEntry")
procFreeMibTable = modiphlpapi.NewProc("FreeMibTable")
prefixList []netip.Prefix prefixList []netip.Prefix
lastUpdate time.Time lastUpdate time.Time
@ -429,6 +470,8 @@ func (rm *RouteMonitor) parseUpdate(row *MIB_IPFORWARD_ROW2, notificationType MI
updateType = RouteAdded updateType = RouteAdded
case MibDeleteInstance: case MibDeleteInstance:
updateType = RouteDeleted updateType = RouteDeleted
case MibInitialNotification:
updateType = RouteAdded // Treat initial notifications as additions
} }
update.Type = updateType update.Type = updateType
@ -508,7 +551,7 @@ func GetRoutesFromTable() ([]netip.Prefix, error) {
prefixList = nil prefixList = nil
for _, route := range routes { for _, route := range routes {
prefixList = append(prefixList, route.Destination) prefixList = append(prefixList, route.Dst)
} }
lastUpdate = time.Now() lastUpdate = time.Now()
@ -551,15 +594,159 @@ func GetRoutes() ([]Route, error) {
} }
routes = append(routes, Route{ routes = append(routes, Route{
Destination: dest, Dst: dest,
Nexthop: nexthop, Gw: nexthop,
Interface: intf, Interface: intf,
}) })
} }
return routes, nil return routes, nil
} }
// GetDetailedRoutesFromTable returns detailed route information using Windows syscalls
func GetDetailedRoutesFromTable() ([]DetailedRoute, error) {
table, err := getWindowsRoutingTable()
if err != nil {
return nil, err
}
defer freeWindowsRoutingTable(table)
return parseWindowsRoutingTable(table), nil
}
func getWindowsRoutingTable() (*MIB_IPFORWARD_TABLE2, error) {
var table *MIB_IPFORWARD_TABLE2
ret, _, err := procGetIpForwardTable2.Call(
uintptr(windows.AF_UNSPEC),
uintptr(unsafe.Pointer(&table)),
)
if ret != 0 {
return nil, fmt.Errorf("GetIpForwardTable2 failed: %w", err)
}
if table == nil {
return nil, fmt.Errorf("received nil routing table")
}
return table, nil
}
func freeWindowsRoutingTable(table *MIB_IPFORWARD_TABLE2) {
if table != nil {
ret, _, _ := procFreeMibTable.Call(uintptr(unsafe.Pointer(table)))
if ret != 0 {
log.Warnf("FreeMibTable failed with return code: %d", ret)
}
}
}
func parseWindowsRoutingTable(table *MIB_IPFORWARD_TABLE2) []DetailedRoute {
var detailedRoutes []DetailedRoute
entrySize := unsafe.Sizeof(MIB_IPFORWARD_ROW2{})
basePtr := uintptr(unsafe.Pointer(&table.Table[0]))
for i := uint32(0); i < table.NumEntries; i++ {
entryPtr := basePtr + uintptr(i)*entrySize
entry := (*MIB_IPFORWARD_ROW2)(unsafe.Pointer(entryPtr))
detailed := buildWindowsDetailedRoute(entry)
if detailed != nil {
detailedRoutes = append(detailedRoutes, *detailed)
}
}
return detailedRoutes
}
func buildWindowsDetailedRoute(entry *MIB_IPFORWARD_ROW2) *DetailedRoute {
dest := parseIPPrefix(entry.DestinationPrefix, int(entry.InterfaceIndex))
if !dest.IsValid() {
return nil
}
gateway := parseIPNexthop(entry.NextHop, int(entry.InterfaceIndex))
var intf *net.Interface
if entry.InterfaceIndex != 0 {
if netIntf, err := net.InterfaceByIndex(int(entry.InterfaceIndex)); err == nil {
intf = netIntf
} else {
// Create a synthetic interface for display when we can't resolve the name
intf = &net.Interface{
Index: int(entry.InterfaceIndex),
Name: fmt.Sprintf("index-%d", entry.InterfaceIndex),
}
}
}
detailed := DetailedRoute{
Route: Route{
Dst: dest,
Gw: gateway,
Interface: intf,
},
Metric: int(entry.Metric),
InterfaceMetric: getInterfaceMetric(entry.InterfaceIndex, entry.DestinationPrefix.Prefix.sin6_family),
InterfaceIndex: int(entry.InterfaceIndex),
Protocol: windowsProtocolToString(entry.Protocol),
Scope: formatRouteAge(entry.Age),
Type: windowsOriginToString(entry.Origin),
Table: "main",
Flags: "-",
}
return &detailed
}
func windowsProtocolToString(protocol uint32) string {
switch protocol {
case 1:
return "other"
case 2:
return "local"
case 3:
return "netmgmt"
case 4:
return "icmp"
case 5:
return "egp"
case 6:
return "ggp"
case 7:
return "hello"
case 8:
return "rip"
case 9:
return "isis"
case 10:
return "esis"
case 11:
return "cisco"
case 12:
return "bbn"
case 13:
return "ospf"
case 14:
return "bgp"
case 15:
return "idpr"
case 16:
return "eigrp"
case 17:
return "dvmrp"
case 18:
return "rpl"
case 19:
return "dhcp"
default:
return fmt.Sprintf("unknown-%d", protocol)
}
}
func isCacheDisabled() bool { func isCacheDisabled() bool {
return os.Getenv("NB_DISABLE_ROUTE_CACHE") == "true" return os.Getenv("NB_DISABLE_ROUTE_CACHE") == "true"
} }
@ -614,3 +801,59 @@ func addZone(ip netip.Addr, interfaceIndex int) netip.Addr {
} }
return ip return ip
} }
// getInterfaceMetric retrieves the interface metric for a given interface and address family
func getInterfaceMetric(interfaceIndex uint32, family int16) int {
if interfaceIndex == 0 {
return -1
}
var ipInterfaceRow MIB_IPINTERFACE_ROW
ipInterfaceRow.Family = uint16(family)
ipInterfaceRow.InterfaceIndex = interfaceIndex
ret, _, _ := procGetIpInterfaceEntry.Call(uintptr(unsafe.Pointer(&ipInterfaceRow)))
if ret != 0 {
log.Debugf("GetIpInterfaceEntry failed for interface %d: %d", interfaceIndex, ret)
return -1
}
return int(ipInterfaceRow.Metric)
}
// formatRouteAge formats the route age in seconds to a human-readable string
func formatRouteAge(ageSeconds uint32) string {
if ageSeconds == 0 {
return "0s"
}
age := time.Duration(ageSeconds) * time.Second
switch {
case age < time.Minute:
return fmt.Sprintf("%ds", int(age.Seconds()))
case age < time.Hour:
return fmt.Sprintf("%dm", int(age.Minutes()))
case age < 24*time.Hour:
return fmt.Sprintf("%dh", int(age.Hours()))
default:
return fmt.Sprintf("%dd", int(age.Hours()/24))
}
}
// windowsOriginToString converts Windows route origin to string
func windowsOriginToString(origin uint32) string {
switch origin {
case 0:
return "manual"
case 1:
return "wellknown"
case 2:
return "dhcp"
case 3:
return "routeradvert"
case 4:
return "6to4"
default:
return fmt.Sprintf("unknown-%d", origin)
}
}