Skip to content

Commit e0d9306

Browse files
authored
[client] Add detailed routes and resolved IPs to debug bundle (#4141)
1 parent 2c4ac33 commit e0d9306

15 files changed

+1501
-165
lines changed

client/internal/debug/debug.go

Lines changed: 95 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,12 @@ status.txt: Anonymized status information of the NetBird client.
4040
client.log: Most recent, anonymized client log file of the NetBird client.
4141
netbird.err: Most recent, anonymized stderr log file of the NetBird client.
4242
netbird.out: Most recent, anonymized stdout log file of the NetBird client.
43-
routes.txt: Anonymized system routes, if --system-info flag was provided.
43+
routes.txt: Detailed system routing table in tabular format including destination, gateway, interface, metrics, and protocol information, if --system-info flag was provided.
4444
interfaces.txt: Anonymized network interface information, if --system-info flag was provided.
45+
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.
4546
iptables.txt: Anonymized iptables rules with packet counters, if --system-info flag was provided.
4647
nftables.txt: Anonymized nftables rules with packet counters, if --system-info flag was provided.
48+
resolved_domains.txt: Anonymized resolved domain IP addresses from the status recorder.
4749
config.txt: Anonymized configuration information of the NetBird client.
4850
network_map.json: Anonymized network map containing peer configurations, routes, DNS settings, and firewall rules.
4951
state.json: Anonymized client state dump containing netbird states.
@@ -107,7 +109,29 @@ go tool pprof -http=:8088 heap.prof
107109
This will open a web browser tab with the profiling information.
108110
109111
Routes
110-
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.
112+
The routes.txt file contains detailed routing table information in a tabular format:
113+
114+
- Destination: Network prefix (IP_ADDRESS/PREFIX_LENGTH)
115+
- Gateway: Next hop IP address (or "-" if direct)
116+
- Interface: Network interface name
117+
- Metric: Route priority/metric (lower values preferred)
118+
- Protocol: Routing protocol (kernel, static, dhcp, etc.)
119+
- Scope: Route scope (global, link, host, etc.)
120+
- Type: Route type (unicast, local, broadcast, etc.)
121+
- Table: Routing table name (main, local, netbird, etc.)
122+
123+
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.
124+
125+
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.
126+
127+
Resolved Domains
128+
The resolved_domains.txt file contains information about domain names that have been resolved to IP addresses by NetBird's DNS resolver. This includes:
129+
- Original domain patterns that were configured for routing
130+
- Resolved domain names that matched those patterns
131+
- IP address prefixes that were resolved for each domain
132+
- Parent domain associations showing which original pattern each resolved domain belongs to
133+
134+
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.
111135
112136
Network Interfaces
113137
The interfaces.txt file contains information about network interfaces, including:
@@ -145,6 +169,22 @@ nftables.txt:
145169
- Shows packet and byte counters for each rule
146170
- All IP addresses are anonymized
147171
- Chain names, table names, and other non-sensitive information remain unchanged
172+
173+
IP Rules (Linux only)
174+
The ip_rules.txt file contains detailed IP routing rule information:
175+
176+
- Priority: Rule priority number (lower values processed first)
177+
- From: Source IP prefix or "all" if unspecified
178+
- To: Destination IP prefix or "all" if unspecified
179+
- IIF: Input interface name or "-" if unspecified
180+
- OIF: Output interface name or "-" if unspecified
181+
- Table: Target routing table name (main, local, netbird, etc.)
182+
- Action: Rule action (lookup, goto, blackhole, etc.)
183+
- Mark: Firewall mark value in hex format or "-" if unspecified
184+
185+
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.
186+
187+
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.
148188
`
149189

150190
const (
@@ -159,13 +199,11 @@ const (
159199
type BundleGenerator struct {
160200
anonymizer *anonymize.Anonymizer
161201

162-
// deps
163202
internalConfig *internal.Config
164203
statusRecorder *peer.Status
165204
networkMap *mgmProto.NetworkMap
166205
logFile string
167206

168-
// config
169207
anonymize bool
170208
clientStatus string
171209
includeSystemInfo bool
@@ -258,58 +296,66 @@ func (g *BundleGenerator) createArchive() error {
258296
}
259297

260298
if err := g.addConfig(); err != nil {
261-
log.Errorf("Failed to add config to debug bundle: %v", err)
299+
log.Errorf("failed to add config to debug bundle: %v", err)
300+
}
301+
302+
if err := g.addResolvedDomains(); err != nil {
303+
log.Errorf("failed to add resolved domains to debug bundle: %v", err)
262304
}
263305

264306
if g.includeSystemInfo {
265307
g.addSystemInfo()
266308
}
267309

268310
if err := g.addProf(); err != nil {
269-
log.Errorf("Failed to add profiles to debug bundle: %v", err)
311+
log.Errorf("failed to add profiles to debug bundle: %v", err)
270312
}
271313

272314
if err := g.addNetworkMap(); err != nil {
273315
return fmt.Errorf("add network map: %w", err)
274316
}
275317

276318
if err := g.addStateFile(); err != nil {
277-
log.Errorf("Failed to add state file to debug bundle: %v", err)
319+
log.Errorf("failed to add state file to debug bundle: %v", err)
278320
}
279321

280322
if err := g.addCorruptedStateFiles(); err != nil {
281-
log.Errorf("Failed to add corrupted state files to debug bundle: %v", err)
323+
log.Errorf("failed to add corrupted state files to debug bundle: %v", err)
282324
}
283325

284326
if err := g.addWgShow(); err != nil {
285-
log.Errorf("Failed to add wg show output: %v", err)
327+
log.Errorf("failed to add wg show output: %v", err)
286328
}
287329

288330
if g.logFile != "" && !slices.Contains(util.SpecialLogs, g.logFile) {
289331
if err := g.addLogfile(); err != nil {
290-
log.Errorf("Failed to add log file to debug bundle: %v", err)
332+
log.Errorf("failed to add log file to debug bundle: %v", err)
291333
if err := g.trySystemdLogFallback(); err != nil {
292-
log.Errorf("Failed to add systemd logs as fallback: %v", err)
334+
log.Errorf("failed to add systemd logs as fallback: %v", err)
293335
}
294336
}
295337
} else if err := g.trySystemdLogFallback(); err != nil {
296-
log.Errorf("Failed to add systemd logs: %v", err)
338+
log.Errorf("failed to add systemd logs: %v", err)
297339
}
298340

299341
return nil
300342
}
301343

302344
func (g *BundleGenerator) addSystemInfo() {
303345
if err := g.addRoutes(); err != nil {
304-
log.Errorf("Failed to add routes to debug bundle: %v", err)
346+
log.Errorf("failed to add routes to debug bundle: %v", err)
305347
}
306348

307349
if err := g.addInterfaces(); err != nil {
308-
log.Errorf("Failed to add interfaces to debug bundle: %v", err)
350+
log.Errorf("failed to add interfaces to debug bundle: %v", err)
351+
}
352+
353+
if err := g.addIPRules(); err != nil {
354+
log.Errorf("failed to add IP rules to debug bundle: %v", err)
309355
}
310356

311357
if err := g.addFirewallRules(); err != nil {
312-
log.Errorf("Failed to add firewall rules to debug bundle: %v", err)
358+
log.Errorf("failed to add firewall rules to debug bundle: %v", err)
313359
}
314360
}
315361

@@ -364,7 +410,6 @@ func (g *BundleGenerator) addConfig() error {
364410
}
365411
}
366412

367-
// Add config content to zip file
368413
configReader := strings.NewReader(configContent.String())
369414
if err := g.addFileToZip(configReader, "config.txt"); err != nil {
370415
return fmt.Errorf("add config file to zip: %w", err)
@@ -376,7 +421,6 @@ func (g *BundleGenerator) addConfig() error {
376421
func (g *BundleGenerator) addCommonConfigFields(configContent *strings.Builder) {
377422
configContent.WriteString("NetBird Client Configuration:\n\n")
378423

379-
// Add non-sensitive fields
380424
configContent.WriteString(fmt.Sprintf("WgIface: %s\n", g.internalConfig.WgIface))
381425
configContent.WriteString(fmt.Sprintf("WgPort: %d\n", g.internalConfig.WgPort))
382426
if g.internalConfig.NetworkMonitor != nil {
@@ -461,6 +505,27 @@ func (g *BundleGenerator) addInterfaces() error {
461505
return nil
462506
}
463507

508+
func (g *BundleGenerator) addResolvedDomains() error {
509+
if g.statusRecorder == nil {
510+
log.Debugf("skipping resolved domains in debug bundle: no status recorder")
511+
return nil
512+
}
513+
514+
resolvedDomains := g.statusRecorder.GetResolvedDomainsStates()
515+
if len(resolvedDomains) == 0 {
516+
log.Debugf("skipping resolved domains in debug bundle: no resolved domains")
517+
return nil
518+
}
519+
520+
resolvedDomainsContent := formatResolvedDomains(resolvedDomains, g.anonymize, g.anonymizer)
521+
resolvedDomainsReader := strings.NewReader(resolvedDomainsContent)
522+
if err := g.addFileToZip(resolvedDomainsReader, "resolved_domains.txt"); err != nil {
523+
return fmt.Errorf("add resolved domains file to zip: %w", err)
524+
}
525+
526+
return nil
527+
}
528+
464529
func (g *BundleGenerator) addNetworkMap() error {
465530
if g.networkMap == nil {
466531
log.Debugf("skipping empty network map in debug bundle")
@@ -572,7 +637,6 @@ func (g *BundleGenerator) addLogfile() error {
572637
return fmt.Errorf("add client log file to zip: %w", err)
573638
}
574639

575-
// add rotated log files based on logFileCount
576640
g.addRotatedLogFiles(logDir)
577641

578642
stdErrLogPath := filepath.Join(logDir, errorLogFile)
@@ -601,7 +665,7 @@ func (g *BundleGenerator) addSingleLogfile(logPath, targetName string) error {
601665
}
602666
defer func() {
603667
if err := logFile.Close(); err != nil {
604-
log.Errorf("Failed to close log file %s: %v", targetName, err)
668+
log.Errorf("failed to close log file %s: %v", targetName, err)
605669
}
606670
}()
607671

@@ -625,13 +689,21 @@ func (g *BundleGenerator) addSingleLogFileGz(logPath, targetName string) error {
625689
if err != nil {
626690
return fmt.Errorf("open gz log file %s: %w", targetName, err)
627691
}
628-
defer f.Close()
692+
defer func() {
693+
if err := f.Close(); err != nil {
694+
log.Errorf("failed to close gz file %s: %v", targetName, err)
695+
}
696+
}()
629697

630698
gzr, err := gzip.NewReader(f)
631699
if err != nil {
632700
return fmt.Errorf("create gzip reader: %w", err)
633701
}
634-
defer gzr.Close()
702+
defer func() {
703+
if err := gzr.Close(); err != nil {
704+
log.Errorf("failed to close gzip reader %s: %v", targetName, err)
705+
}
706+
}()
635707

636708
var logReader io.Reader = gzr
637709
if g.anonymize {
@@ -689,7 +761,6 @@ func (g *BundleGenerator) addRotatedLogFiles(logDir string) {
689761
return fi.ModTime().After(fj.ModTime())
690762
})
691763

692-
// include up to logFileCount rotated files
693764
maxFiles := int(g.logFileCount)
694765
if maxFiles > len(files) {
695766
maxFiles = len(files)
@@ -717,7 +788,7 @@ func (g *BundleGenerator) addFileToZip(reader io.Reader, filename string) error
717788
// If the reader is a file, we can get more accurate information
718789
if f, ok := reader.(*os.File); ok {
719790
if stat, err := f.Stat(); err != nil {
720-
log.Tracef("Failed to get file stat for %s: %v", filename, err)
791+
log.Tracef("failed to get file stat for %s: %v", filename, err)
721792
} else {
722793
header.Modified = stat.ModTime()
723794
}
@@ -765,89 +836,6 @@ func seedFromStatus(a *anonymize.Anonymizer, status *peer.FullStatus) {
765836
}
766837
}
767838

768-
func formatRoutes(routes []netip.Prefix, anonymize bool, anonymizer *anonymize.Anonymizer) string {
769-
var ipv4Routes, ipv6Routes []netip.Prefix
770-
771-
// Separate IPv4 and IPv6 routes
772-
for _, route := range routes {
773-
if route.Addr().Is4() {
774-
ipv4Routes = append(ipv4Routes, route)
775-
} else {
776-
ipv6Routes = append(ipv6Routes, route)
777-
}
778-
}
779-
780-
// Sort IPv4 and IPv6 routes separately
781-
sort.Slice(ipv4Routes, func(i, j int) bool {
782-
return ipv4Routes[i].Bits() > ipv4Routes[j].Bits()
783-
})
784-
sort.Slice(ipv6Routes, func(i, j int) bool {
785-
return ipv6Routes[i].Bits() > ipv6Routes[j].Bits()
786-
})
787-
788-
var builder strings.Builder
789-
790-
// Format IPv4 routes
791-
builder.WriteString("IPv4 Routes:\n")
792-
for _, route := range ipv4Routes {
793-
formatRoute(&builder, route, anonymize, anonymizer)
794-
}
795-
796-
// Format IPv6 routes
797-
builder.WriteString("\nIPv6 Routes:\n")
798-
for _, route := range ipv6Routes {
799-
formatRoute(&builder, route, anonymize, anonymizer)
800-
}
801-
802-
return builder.String()
803-
}
804-
805-
func formatRoute(builder *strings.Builder, route netip.Prefix, anonymize bool, anonymizer *anonymize.Anonymizer) {
806-
if anonymize {
807-
anonymizedIP := anonymizer.AnonymizeIP(route.Addr())
808-
builder.WriteString(fmt.Sprintf("%s/%d\n", anonymizedIP, route.Bits()))
809-
} else {
810-
builder.WriteString(fmt.Sprintf("%s\n", route))
811-
}
812-
}
813-
814-
func formatInterfaces(interfaces []net.Interface, anonymize bool, anonymizer *anonymize.Anonymizer) string {
815-
sort.Slice(interfaces, func(i, j int) bool {
816-
return interfaces[i].Name < interfaces[j].Name
817-
})
818-
819-
var builder strings.Builder
820-
builder.WriteString("Network Interfaces:\n")
821-
822-
for _, iface := range interfaces {
823-
builder.WriteString(fmt.Sprintf("\nInterface: %s\n", iface.Name))
824-
builder.WriteString(fmt.Sprintf(" Index: %d\n", iface.Index))
825-
builder.WriteString(fmt.Sprintf(" MTU: %d\n", iface.MTU))
826-
builder.WriteString(fmt.Sprintf(" Flags: %v\n", iface.Flags))
827-
828-
addrs, err := iface.Addrs()
829-
if err != nil {
830-
builder.WriteString(fmt.Sprintf(" Addresses: Error retrieving addresses: %v\n", err))
831-
} else {
832-
builder.WriteString(" Addresses:\n")
833-
for _, addr := range addrs {
834-
prefix, err := netip.ParsePrefix(addr.String())
835-
if err != nil {
836-
builder.WriteString(fmt.Sprintf(" Error parsing address: %v\n", err))
837-
continue
838-
}
839-
ip := prefix.Addr()
840-
if anonymize {
841-
ip = anonymizer.AnonymizeIP(ip)
842-
}
843-
builder.WriteString(fmt.Sprintf(" %s/%d\n", ip, prefix.Bits()))
844-
}
845-
}
846-
}
847-
848-
return builder.String()
849-
}
850-
851839
func anonymizeLog(reader io.Reader, writer *io.PipeWriter, anonymizer *anonymize.Anonymizer) {
852840
defer func() {
853841
// always nil
@@ -954,7 +942,6 @@ func anonymizeRemotePeer(peer *mgmProto.RemotePeerConfig, anonymizer *anonymize.
954942
}
955943

956944
for i, ip := range peer.AllowedIps {
957-
// Try to parse as prefix first (CIDR)
958945
if prefix, err := netip.ParsePrefix(ip); err == nil {
959946
anonIP := anonymizer.AnonymizeIP(prefix.Addr())
960947
peer.AllowedIps[i] = fmt.Sprintf("%s/%d", anonIP, prefix.Bits())
@@ -1033,7 +1020,7 @@ func anonymizeRecords(records []*mgmProto.SimpleRecord, anonymizer *anonymize.An
10331020

10341021
func anonymizeRData(record *mgmProto.SimpleRecord, anonymizer *anonymize.Anonymizer) {
10351022
switch record.Type {
1036-
case 1, 28: // A or AAAA record
1023+
case 1, 28:
10371024
if addr, err := netip.ParseAddr(record.RData); err == nil {
10381025
record.RData = anonymizer.AnonymizeIP(addr).String()
10391026
}

0 commit comments

Comments
 (0)