Files
netbird/client/ui/services/debug.go
T
riccardom 9f6d17b9e8 [management,client] Take the debug-bundle upload destination from management
The debug-bundle paths that upload without a human picking a destination
compiled the vendor endpoint in: the mobile clients and the desktop UI hold
`https://upload.debug.netbird.io/upload-url` as a constant, the CLI defaults its
flag to it, and the remote job falls back to it when nothing else is set. A
self-hosted deployment therefore shipped peer logs, routes, DNS and firewall
state to NetBird-run infrastructure without its operator ever configuring that,
and had no way to point those paths anywhere else. #7147 and #7153 gave the
remote job a per-job URL and an MDM override, but neither reaches the mobile,
UI or CLI paths, and both fail open when unset.

Publish the destination from the management server instead, on the channel that
already carries stun/turn/signal/relay/flow/metrics:

- `NetbirdConfig.debug.upload_url`, sourced from the new account setting
  `debug_bundle_upload_url` (REST + dashboard) and falling back to the new
  `DebugUpload.URL` in the management server config, which a self-hosted install
  can set once so a fresh account is not left on the vendor default. Both are
  validated as https-with-host where they are written; a change fans out to
  connected peers rather than waiting for the next login.
- One resolver on the client, `debug.ResolveUploadURL`, used by every path:
  MDM override > explicitly named URL > destination published by management >
  the NetBird service, but only for a peer enrolled with NetBird's cloud.
  Anything else fails closed with ErrNoUploadDestination and the bundle stays
  local, which is the behaviour change: a self-hosted deployment that names no
  upload service no longer uploads at all.
- The engine keeps the published value (`Engine.DebugUploadURL`) so the bundle
  paths, which run off the engine loop, do not have to read it back out of the
  opt-in sync-response store.
- The daemon request grows `upload`, so "upload to wherever this deployment
  says" is expressible; an empty `uploadURL` no longer has to mean "no upload".
  The privilege gate is unchanged and still applies only to a URL the local
  caller named — a destination published by management is the operator naming
  their own service.
- The desktop UI stops carrying a vendor URL of its own and sends the intent.

Reported privately as GHSA-hf99-43rj-h577.
2026-09-10 16:38:41 +02:00

145 lines
4.1 KiB
Go

//go:build !android && !ios && !freebsd && !js
package services
import (
"context"
"fmt"
"strings"
"time"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/version"
)
type DebugBundleParams struct {
Anonymize bool `json:"anonymize"`
// AnonymizeLevel is "default" or "strict"; strict also anonymizes
// private IP ranges, peer names, and WireGuard public keys.
AnonymizeLevel string `json:"anonymizeLevel"`
SystemInfo bool `json:"systemInfo"`
// Upload asks the daemon to upload the bundle. The UI carries no
// destination of its own: the daemon resolves it from what the management
// server publishes, so a self-hosted deployment's bundles do not leave the
// operator's control sphere.
Upload bool `json:"upload"`
LogFileCount uint32 `json:"logFileCount"`
}
// DebugBundleResult: Path is set for local-only bundles, UploadedKey on upload
// success, UploadFailureReason on upload failure.
type DebugBundleResult struct {
Path string `json:"path"`
UploadedKey string `json:"uploadedKey"`
UploadFailureReason string `json:"uploadFailureReason"`
}
// LogLevel carries a logrus level name: "error", "warn", "info", "debug", "trace".
type LogLevel struct {
Level string `json:"level"`
}
type Debug struct {
conn DaemonConn
}
func NewDebug(conn DaemonConn) *Debug {
return &Debug{conn: conn}
}
func (s *Debug) Bundle(ctx context.Context, p DebugBundleParams) (DebugBundleResult, error) {
cli, err := s.conn.Client()
if err != nil {
return DebugBundleResult{}, err
}
resp, err := cli.DebugBundle(ctx, &proto.DebugBundleRequest{
Anonymize: p.Anonymize,
AnonymizeLevel: p.AnonymizeLevel,
SystemInfo: p.SystemInfo,
Upload: p.Upload,
LogFileCount: p.LogFileCount,
CliVersion: version.NetbirdVersion(),
})
if err != nil {
return DebugBundleResult{}, err
}
return DebugBundleResult{
Path: resp.GetPath(),
UploadedKey: resp.GetUploadedKey(),
UploadFailureReason: resp.GetUploadFailureReason(),
}, nil
}
func (s *Debug) GetLogLevel(ctx context.Context) (LogLevel, error) {
cli, err := s.conn.Client()
if err != nil {
return LogLevel{}, err
}
resp, err := cli.GetLogLevel(ctx, &proto.GetLogLevelRequest{})
if err != nil {
return LogLevel{}, err
}
return LogLevel{Level: resp.GetLevel().String()}, nil
}
// RevealFile opens the OS file manager focused on path. Needed because Wails'
// Browser.OpenURL refuses non-http(s) schemes like file://.
func (s *Debug) RevealFile(_ context.Context, path string) error {
if path == "" {
return fmt.Errorf("empty path")
}
return revealFile(path)
}
// RegisterUILog reports the GUI log path to the daemon for bundle collection;
// the daemon runs as root and can't resolve the user's config dir. Called on
// each daemon (re)connect.
func (s *Debug) RegisterUILog(ctx context.Context, path string) error {
cli, err := s.conn.Client()
if err != nil {
return err
}
_, err = cli.RegisterUILog(ctx, &proto.RegisterUILogRequest{Path: path})
return err
}
func (s *Debug) StartBundleCapture(ctx context.Context, timeoutSeconds int32) error {
cli, err := s.conn.Client()
if err != nil {
return err
}
req := &proto.StartBundleCaptureRequest{}
if timeoutSeconds > 0 {
req.Timeout = durationpb.New(time.Duration(timeoutSeconds) * time.Second)
}
_, err = cli.StartBundleCapture(ctx, req)
return err
}
func (s *Debug) StopBundleCapture(ctx context.Context) error {
cli, err := s.conn.Client()
if err != nil {
return err
}
_, err = cli.StopBundleCapture(ctx, &proto.StopBundleCaptureRequest{})
return err
}
func (s *Debug) SetLogLevel(ctx context.Context, lvl LogLevel) error {
cli, err := s.conn.Client()
if err != nil {
return err
}
// proto.LogLevel_value keys are upper-case enum names; callers pass
// lowercase logrus names. Upper-case before lookup or a valid level
// silently falls through to INFO.
level, ok := proto.LogLevel_value[strings.ToUpper(lvl.Level)]
if !ok {
level = int32(proto.LogLevel_INFO)
}
_, err = cli.SetLogLevel(ctx, &proto.SetLogLevelRequest{Level: proto.LogLevel(level)})
return err
}