mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-18 00:06:38 +00:00
[client] Add detailed routes and resolved IPs to debug bundle (#4141)
This commit is contained in:
@@ -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.
|
||||
netbird.err: Most recent, anonymized stderr 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.
|
||||
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.
|
||||
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.
|
||||
network_map.json: Anonymized network map containing peer configurations, routes, DNS settings, and firewall rules.
|
||||
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.
|
||||
|
||||
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
|
||||
The interfaces.txt file contains information about network interfaces, including:
|
||||
@@ -145,6 +169,22 @@ nftables.txt:
|
||||
- Shows packet and byte counters for each rule
|
||||
- All IP addresses are anonymized
|
||||
- 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 (
|
||||
@@ -159,13 +199,11 @@ const (
|
||||
type BundleGenerator struct {
|
||||
anonymizer *anonymize.Anonymizer
|
||||
|
||||
// deps
|
||||
internalConfig *internal.Config
|
||||
statusRecorder *peer.Status
|
||||
networkMap *mgmProto.NetworkMap
|
||||
logFile string
|
||||
|
||||
// config
|
||||
anonymize bool
|
||||
clientStatus string
|
||||
includeSystemInfo bool
|
||||
@@ -258,7 +296,11 @@ func (g *BundleGenerator) createArchive() error {
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -266,7 +308,7 @@ func (g *BundleGenerator) createArchive() error {
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -274,26 +316,26 @@ func (g *BundleGenerator) createArchive() error {
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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 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 {
|
||||
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 {
|
||||
log.Errorf("Failed to add systemd logs: %v", err)
|
||||
log.Errorf("failed to add systemd logs: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -301,15 +343,19 @@ func (g *BundleGenerator) createArchive() error {
|
||||
|
||||
func (g *BundleGenerator) addSystemInfo() {
|
||||
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 {
|
||||
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 {
|
||||
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())
|
||||
if err := g.addFileToZip(configReader, "config.txt"); err != nil {
|
||||
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) {
|
||||
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("WgPort: %d\n", g.internalConfig.WgPort))
|
||||
if g.internalConfig.NetworkMonitor != nil {
|
||||
@@ -461,6 +505,27 @@ func (g *BundleGenerator) addInterfaces() error {
|
||||
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 {
|
||||
if g.networkMap == nil {
|
||||
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)
|
||||
}
|
||||
|
||||
// add rotated log files based on logFileCount
|
||||
g.addRotatedLogFiles(logDir)
|
||||
|
||||
stdErrLogPath := filepath.Join(logDir, errorLogFile)
|
||||
@@ -601,7 +665,7 @@ func (g *BundleGenerator) addSingleLogfile(logPath, targetName string) error {
|
||||
}
|
||||
defer func() {
|
||||
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 {
|
||||
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)
|
||||
if err != nil {
|
||||
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
|
||||
if g.anonymize {
|
||||
@@ -689,7 +761,6 @@ func (g *BundleGenerator) addRotatedLogFiles(logDir string) {
|
||||
return fi.ModTime().After(fj.ModTime())
|
||||
})
|
||||
|
||||
// include up to logFileCount rotated files
|
||||
maxFiles := int(g.logFileCount)
|
||||
if 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 f, ok := reader.(*os.File); ok {
|
||||
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 {
|
||||
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) {
|
||||
defer func() {
|
||||
// always nil
|
||||
@@ -954,7 +942,6 @@ func anonymizeRemotePeer(peer *mgmProto.RemotePeerConfig, anonymizer *anonymize.
|
||||
}
|
||||
|
||||
for i, ip := range peer.AllowedIps {
|
||||
// Try to parse as prefix first (CIDR)
|
||||
if prefix, err := netip.ParsePrefix(ip); err == nil {
|
||||
anonIP := anonymizer.AnonymizeIP(prefix.Addr())
|
||||
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) {
|
||||
switch record.Type {
|
||||
case 1, 28: // A or AAAA record
|
||||
case 1, 28:
|
||||
if addr, err := netip.ParseAddr(record.RData); err == nil {
|
||||
record.RData = anonymizer.AnonymizeIP(addr).String()
|
||||
}
|
||||
|
||||
@@ -17,8 +17,27 @@ import (
|
||||
"github.com/google/nftables"
|
||||
"github.com/google/nftables/expr"
|
||||
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 (
|
||||
maxLogEntries = 100000
|
||||
maxLogAge = 7 * 24 * time.Hour // Last 7 days
|
||||
@@ -136,7 +155,6 @@ func (g *BundleGenerator) addFirewallRules() error {
|
||||
func collectIPTablesRules() (string, error) {
|
||||
var builder strings.Builder
|
||||
|
||||
// First try using iptables-save
|
||||
saveOutput, err := collectIPTablesSave()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to collect iptables rules using iptables-save: %v", err)
|
||||
@@ -146,7 +164,6 @@ func collectIPTablesRules() (string, error) {
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
// Collect ipset information
|
||||
ipsetOutput, err := collectIPSets()
|
||||
if err != nil {
|
||||
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
|
||||
func collectNFTablesRules() (string, error) {
|
||||
// First try using nft command
|
||||
rules, err := collectNFTablesFromCommand()
|
||||
if err != nil {
|
||||
log.Debugf("Failed to collect nftables rules using nft command: %v, falling back to netlink", err)
|
||||
// Fall back to netlink
|
||||
rules, err = collectNFTablesFromNetlink()
|
||||
if err != nil {
|
||||
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 {
|
||||
curr := exprs[i]
|
||||
|
||||
// Handle Meta + Cmp sequence
|
||||
if meta, ok := curr.(*expr.Meta); ok && i+1 < len(exprs) {
|
||||
if cmp, ok := exprs[i+1].(*expr.Cmp); ok {
|
||||
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 cmp, ok := exprs[i+1].(*expr.Cmp); ok {
|
||||
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 {
|
||||
if p.Base == expr.PayloadBaseNetworkHeader {
|
||||
switch p.Offset {
|
||||
case 12: // Source IP
|
||||
case 12:
|
||||
if p.Len == 4 {
|
||||
return fmt.Sprintf("ip saddr %s %s", formatCmpOp(cmp.Op), formatIPBytes(cmp.Data))
|
||||
} else if p.Len == 2 {
|
||||
return fmt.Sprintf("ip saddr %s %s", formatCmpOp(cmp.Op), formatIPBytes(cmp.Data))
|
||||
}
|
||||
case 16: // Destination IP
|
||||
case 16:
|
||||
if p.Len == 4 {
|
||||
return fmt.Sprintf("ip daddr %s %s", formatCmpOp(cmp.Op), formatIPBytes(cmp.Data))
|
||||
} else if p.Len == 2 {
|
||||
@@ -580,7 +593,6 @@ func formatExpr(exp expr.Any) string {
|
||||
}
|
||||
|
||||
func formatImmediateData(data []byte) string {
|
||||
// For IP addresses (4 bytes)
|
||||
if len(data) == 4 {
|
||||
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 {
|
||||
// Handle source register case first (meta mark set)
|
||||
if e.SourceRegister {
|
||||
return fmt.Sprintf("meta %s set reg %d", formatMetaKey(e.Key), e.Register)
|
||||
}
|
||||
|
||||
// For interface names, handle register load operation
|
||||
switch e.Key {
|
||||
case expr.MetaKeyIIFNAME,
|
||||
expr.MetaKeyOIFNAME,
|
||||
expr.MetaKeyBRIIIFNAME,
|
||||
expr.MetaKeyBRIOIFNAME:
|
||||
// Simply the key name with no register reference
|
||||
return formatMetaKey(e.Key)
|
||||
|
||||
case expr.MetaKeyMARK:
|
||||
// For mark operations, we want just "mark"
|
||||
return "mark"
|
||||
}
|
||||
|
||||
// For other meta keys, show as loading into register
|
||||
return fmt.Sprintf("meta %s => reg %d", formatMetaKey(e.Key), e.Register)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,3 +12,8 @@ func (g *BundleGenerator) trySystemdLogFallback() error {
|
||||
// TODO: Add BSD support
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *BundleGenerator) addIPRules() error {
|
||||
// IP rules are only supported on Linux
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -10,16 +10,16 @@ import (
|
||||
)
|
||||
|
||||
func (g *BundleGenerator) addRoutes() error {
|
||||
routes, err := systemops.GetRoutesFromTable()
|
||||
detailedRoutes, err := systemops.GetDetailedRoutesFromTable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get routes: %w", err)
|
||||
return fmt.Errorf("get detailed routes: %w", err)
|
||||
}
|
||||
|
||||
// TODO: get routes including nexthop
|
||||
routesContent := formatRoutes(routes, g.anonymize, g.anonymizer)
|
||||
routesContent := formatRoutesTable(detailedRoutes, g.anonymize, g.anonymizer)
|
||||
routesReader := strings.NewReader(routesContent)
|
||||
if err := g.addFileToZip(routesReader, "routes.txt"); err != nil {
|
||||
return fmt.Errorf("add routes file to zip: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
206
client/internal/debug/format.go
Normal file
206
client/internal/debug/format.go
Normal 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()
|
||||
}
|
||||
185
client/internal/debug/format_linux.go
Normal file
185
client/internal/debug/format_linux.go
Normal 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
|
||||
}
|
||||
27
client/internal/debug/format_nonwindows.go
Normal file
27
client/internal/debug/format_nonwindows.go
Normal 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
|
||||
}
|
||||
37
client/internal/debug/format_windows.go
Normal file
37
client/internal/debug/format_windows.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user