From 84354951d37fdcff137e0f045cff4190d5ee61a2 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Thu, 5 Jun 2025 13:54:15 +0200 Subject: [PATCH] [client] Add systemd netbird logs to debug bundle (#3917) --- client/cmd/service.go | 10 ++- client/internal/debug/debug.go | 9 ++- client/internal/debug/debug_linux.go | 89 ++++++++++++++++++++++++- client/internal/debug/debug_nonlinux.go | 6 ++ 4 files changed, 110 insertions(+), 4 deletions(-) diff --git a/client/cmd/service.go b/client/cmd/service.go index 005479306..156e67d6d 100644 --- a/client/cmd/service.go +++ b/client/cmd/service.go @@ -2,6 +2,7 @@ package cmd import ( "context" + "runtime" "sync" "github.com/kardianos/service" @@ -27,12 +28,19 @@ func newProgram(ctx context.Context, cancel context.CancelFunc) *program { } func newSVCConfig() *service.Config { - return &service.Config{ + config := &service.Config{ Name: serviceName, DisplayName: "Netbird", Description: "Netbird mesh network client", Option: make(service.KeyValue), + EnvVars: make(map[string]string), } + + if runtime.GOOS == "linux" { + config.EnvVars["SYSTEMD_UNIT"] = serviceName + } + + return config } func newSVC(prg *program, conf *service.Config) (service.Service, error) { diff --git a/client/internal/debug/debug.go b/client/internal/debug/debug.go index a7d873c8f..dfed47f05 100644 --- a/client/internal/debug/debug.go +++ b/client/internal/debug/debug.go @@ -274,10 +274,15 @@ func (g *BundleGenerator) createArchive() error { log.Errorf("Failed to add wg show output: %v", err) } - if g.logFile != "console" { + if g.logFile != "console" && g.logFile != "" { if err := g.addLogfile(); err != nil { - return fmt.Errorf("add log file: %w", 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) + } } + } else if err := g.trySystemdLogFallback(); err != nil { + log.Errorf("Failed to add systemd logs: %v", err) } return nil diff --git a/client/internal/debug/debug_linux.go b/client/internal/debug/debug_linux.go index b4907beca..4626cd9a2 100644 --- a/client/internal/debug/debug_linux.go +++ b/client/internal/debug/debug_linux.go @@ -4,17 +4,104 @@ package debug import ( "bytes" + "context" "encoding/binary" + "errors" "fmt" + "os" "os/exec" "sort" "strings" + "time" "github.com/google/nftables" "github.com/google/nftables/expr" log "github.com/sirupsen/logrus" ) +const ( + maxLogEntries = 100000 + maxLogAge = 7 * 24 * time.Hour // Last 7 days +) + +// trySystemdLogFallback attempts to get logs from systemd journal as fallback +func (g *BundleGenerator) trySystemdLogFallback() error { + log.Debug("Attempting to collect systemd journal logs") + + serviceName := getServiceName() + journalLogs, err := getSystemdLogs(serviceName) + if err != nil { + return fmt.Errorf("get systemd logs for %s: %w", serviceName, err) + } + + if strings.Contains(journalLogs, "No recent log entries found") { + log.Debug("No recent log entries found in systemd journal") + return nil + } + + if g.anonymize { + journalLogs = g.anonymizer.AnonymizeString(journalLogs) + } + + logReader := strings.NewReader(journalLogs) + fileName := fmt.Sprintf("systemd-%s.log", serviceName) + if err := g.addFileToZip(logReader, fileName); err != nil { + return fmt.Errorf("add systemd logs to bundle: %w", err) + } + + log.Infof("Added systemd journal logs for %s to debug bundle", serviceName) + return nil +} + +// getServiceName gets the service name from environment or defaults to netbird +func getServiceName() string { + if unitName := os.Getenv("SYSTEMD_UNIT"); unitName != "" { + log.Debugf("Detected SYSTEMD_UNIT environment variable: %s", unitName) + return unitName + } + + return "netbird" +} + +// getSystemdLogs retrieves logs from systemd journal for a specific service using journalctl +func getSystemdLogs(serviceName string) (string, error) { + args := []string{ + "-u", fmt.Sprintf("%s.service", serviceName), + "--since", fmt.Sprintf("-%s", maxLogAge.String()), + "--lines", fmt.Sprintf("%d", maxLogEntries), + "--no-pager", + "--output", "short-iso", + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, "journalctl", args...) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + return "", fmt.Errorf("journalctl command timed out after 30 seconds") + } + if strings.Contains(err.Error(), "executable file not found") { + return "", fmt.Errorf("journalctl command not found: %w", err) + } + return "", fmt.Errorf("execute journalctl: %w (stderr: %s)", err, stderr.String()) + } + + logs := stdout.String() + if strings.TrimSpace(logs) == "" { + return "No recent log entries found in systemd journal", nil + } + + header := fmt.Sprintf("=== Systemd Journal Logs for %s.service (last %d entries, max %s) ===\n", + serviceName, maxLogEntries, maxLogAge.String()) + + return header + logs, nil +} + // addFirewallRules collects and adds firewall rules to the archive func (g *BundleGenerator) addFirewallRules() error { log.Info("Collecting firewall rules") @@ -481,7 +568,7 @@ func formatExpr(exp expr.Any) string { case *expr.Fib: return formatFib(e) case *expr.Target: - return fmt.Sprintf("jump %s", e.Name) // Properly format jump targets + return fmt.Sprintf("jump %s", e.Name) case *expr.Immediate: if e.Register == 1 { return formatImmediateData(e.Data) diff --git a/client/internal/debug/debug_nonlinux.go b/client/internal/debug/debug_nonlinux.go index ef93620a0..b0ff55613 100644 --- a/client/internal/debug/debug_nonlinux.go +++ b/client/internal/debug/debug_nonlinux.go @@ -6,3 +6,9 @@ package debug func (g *BundleGenerator) addFirewallRules() error { return nil } + +func (g *BundleGenerator) trySystemdLogFallback() error { + // Systemd is only available on Linux + // TODO: Add BSD support + return nil +}