mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-12 17:59:06 +02:00
[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.
This commit is contained in:
@@ -32,7 +32,6 @@ import (
|
||||
"github.com/netbirdio/netbird/formatter"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
"github.com/netbirdio/netbird/shared/management/domain"
|
||||
types "github.com/netbirdio/netbird/upload-server/types"
|
||||
)
|
||||
|
||||
// AnonymizeLevelDefault and AnonymizeLevelStrict are the accepted
|
||||
@@ -349,6 +348,11 @@ func (c *Client) DebugBundle(platformFiles PlatformFiles, anonymize bool, anonym
|
||||
StatePath: platformFiles.StateFilePath(),
|
||||
}
|
||||
|
||||
// Empty unless an engine is running and has synced: a bundle generated with
|
||||
// the client stopped has no management-published destination, so it uploads
|
||||
// only when the peer is enrolled with NetBird's cloud.
|
||||
var publishedUploadURL string
|
||||
|
||||
if cc != nil {
|
||||
resp, err := cc.GetLatestSyncResponse()
|
||||
if err != nil {
|
||||
@@ -357,6 +361,7 @@ func (c *Client) DebugBundle(platformFiles PlatformFiles, anonymize bool, anonym
|
||||
deps.SyncResponse = resp
|
||||
|
||||
if e := cc.Engine(); e != nil {
|
||||
publishedUploadURL = e.DebugUploadURL()
|
||||
deps.RefreshStatus = func() {
|
||||
e.RunHealthProbes(context.Background(), true)
|
||||
}
|
||||
@@ -375,6 +380,16 @@ func (c *Client) DebugBundle(platformFiles PlatformFiles, anonymize bool, anonym
|
||||
},
|
||||
)
|
||||
|
||||
// Resolved before the bundle is generated: with no destination there is
|
||||
// nothing to hand back to the app, and generating (then deleting) a bundle
|
||||
// nobody can collect is wasted work on the device. An MDM override wins;
|
||||
// otherwise the destination this deployment publishes is used, and only a
|
||||
// peer enrolled with NetBird's cloud falls back to the service NetBird runs.
|
||||
uploadURL, err := debug.ResolveUploadURL(cfg.DebugBundleUploadURL, publishedUploadURL, cfg.ManagementURL.String())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
path, err := bundleGenerator.Generate()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("generate debug bundle: %w", err)
|
||||
@@ -388,7 +403,7 @@ func (c *Client) DebugBundle(platformFiles PlatformFiles, anonymize bool, anonym
|
||||
uploadCtx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
key, err := debug.UploadDebugBundle(uploadCtx, types.DefaultBundleURL, cfg.ManagementURL.String(), path, false)
|
||||
key, err := debug.UploadDebugBundle(uploadCtx, uploadURL, cfg.ManagementURL.String(), path, false)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("upload debug bundle: %w", err)
|
||||
}
|
||||
|
||||
+32
-7
@@ -2,6 +2,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -19,12 +20,19 @@ import (
|
||||
"github.com/netbirdio/netbird/client/proto"
|
||||
"github.com/netbirdio/netbird/client/server"
|
||||
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
|
||||
"github.com/netbirdio/netbird/upload-server/types"
|
||||
"github.com/netbirdio/netbird/version"
|
||||
)
|
||||
|
||||
const errCloseConnection = "Failed to close connection: %v"
|
||||
|
||||
// uploadBundleURLUsage documents that an empty flag is not "no upload" but
|
||||
// "wherever this deployment says": the daemon takes the destination from the
|
||||
// management server, and only falls back to the service NetBird runs for a peer
|
||||
// enrolled with NetBird's cloud. Naming another one requires root, since the
|
||||
// daemon fetches the URL and PUTs its own logs and state to whatever it returns.
|
||||
const uploadBundleURLUsage = "Upload service URL to get an upload URL from. " +
|
||||
"Defaults to the one the management server publishes; requires root when set explicitly"
|
||||
|
||||
var (
|
||||
logFileCount uint32
|
||||
systemInfoFlag bool
|
||||
@@ -179,6 +187,7 @@ func debugBundle(cmd *cobra.Command, _ []string) error {
|
||||
CliVersion: version.NetbirdVersion(),
|
||||
}
|
||||
if uploadBundleFlag {
|
||||
request.Upload = true
|
||||
request.UploadURL = uploadBundleURLFlag
|
||||
request.UploadInsecure = uploadBundleInsecureFlag
|
||||
}
|
||||
@@ -192,8 +201,8 @@ func debugBundle(cmd *cobra.Command, _ []string) error {
|
||||
return fmt.Errorf("upload failed: %s", resp.GetUploadFailureReason())
|
||||
}
|
||||
|
||||
if uploadBundleFlag {
|
||||
cmd.Printf("Upload file key:\n%s\n", resp.GetUploadedKey())
|
||||
if err := printUploadKey(cmd, resp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -385,6 +394,7 @@ func runForDuration(cmd *cobra.Command, args []string) error {
|
||||
CliVersion: version.NetbirdVersion(),
|
||||
}
|
||||
if uploadBundleFlag {
|
||||
request.Upload = true
|
||||
request.UploadURL = uploadBundleURLFlag
|
||||
request.UploadInsecure = uploadBundleInsecureFlag
|
||||
}
|
||||
@@ -423,8 +433,8 @@ func runForDuration(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("upload failed: %s", resp.GetUploadFailureReason())
|
||||
}
|
||||
|
||||
if uploadBundleFlag {
|
||||
cmd.Printf("Upload file key:\n%s\n", resp.GetUploadedKey())
|
||||
if err := printUploadKey(cmd, resp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -533,17 +543,32 @@ func generateDebugBundle(config *profilemanager.Config, recorder *peer.Status, c
|
||||
log.Infof("Generated debug bundle from SIGUSR1 at: %s", path)
|
||||
}
|
||||
|
||||
// printUploadKey reports the upload key, or why there is none. A daemon that
|
||||
// predates the destination-from-management change ignores an empty upload URL
|
||||
// and returns neither a key nor a failure reason, which would otherwise print
|
||||
// as an empty key.
|
||||
func printUploadKey(cmd *cobra.Command, resp *proto.DebugBundleResponse) error {
|
||||
if !uploadBundleFlag {
|
||||
return nil
|
||||
}
|
||||
if resp.GetUploadedKey() == "" {
|
||||
return errors.New("the daemon did not upload the bundle; pass --upload-bundle-url explicitly or update the daemon")
|
||||
}
|
||||
cmd.Printf("Upload file key:\n%s\n", resp.GetUploadedKey())
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
debugBundleCmd.Flags().Uint32VarP(&logFileCount, "log-file-count", "C", 1, "Number of rotated log files to include in debug bundle")
|
||||
debugBundleCmd.Flags().BoolVarP(&systemInfoFlag, "system-info", "S", true, "Adds system information to the debug bundle")
|
||||
debugBundleCmd.Flags().BoolVarP(&uploadBundleFlag, "upload-bundle", "U", false, "Uploads the debug bundle to a server")
|
||||
debugBundleCmd.Flags().StringVar(&uploadBundleURLFlag, "upload-bundle-url", types.DefaultBundleURL, "Service URL to get an URL to upload the debug bundle")
|
||||
debugBundleCmd.Flags().StringVar(&uploadBundleURLFlag, "upload-bundle-url", "", uploadBundleURLUsage)
|
||||
debugBundleCmd.Flags().BoolVar(&uploadBundleInsecureFlag, "upload-bundle-insecure", false, "Allow uploading to an http or untrusted-TLS upload server (self-hosted); requires root")
|
||||
|
||||
forCmd.Flags().Uint32VarP(&logFileCount, "log-file-count", "C", 1, "Number of rotated log files to include in debug bundle")
|
||||
forCmd.Flags().BoolVarP(&systemInfoFlag, "system-info", "S", true, "Adds system information to the debug bundle")
|
||||
forCmd.Flags().BoolVarP(&uploadBundleFlag, "upload-bundle", "U", false, "Uploads the debug bundle to a server")
|
||||
forCmd.Flags().StringVar(&uploadBundleURLFlag, "upload-bundle-url", types.DefaultBundleURL, "Service URL to get an URL to upload the debug bundle")
|
||||
forCmd.Flags().StringVar(&uploadBundleURLFlag, "upload-bundle-url", "", uploadBundleURLUsage)
|
||||
forCmd.Flags().BoolVar(&uploadBundleInsecureFlag, "upload-bundle-insecure", false, "Allow uploading to an http or untrusted-TLS upload server (self-hosted); requires root")
|
||||
forCmd.Flags().Bool("capture", false, "Capture packets during the debug duration and include in bundle")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal/metrics"
|
||||
"github.com/netbirdio/netbird/upload-server/types"
|
||||
)
|
||||
|
||||
// ErrNoUploadDestination reports that a bundle has nowhere to go: the
|
||||
// management server of this deployment publishes no upload service, and the
|
||||
// peer is not enrolled with NetBird's cloud either. A debug bundle carries the
|
||||
// peer's logs, routes, DNS and firewall state, so the default is to keep it
|
||||
// inside the operator's control sphere rather than fall back to the service
|
||||
// NetBird runs.
|
||||
var ErrNoUploadDestination = errors.New("this deployment publishes no debug bundle upload service; set it on the account settings or in the management server config, or pass an explicit upload URL")
|
||||
|
||||
// ResolveUploadURL decides where a debug bundle may be uploaded.
|
||||
//
|
||||
// requested is a destination a caller named explicitly (a CLI flag, the daemon
|
||||
// request); it always wins, and the callers that accept one gate it separately.
|
||||
// published is what the management server of this deployment advertises, which
|
||||
// the engine holds (Engine.DebugUploadURL). With neither, only a peer enrolled
|
||||
// with NetBird's cloud falls back to the service NetBird runs — for anyone else
|
||||
// that would carry the bundle out of the deployment the operator controls, so it
|
||||
// fails closed with ErrNoUploadDestination.
|
||||
func ResolveUploadURL(requested, published, managementURL string) (string, error) {
|
||||
if requested != "" {
|
||||
return requested, nil
|
||||
}
|
||||
|
||||
if published != "" {
|
||||
return published, nil
|
||||
}
|
||||
|
||||
if metrics.DetermineDeploymentType(managementURL) == metrics.DeploymentTypeCloud {
|
||||
return types.DefaultBundleURL, nil
|
||||
}
|
||||
|
||||
return "", ErrNoUploadDestination
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/upload-server/types"
|
||||
)
|
||||
|
||||
func TestResolveUploadURL(t *testing.T) {
|
||||
const (
|
||||
cloudMgm = "https://api.netbird.io:443"
|
||||
selfHostedMgm = "https://netbird.example.com:33073"
|
||||
operatorURL = "https://upload.example.com/upload-url"
|
||||
requestedURL = "https://requested.example.com/upload-url"
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
requested string
|
||||
published string
|
||||
managementURL string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "requested wins over published",
|
||||
requested: requestedURL,
|
||||
published: operatorURL,
|
||||
managementURL: selfHostedMgm,
|
||||
want: requestedURL,
|
||||
},
|
||||
{
|
||||
name: "requested wins on cloud too",
|
||||
requested: requestedURL,
|
||||
managementURL: cloudMgm,
|
||||
want: requestedURL,
|
||||
},
|
||||
{
|
||||
name: "published used when nothing requested",
|
||||
published: operatorURL,
|
||||
managementURL: selfHostedMgm,
|
||||
want: operatorURL,
|
||||
},
|
||||
{
|
||||
// A cloud deployment publishing its own destination must not be
|
||||
// overridden by the compiled-in default.
|
||||
name: "published wins over the cloud fallback",
|
||||
published: operatorURL,
|
||||
managementURL: cloudMgm,
|
||||
want: operatorURL,
|
||||
},
|
||||
{
|
||||
name: "cloud falls back to the NetBird service",
|
||||
managementURL: cloudMgm,
|
||||
want: types.DefaultBundleURL,
|
||||
},
|
||||
{
|
||||
// The whole point of GHSA-hf99-43rj-h577: no silent hop to a
|
||||
// vendor-controlled destination.
|
||||
name: "self-hosted with no destination fails closed",
|
||||
managementURL: selfHostedMgm,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "unknown management URL fails closed",
|
||||
managementURL: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := ResolveUploadURL(tc.requested, tc.published, tc.managementURL)
|
||||
if tc.wantErr {
|
||||
require.ErrorIs(t, err, ErrNoUploadDestination)
|
||||
assert.Empty(t, got)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
@@ -226,6 +227,14 @@ type Engine struct {
|
||||
TURNs []*stun.URI
|
||||
stunTurn icemaker.StunTurn
|
||||
|
||||
// debugUploadURL is the debug-bundle upload service the management server
|
||||
// publishes for this deployment, refreshed on every NetbirdConfig update.
|
||||
// Atomic because the bundle paths (remote job, daemon RPC, mobile SDK) read
|
||||
// it off the engine loop. Empty when the deployment publishes none, which is
|
||||
// what makes a self-hosted peer keep its bundle local instead of shipping it
|
||||
// to the upload service NetBird runs.
|
||||
debugUploadURL atomic.Pointer[string]
|
||||
|
||||
clientCtx context.Context
|
||||
clientCancel context.CancelFunc
|
||||
|
||||
@@ -1144,6 +1153,8 @@ func (e *Engine) updateNetbirdConfig(wCfg *mgmProto.NetbirdConfig) error {
|
||||
|
||||
e.handleMetricsUpdate(wCfg.GetMetrics())
|
||||
|
||||
e.handleDebugUploadUpdate(wCfg.GetDebug())
|
||||
|
||||
if err := e.PopulateNetbirdConfig(wCfg, nil); err != nil {
|
||||
log.Warnf("Failed to update DNS server config: %v", err)
|
||||
}
|
||||
@@ -1221,6 +1232,26 @@ func (e *Engine) handleMetricsUpdate(config *mgmProto.MetricsConfig) {
|
||||
e.clientMetrics.UpdatePushFromMgm(e.metricsCtx, config.GetEnabled())
|
||||
}
|
||||
|
||||
// handleDebugUploadUpdate records the debug-bundle destination the management
|
||||
// server published. A nil DebugConfig clears it: a management server that stops
|
||||
// publishing a destination must take it away from the peer, not leave the peer
|
||||
// uploading to a host the operator has since removed.
|
||||
func (e *Engine) handleDebugUploadUpdate(config *mgmProto.DebugConfig) {
|
||||
url := config.GetUploadUrl()
|
||||
e.debugUploadURL.Store(&url)
|
||||
}
|
||||
|
||||
// DebugUploadURL returns the debug-bundle upload service the management server
|
||||
// published, or empty when it published none or the engine never synced. The
|
||||
// callers treat empty as "no destination from this deployment" and fail closed
|
||||
// unless the peer is enrolled with NetBird's cloud; see debug.ResolveUploadURL.
|
||||
func (e *Engine) DebugUploadURL() string {
|
||||
if url := e.debugUploadURL.Load(); url != nil {
|
||||
return *url
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func toFlowLoggerConfig(config *mgmProto.FlowConfig) (*nftypes.FlowConfig, error) {
|
||||
if config.GetInterval() == nil {
|
||||
return nil, errors.New("flow interval is nil")
|
||||
@@ -1428,9 +1459,17 @@ func (e *Engine) handleBundle(params *mgmProto.BundleParameters) (*mgmProto.JobR
|
||||
params.GetAnonymize(), params.GetAnonymizeLevel(), params.GetLogFileCount(), params.GetBundleFor(), params.GetBundleForTime())
|
||||
log.Debugf("remote debug bundle request parameters: %s", params.String())
|
||||
|
||||
syncResponse, err := e.GetLatestSyncResponse()
|
||||
if err != nil {
|
||||
log.Warnf("get latest sync response: %v", err)
|
||||
}
|
||||
|
||||
// Resolve the upload destination: an MDM override, when set, takes
|
||||
// precedence over the management-supplied URL. Both are validated the same
|
||||
// way; an empty result falls back to the default upload server downstream.
|
||||
// precedence over the job's URL. Both are validated the same way. With
|
||||
// neither, the destination this deployment publishes is used, and only a
|
||||
// peer enrolled with NetBird's cloud falls back to the service NetBird runs
|
||||
// — a self-hosted deployment that named no upload service gets no upload
|
||||
// rather than one that leaves the operator's control sphere.
|
||||
uploadURL := params.GetUploadUrl()
|
||||
if override := e.config.ProfileConfig.DebugBundleUploadURL; override != "" {
|
||||
log.Infof("using MDM debug bundle upload URL override instead of the management-supplied value")
|
||||
@@ -1440,9 +1479,9 @@ func (e *Engine) handleBundle(params *mgmProto.BundleParameters) (*mgmProto.JobR
|
||||
return nil, err
|
||||
}
|
||||
|
||||
syncResponse, err := e.GetLatestSyncResponse()
|
||||
uploadURL, err = debug.ResolveUploadURL(uploadURL, e.DebugUploadURL(), e.config.ProfileConfig.ManagementURL.String())
|
||||
if err != nil {
|
||||
log.Warnf("get latest sync response: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bundleDeps := debug.GeneratorDependencies{
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
|
||||
)
|
||||
|
||||
// TestValidateBundleUploadURL covers the sanity check applied to a
|
||||
@@ -15,7 +17,7 @@ func TestValidateBundleUploadURL(t *testing.T) {
|
||||
raw string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "empty falls back to default", raw: ""},
|
||||
{name: "empty defers to the deployment destination", raw: ""},
|
||||
{name: "https with host", raw: "https://upload.debug.netbird.io/upload"},
|
||||
{name: "https self-hosted host", raw: "https://upload.example.com"},
|
||||
{name: "plaintext rejected", raw: "http://upload.example.com", wantErr: true},
|
||||
@@ -34,3 +36,22 @@ func TestValidateBundleUploadURL(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestEngineDebugUploadURL covers the destination the management server
|
||||
// publishes: the engine keeps the last value it saw so the bundle paths, which
|
||||
// run off the engine loop, do not have to re-read a sync response.
|
||||
func TestEngineDebugUploadURL(t *testing.T) {
|
||||
e := &Engine{}
|
||||
assert.Empty(t, e.DebugUploadURL(), "a peer that never synced publishes no destination")
|
||||
|
||||
e.handleDebugUploadUpdate(nil)
|
||||
assert.Empty(t, e.DebugUploadURL(), "a management server predating the field publishes none")
|
||||
|
||||
e.handleDebugUploadUpdate(&mgmProto.DebugConfig{UploadUrl: "https://upload.example.com/upload-url"})
|
||||
assert.Equal(t, "https://upload.example.com/upload-url", e.DebugUploadURL())
|
||||
|
||||
// An operator that removes the destination must take it away from the peer,
|
||||
// not leave it uploading to a host that no longer exists.
|
||||
e.handleDebugUploadUpdate(&mgmProto.DebugConfig{})
|
||||
assert.Empty(t, e.DebugUploadURL())
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ import (
|
||||
"github.com/netbirdio/netbird/formatter"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
"github.com/netbirdio/netbird/shared/management/domain"
|
||||
types "github.com/netbirdio/netbird/upload-server/types"
|
||||
)
|
||||
|
||||
// AnonymizeLevelDefault and AnonymizeLevelStrict are the accepted
|
||||
@@ -298,6 +297,11 @@ func (c *Client) DebugBundle(anonymize bool, anonymizeLevel string) (string, err
|
||||
LogPath: c.logFilePath,
|
||||
}
|
||||
|
||||
// Empty unless an engine is running and has synced: a bundle generated with
|
||||
// the client stopped has no management-published destination, so it uploads
|
||||
// only when the peer is enrolled with NetBird's cloud.
|
||||
var publishedUploadURL string
|
||||
|
||||
if cc != nil {
|
||||
resp, err := cc.GetLatestSyncResponse()
|
||||
if err != nil {
|
||||
@@ -306,6 +310,7 @@ func (c *Client) DebugBundle(anonymize bool, anonymizeLevel string) (string, err
|
||||
deps.SyncResponse = resp
|
||||
|
||||
if e := cc.Engine(); e != nil {
|
||||
publishedUploadURL = e.DebugUploadURL()
|
||||
deps.RefreshStatus = func() {
|
||||
e.RunHealthProbes(context.Background(), true)
|
||||
}
|
||||
@@ -324,6 +329,16 @@ func (c *Client) DebugBundle(anonymize bool, anonymizeLevel string) (string, err
|
||||
},
|
||||
)
|
||||
|
||||
// Resolved before the bundle is generated: with no destination there is
|
||||
// nothing to hand back to the app, and generating (then deleting) a bundle
|
||||
// nobody can collect is wasted work on the device. An MDM override wins;
|
||||
// otherwise the destination this deployment publishes is used, and only a
|
||||
// peer enrolled with NetBird's cloud falls back to the service NetBird runs.
|
||||
uploadURL, err := debug.ResolveUploadURL(cfg.DebugBundleUploadURL, publishedUploadURL, cfg.ManagementURL.String())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
path, err := bundleGenerator.Generate()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("generate debug bundle: %w", err)
|
||||
@@ -337,7 +352,7 @@ func (c *Client) DebugBundle(anonymize bool, anonymizeLevel string) (string, err
|
||||
uploadCtx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
key, err := debug.UploadDebugBundle(uploadCtx, types.DefaultBundleURL, cfg.ManagementURL.String(), path, false)
|
||||
key, err := debug.UploadDebugBundle(uploadCtx, uploadURL, cfg.ManagementURL.String(), path, false)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("upload debug bundle: %w", err)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal/debug"
|
||||
"github.com/netbirdio/netbird/upload-server/types"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -28,9 +27,14 @@ func NewExecutor() *Executor {
|
||||
return &Executor{}
|
||||
}
|
||||
|
||||
// BundleJob generates a debug bundle for a remote job and uploads it to
|
||||
// uploadURL, returning the key the management server hands back to whoever asked.
|
||||
// The caller resolves uploadURL (see debug.ResolveUploadURL): a job whose
|
||||
// deployment names no upload service never reaches here, so there is no
|
||||
// fallback destination to pick locally.
|
||||
func (e *Executor) BundleJob(ctx context.Context, debugBundleDependencies debug.GeneratorDependencies, params debug.BundleConfig, waitForDuration time.Duration, mgmURL, uploadURL string) (string, error) {
|
||||
if uploadURL == "" {
|
||||
uploadURL = types.DefaultBundleURL
|
||||
return "", debug.ErrNoUploadDestination
|
||||
}
|
||||
|
||||
if waitForDuration > MaxBundleWaitTime {
|
||||
|
||||
@@ -2820,8 +2820,15 @@ type DebugBundleRequest struct {
|
||||
// Unknown values are treated as "strict". Only meaningful with anonymize;
|
||||
// "strict" implies it.
|
||||
AnonymizeLevel string `protobuf:"bytes,8,opt,name=anonymizeLevel,proto3" json:"anonymizeLevel,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
// upload asks the daemon to upload the bundle. When uploadURL is empty the
|
||||
// daemon resolves the destination itself: the one the management server
|
||||
// publishes, or the service NetBird runs when the peer is enrolled with
|
||||
// NetBird's cloud. A self-hosted deployment that publishes no destination
|
||||
// gets no upload, so a bundle never leaves the operator's control sphere by
|
||||
// default. uploadURL still overrides it, for a privileged caller.
|
||||
Upload bool `protobuf:"varint,9,opt,name=upload,proto3" json:"upload,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DebugBundleRequest) Reset() {
|
||||
@@ -2903,6 +2910,13 @@ func (x *DebugBundleRequest) GetAnonymizeLevel() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *DebugBundleRequest) GetUpload() bool {
|
||||
if x != nil {
|
||||
return x.Upload
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type DebugBundleResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"`
|
||||
@@ -7354,7 +7368,7 @@ const file_daemon_proto_rawDesc = "" +
|
||||
"\x12translatedHostname\x18\x04 \x01(\tR\x12translatedHostname\x128\n" +
|
||||
"\x0etranslatedPort\x18\x05 \x01(\v2\x10.daemon.PortInfoR\x0etranslatedPort\"G\n" +
|
||||
"\x17ForwardingRulesResponse\x12,\n" +
|
||||
"\x05rules\x18\x01 \x03(\v2\x16.daemon.ForwardingRuleR\x05rules\"\x84\x02\n" +
|
||||
"\x05rules\x18\x01 \x03(\v2\x16.daemon.ForwardingRuleR\x05rules\"\x9c\x02\n" +
|
||||
"\x12DebugBundleRequest\x12\x1c\n" +
|
||||
"\tanonymize\x18\x01 \x01(\bR\tanonymize\x12\x1e\n" +
|
||||
"\n" +
|
||||
@@ -7366,7 +7380,8 @@ const file_daemon_proto_rawDesc = "" +
|
||||
"cliVersion\x18\x06 \x01(\tR\n" +
|
||||
"cliVersion\x12&\n" +
|
||||
"\x0euploadInsecure\x18\a \x01(\bR\x0euploadInsecure\x12&\n" +
|
||||
"\x0eanonymizeLevel\x18\b \x01(\tR\x0eanonymizeLevel\"}\n" +
|
||||
"\x0eanonymizeLevel\x18\b \x01(\tR\x0eanonymizeLevel\x12\x16\n" +
|
||||
"\x06upload\x18\t \x01(\bR\x06upload\"}\n" +
|
||||
"\x13DebugBundleResponse\x12\x12\n" +
|
||||
"\x04path\x18\x01 \x01(\tR\x04path\x12 \n" +
|
||||
"\vuploadedKey\x18\x02 \x01(\tR\vuploadedKey\x120\n" +
|
||||
|
||||
@@ -553,6 +553,13 @@ message DebugBundleRequest {
|
||||
// Unknown values are treated as "strict". Only meaningful with anonymize;
|
||||
// "strict" implies it.
|
||||
string anonymizeLevel = 8;
|
||||
// upload asks the daemon to upload the bundle. When uploadURL is empty the
|
||||
// daemon resolves the destination itself: the one the management server
|
||||
// publishes, or the service NetBird runs when the peer is enrolled with
|
||||
// NetBird's cloud. A self-hosted deployment that publishes no destination
|
||||
// gets no upload, so a bundle never leaves the operator's control sphere by
|
||||
// default. uploadURL still overrides it, for a privileged caller.
|
||||
bool upload = 9;
|
||||
}
|
||||
|
||||
message DebugBundleResponse {
|
||||
|
||||
+22
-10
@@ -35,35 +35,46 @@ func (s *Server) DebugBundle(callerCtx context.Context, req *proto.DebugBundleRe
|
||||
// socket that carries no identity, which skips the UI log.
|
||||
callerID, callerIdentified := ipcauth.CallerIdentity(callerCtx)
|
||||
|
||||
path, managementURL, err := s.generateDebugBundle(req, uiLogOpener(callerID, callerIdentified))
|
||||
path, managementURL, publishedUploadURL, err := s.generateDebugBundle(req, uiLogOpener(callerID, callerIdentified))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if req.GetUploadURL() == "" {
|
||||
if !req.GetUpload() && req.GetUploadURL() == "" {
|
||||
return &proto.DebugBundleResponse{Path: path}, nil
|
||||
}
|
||||
|
||||
// The destination the management server publishes needs no privilege check:
|
||||
// it is the operator of this deployment naming their own upload service, and
|
||||
// the peer already trusts that server for its whole configuration. Only a
|
||||
// URL the local caller named goes through requirePrivilegeForUploadURL above.
|
||||
uploadURL, err := debug.ResolveUploadURL(req.GetUploadURL(), publishedUploadURL, managementURL)
|
||||
if err != nil {
|
||||
log.Errorf("cannot upload debug bundle: %v", err)
|
||||
return &proto.DebugBundleResponse{Path: path, UploadFailureReason: err.Error()}, nil
|
||||
}
|
||||
|
||||
// The upload runs without s.mutex held: it does network I/O to a possibly
|
||||
// slow destination and must not block the other RPCs that take the lock. The
|
||||
// bounded context is a backstop against a hung connection.
|
||||
uploadCtx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
key, err := debug.UploadDebugBundle(uploadCtx, req.GetUploadURL(), managementURL, path, req.GetUploadInsecure())
|
||||
key, err := debug.UploadDebugBundle(uploadCtx, uploadURL, managementURL, path, req.GetUploadInsecure())
|
||||
if err != nil {
|
||||
log.Errorf("failed to upload debug bundle to %s: %v", req.GetUploadURL(), err)
|
||||
log.Errorf("failed to upload debug bundle to %s: %v", uploadURL, err)
|
||||
return &proto.DebugBundleResponse{Path: path, UploadFailureReason: err.Error()}, nil
|
||||
}
|
||||
|
||||
log.Infof("debug bundle uploaded to %s with key %s", req.GetUploadURL(), key)
|
||||
log.Infof("debug bundle uploaded to %s with key %s", uploadURL, key)
|
||||
|
||||
return &proto.DebugBundleResponse{Path: path, UploadedKey: key}, nil
|
||||
}
|
||||
|
||||
// generateDebugBundle builds the bundle under s.mutex and returns its path plus
|
||||
// the management URL captured under the lock, so the caller can run the upload
|
||||
// without holding the lock.
|
||||
func (s *Server) generateDebugBundle(req *proto.DebugBundleRequest, uiOpener debug.LogOpener) (path string, managementURL string, err error) {
|
||||
// the management URL and the upload service the management server publishes,
|
||||
// both captured under the lock, so the caller can run the upload without
|
||||
// holding the lock.
|
||||
func (s *Server) generateDebugBundle(req *proto.DebugBundleRequest, uiOpener debug.LogOpener) (path string, managementURL string, publishedUploadURL string, err error) {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
@@ -78,6 +89,7 @@ func (s *Server) generateDebugBundle(req *proto.DebugBundleRequest, uiOpener deb
|
||||
if cm := engine.GetClientMetrics(); cm != nil {
|
||||
clientMetrics = cm
|
||||
}
|
||||
publishedUploadURL = engine.DebugUploadURL()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,14 +143,14 @@ func (s *Server) generateDebugBundle(req *proto.DebugBundleRequest, uiOpener deb
|
||||
|
||||
path, err = bundleGenerator.Generate()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("generate debug bundle: %w", err)
|
||||
return "", "", "", fmt.Errorf("generate debug bundle: %w", err)
|
||||
}
|
||||
|
||||
if s.config != nil && s.config.ManagementURL != nil {
|
||||
managementURL = s.config.ManagementURL.String()
|
||||
}
|
||||
|
||||
return path, managementURL, nil
|
||||
return path, managementURL, publishedUploadURL, nil
|
||||
}
|
||||
|
||||
// GetLogLevel gets the current logging level for the server.
|
||||
|
||||
@@ -148,7 +148,7 @@ Debug.RevealFile(path: string): Promise<void> // OS file-manager focus
|
||||
|
||||
**Log level case sensitivity bug:** `proto.LogLevel_value` is keyed on uppercase enum names (`"TRACE"`, `"DEBUG"`, `"INFO"`, `"WARN"`, `"ERROR"`, `"PANIC"`, `"FATAL"`, `"UNKNOWN"`). `Debug.SetLogLevel` calls `proto.LogLevel_value[lvl.Level]` and falls back to `INFO` on miss. `useDebugBundle` currently passes `"trace"` (lowercase), which silently maps to `INFO` — the trace-capture flow doesn't actually raise the log level today. To raise to trace, pass `{ level: "TRACE" }`. Fix on the cleanup list.
|
||||
|
||||
`Debug.Bundle` uploads when `uploadUrl != ""`. Result fields: `path` (local copy), `uploadedKey` (set on success), `uploadFailureReason` (set on upload failure — the local copy is still saved).
|
||||
`Debug.Bundle` uploads when `upload` is true; the daemon picks the destination from what the management server publishes, so the UI never names one. Result fields: `path` (local copy), `uploadedKey` (set on success), `uploadFailureReason` (set on upload failure, including a deployment that publishes no upload service — the local copy is still saved).
|
||||
|
||||
## `Update`
|
||||
|
||||
@@ -283,7 +283,7 @@ The tray also reads a tray-only synthetic `"Error"` for icon purposes; the front
|
||||
|
||||
`UpParams` / `LogoutParams` / `ProfileRef` / `ConfigParams` / `ActiveProfile`: all `{ profileName, username: string }` (different names but same shape — kept distinct by Wails for clarity).
|
||||
|
||||
`DebugBundleParams`: `{ anonymize, systemInfo: boolean; uploadUrl: string; logFileCount: number }`.
|
||||
`DebugBundleParams`: `{ anonymize, systemInfo, upload: boolean; logFileCount: number }`.
|
||||
|
||||
`DebugBundleResult`: `{ path, uploadedKey, uploadFailureReason: string }`.
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import i18next from "@/lib/i18n";
|
||||
import { errorDialog, formatErrorMessage } from "@/lib/errors.ts";
|
||||
import { startConnection } from "@/lib/connection.ts";
|
||||
|
||||
const NETBIRD_UPLOAD_URL = "https://upload.debug.netbird.io/upload-url";
|
||||
const TRACE_LOG_FILE_COUNT = 5;
|
||||
const PLAIN_LOG_FILE_COUNT = 1;
|
||||
const TRACE_LOG_LEVEL = "trace";
|
||||
@@ -70,7 +69,12 @@ type BundleOptions = {
|
||||
capturePackets: boolean;
|
||||
hasWindow: boolean;
|
||||
totalSec: number;
|
||||
uploadUrl: string;
|
||||
// Whether to upload at all. The destination is the daemon's to pick: it
|
||||
// takes the one the management server publishes, and only falls back to the
|
||||
// service NetBird runs for a peer enrolled with NetBird's cloud. The UI must
|
||||
// not name a vendor endpoint of its own, or a self-hosted deployment would
|
||||
// ship its bundles out of the operator's control sphere.
|
||||
upload: boolean;
|
||||
anonymizeLevel: AnonymizeLevel;
|
||||
systemInfo: boolean;
|
||||
};
|
||||
@@ -187,19 +191,19 @@ const runBundleFlow = async (
|
||||
setStage({ kind: "bundling" });
|
||||
const logFileCount = opts.trace ? TRACE_LOG_FILE_COUNT : PLAIN_LOG_FILE_COUNT;
|
||||
|
||||
if (opts.uploadUrl) setStage({ kind: "uploading" });
|
||||
if (opts.upload) setStage({ kind: "uploading" });
|
||||
const result = await DebugSvc.Bundle({
|
||||
anonymize: opts.anonymizeLevel !== "none",
|
||||
// The daemon only knows "default" and "strict"; "none" is expressed
|
||||
// through the anonymize flag being off.
|
||||
anonymizeLevel: opts.anonymizeLevel === "strict" ? "strict" : "default",
|
||||
systemInfo: opts.systemInfo,
|
||||
uploadUrl: opts.uploadUrl,
|
||||
upload: opts.upload,
|
||||
logFileCount,
|
||||
});
|
||||
throwIfAborted(signal);
|
||||
if (result.path) setLastBundlePath(result.path);
|
||||
setStage({ kind: "done", result, uploadAttempted: Boolean(opts.uploadUrl) });
|
||||
setStage({ kind: "done", result, uploadAttempted: opts.upload });
|
||||
};
|
||||
|
||||
const useDebugBundle = () => {
|
||||
@@ -244,7 +248,7 @@ const useDebugBundle = () => {
|
||||
capturePackets,
|
||||
hasWindow: capture && totalSec > 0,
|
||||
totalSec,
|
||||
uploadUrl: upload ? NETBIRD_UPLOAD_URL : "",
|
||||
upload,
|
||||
anonymizeLevel,
|
||||
systemInfo,
|
||||
};
|
||||
|
||||
@@ -20,8 +20,12 @@ type DebugBundleParams struct {
|
||||
// private IP ranges, peer names, and WireGuard public keys.
|
||||
AnonymizeLevel string `json:"anonymizeLevel"`
|
||||
SystemInfo bool `json:"systemInfo"`
|
||||
UploadURL string `json:"uploadUrl"`
|
||||
LogFileCount uint32 `json:"logFileCount"`
|
||||
// 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
|
||||
@@ -54,7 +58,7 @@ func (s *Debug) Bundle(ctx context.Context, p DebugBundleParams) (DebugBundleRes
|
||||
Anonymize: p.Anonymize,
|
||||
AnonymizeLevel: p.AnonymizeLevel,
|
||||
SystemInfo: p.SystemInfo,
|
||||
UploadURL: p.UploadURL,
|
||||
Upload: p.Upload,
|
||||
LogFileCount: p.LogFileCount,
|
||||
CliVersion: version.NetbirdVersion(),
|
||||
})
|
||||
|
||||
@@ -184,6 +184,10 @@ func LoadMgmtConfig(ctx context.Context, mgmtConfigPath string) (*nbconfig.Confi
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := loadedConfig.DebugUpload.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for account, version := range loadedConfig.PerAccountHighestSupportedSyncMessageVersion {
|
||||
err := grpc.ValidateSyncMessageVersion(&version)
|
||||
if err != nil {
|
||||
|
||||
@@ -199,6 +199,7 @@ func accountSettings(s *nmdata.AccountSettingsInfo) *types.Settings {
|
||||
AutoUpdateVersion: s.AutoUpdateVersion,
|
||||
AutoUpdateAlways: s.AutoUpdateAlways,
|
||||
MetricsPushEnabled: s.MetricsPushEnabled,
|
||||
DebugBundleUploadURL: s.DebugBundleUploadURL,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@ const (
|
||||
settings_lazy_connection_enabled as lazy_connection_enabled,
|
||||
settings_auto_update_version as auto_update_version,
|
||||
settings_auto_update_always as auto_update_always,
|
||||
settings_metrics_push_enabled as metrics_push_enabled
|
||||
settings_metrics_push_enabled as metrics_push_enabled,
|
||||
settings_debug_bundle_upload_url as debug_bundle_upload_url
|
||||
from accounts
|
||||
where id=$1
|
||||
`
|
||||
@@ -50,6 +51,7 @@ func (pgc *PgStoreConn) GetAccountSettings(ctx context.Context, accountId string
|
||||
AutoUpdateVersion: settings.AutoUpdateVersion.String,
|
||||
AutoUpdateAlways: settings.AutoUpdateAlways.Bool,
|
||||
MetricsPushEnabled: settings.MetricsPushEnabled.Bool,
|
||||
DebugBundleUploadURL: settings.DebugBundleUploadURL.String,
|
||||
}
|
||||
if settings.IPv6EnabledGroups != nil {
|
||||
if err := json.Unmarshal(settings.IPv6EnabledGroups, &settingsInfo.IPv6EnabledGroups); err != nil {
|
||||
|
||||
@@ -68,6 +68,7 @@ type Account struct {
|
||||
AutoUpdateVersion sql.NullString
|
||||
AutoUpdateAlways sql.NullBool
|
||||
MetricsPushEnabled sql.NullBool
|
||||
DebugBundleUploadURL sql.NullString
|
||||
}
|
||||
|
||||
type Domain struct {
|
||||
|
||||
@@ -20,7 +20,8 @@ const (
|
||||
settings_lazy_connection_enabled as lazy_connection_enabled,
|
||||
settings_auto_update_version as auto_update_version,
|
||||
settings_auto_update_always as auto_update_always,
|
||||
settings_metrics_push_enabled as metrics_push_enabled
|
||||
settings_metrics_push_enabled as metrics_push_enabled,
|
||||
settings_debug_bundle_upload_url as debug_bundle_upload_url
|
||||
from accounts
|
||||
where id=?
|
||||
`
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/idp"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
@@ -57,6 +60,10 @@ type Config struct {
|
||||
|
||||
AgentNetwork AgentNetwork
|
||||
|
||||
// DebugUpload configures where the peers of this deployment send their
|
||||
// debug bundles. See DebugUpload.
|
||||
DebugUpload DebugUpload
|
||||
|
||||
// disable default all-to-all policy
|
||||
DisableDefaultPolicy bool
|
||||
|
||||
@@ -206,6 +213,48 @@ type AgentNetwork struct {
|
||||
PricingDefaultsFile string
|
||||
}
|
||||
|
||||
// DebugUpload configures the debug-bundle upload service this deployment
|
||||
// publishes to its peers.
|
||||
//
|
||||
// The client paths that upload without a human picking a destination — the
|
||||
// remote debug-bundle job, the mobile clients and the desktop UI — take the
|
||||
// destination from here. It exists so a self-hosted deployment keeps its
|
||||
// bundles, which carry peer logs, routes, DNS and firewall state, inside the
|
||||
// operator's own control sphere instead of reaching the upload service NetBird
|
||||
// runs. Leaving it unset publishes no destination: a peer enrolled with
|
||||
// NetBird's cloud still uses NetBird's service, a self-hosted peer keeps the
|
||||
// bundle local.
|
||||
//
|
||||
// Set URL to the upload service's get-URL endpoint, e.g.
|
||||
// https://upload.example.com/upload-url (see the upload-server component).
|
||||
type DebugUpload struct {
|
||||
// URL is the get-URL endpoint of the upload service. Must be https: the
|
||||
// client fetches an upload URL from it and then PUTs the bundle to whatever
|
||||
// that fetch returns, so a plaintext hop is a place to intercept both.
|
||||
URL string
|
||||
}
|
||||
|
||||
// Validate rejects a destination the client would refuse anyway, so a typo in
|
||||
// management.json surfaces at startup instead of at the first bundle upload.
|
||||
func (d DebugUpload) Validate() error {
|
||||
if d.URL == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(d.URL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse debug upload URL: %w", err)
|
||||
}
|
||||
if parsed.Scheme != "https" {
|
||||
return fmt.Errorf("debug upload URL must use https, got scheme %q", parsed.Scheme)
|
||||
}
|
||||
if parsed.Host == "" {
|
||||
return errors.New("debug upload URL must have a host")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReverseProxy contains reverse proxy configuration in front of management.
|
||||
type ReverseProxy struct {
|
||||
// TrustedHTTPProxies represents a list of trusted HTTP proxies by their IP prefixes.
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDebugUploadValidate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
wantErr string
|
||||
}{
|
||||
{name: "unset publishes no destination", url: ""},
|
||||
{name: "https accepted", url: "https://upload.example.com/upload-url"},
|
||||
{name: "https with port accepted", url: "https://upload.example.com:8443/upload-url"},
|
||||
// The client fetches an upload URL from this endpoint and then PUTs the
|
||||
// bundle to whatever comes back, so a plaintext hop intercepts both.
|
||||
{name: "http refused", url: "http://upload.example.com/upload-url", wantErr: "must use https"},
|
||||
{name: "scheme-less refused", url: "upload.example.com/upload-url", wantErr: "must use https"},
|
||||
{name: "host-less refused", url: "https:///upload-url", wantErr: "must have a host"},
|
||||
{name: "unparsable refused", url: "https://upload.example.com:port", wantErr: "parse debug upload URL"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := DebugUpload{URL: tc.url}.Validate()
|
||||
if tc.wantErr == "" {
|
||||
require.NoError(t, err)
|
||||
return
|
||||
}
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tc.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -115,6 +115,17 @@ func toNetbirdConfig(config *nbconfig.Config, turnCredentials *Token, relayToken
|
||||
}
|
||||
}
|
||||
|
||||
// The account setting wins, the server config is the deployment-wide default
|
||||
// a self-hosted install can set once so a fresh account is not left with the
|
||||
// vendor fallback. Both are https-validated where they are written.
|
||||
debugUploadURL := config.DebugUpload.URL
|
||||
if settings != nil && settings.DebugBundleUploadURL != "" {
|
||||
debugUploadURL = settings.DebugBundleUploadURL
|
||||
}
|
||||
if debugUploadURL != "" {
|
||||
nbConfig.Debug = &proto.DebugConfig{UploadUrl: debugUploadURL}
|
||||
}
|
||||
|
||||
return nbConfig
|
||||
}
|
||||
|
||||
|
||||
@@ -363,7 +363,8 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco
|
||||
oldSettings.AutoUpdateAlways != newSettings.AutoUpdateAlways ||
|
||||
oldSettings.PeerLoginExpirationEnabled != newSettings.PeerLoginExpirationEnabled ||
|
||||
oldSettings.PeerLoginExpiration != newSettings.PeerLoginExpiration ||
|
||||
oldSettings.MetricsPushEnabled != newSettings.MetricsPushEnabled {
|
||||
oldSettings.MetricsPushEnabled != newSettings.MetricsPushEnabled ||
|
||||
oldSettings.DebugBundleUploadURL != newSettings.DebugBundleUploadURL {
|
||||
// Session deadline is derived from LastLogin + PeerLoginExpiration
|
||||
// on every Login/Sync response. Without a fan-out push, connected
|
||||
// peers keep the deadline they received at login time and only see
|
||||
@@ -415,6 +416,7 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco
|
||||
am.handleAutoUpdateAlwaysSettings(ctx, oldSettings, newSettings, userID, accountID)
|
||||
am.handlePeerExposeSettings(ctx, oldSettings, newSettings, userID, accountID)
|
||||
am.handleMetricsPushSettings(ctx, oldSettings, newSettings, userID, accountID)
|
||||
am.handleDebugBundleUploadURLSettings(ctx, oldSettings, newSettings, userID, accountID)
|
||||
if err = am.handleInactivityExpirationSettings(ctx, oldSettings, newSettings, userID, accountID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -579,6 +581,17 @@ func (am *DefaultAccountManager) handleMetricsPushSettings(ctx context.Context,
|
||||
}
|
||||
}
|
||||
|
||||
// handleDebugBundleUploadURLSettings records a change of debug-bundle
|
||||
// destination. The value decides whose infrastructure the account's peer logs,
|
||||
// routes and firewall state land on, so a change is worth an audit entry even
|
||||
// though it is not a permission change. The URL itself is not recorded: it is
|
||||
// operator-supplied free text that can carry a host or a token.
|
||||
func (am *DefaultAccountManager) handleDebugBundleUploadURLSettings(ctx context.Context, oldSettings, newSettings *types.Settings, userID, accountID string) {
|
||||
if oldSettings.DebugBundleUploadURL != newSettings.DebugBundleUploadURL {
|
||||
am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountDebugBundleUploadURLUpdated, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (am *DefaultAccountManager) handlePeerLoginExpirationSettings(ctx context.Context, oldSettings, newSettings *types.Settings, userID, accountID string) {
|
||||
if oldSettings.PeerLoginExpirationEnabled != newSettings.PeerLoginExpirationEnabled {
|
||||
event := activity.AccountPeerLoginExpirationEnabled
|
||||
|
||||
@@ -284,6 +284,10 @@ const (
|
||||
// AgentNetworkSettingsDeleted indicates that a user deleted the Agent Network account settings, releasing the endpoint
|
||||
AgentNetworkSettingsDeleted Activity = 142
|
||||
|
||||
// AccountDebugBundleUploadURLUpdated indicates that a user changed where the
|
||||
// account's peers upload their debug bundles
|
||||
AccountDebugBundleUploadURLUpdated Activity = 143
|
||||
|
||||
AccountDeleted Activity = 99999
|
||||
)
|
||||
|
||||
@@ -455,8 +459,9 @@ var activityMap = map[Activity]Code{
|
||||
AgentNetworkBudgetRuleUpdated: {"Agent Network budget rule updated", "agent_network.budget_rule.update"},
|
||||
AgentNetworkBudgetRuleDeleted: {"Agent Network budget rule deleted", "agent_network.budget_rule.delete"},
|
||||
|
||||
AgentNetworkSettingsUpdated: {"Agent Network settings updated", "agent_network.settings.update"},
|
||||
AgentNetworkSettingsDeleted: {"Agent Network settings deleted", "agent_network.settings.delete"},
|
||||
AgentNetworkSettingsUpdated: {"Agent Network settings updated", "agent_network.settings.update"},
|
||||
AgentNetworkSettingsDeleted: {"Agent Network settings deleted", "agent_network.settings.delete"},
|
||||
AccountDebugBundleUploadURLUpdated: {"Account debug bundle upload URL updated", "account.setting.debug.upload.url.update"},
|
||||
|
||||
AccountMetricsPushEnabled: {"Account metrics push enabled", "account.setting.metrics.push.enable"},
|
||||
AccountMetricsPushDisabled: {"Account metrics push disabled", "account.setting.metrics.push.disable"},
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
goversion "github.com/hashicorp/go-version"
|
||||
|
||||
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
||||
"github.com/netbirdio/netbird/management/server/account"
|
||||
nbcontext "github.com/netbirdio/netbird/management/server/context"
|
||||
"github.com/netbirdio/netbird/management/server/settings"
|
||||
@@ -286,6 +287,14 @@ func (h *handler) updateAccountRequestSettings(req api.PutApiAccountsAccountIdJS
|
||||
if req.Settings.MetricsPushEnabled != nil {
|
||||
returnSettings.MetricsPushEnabled = *req.Settings.MetricsPushEnabled
|
||||
}
|
||||
if req.Settings.DebugBundleUploadUrl != nil {
|
||||
// Same rule the management server config and the peers apply, so a
|
||||
// destination accepted here cannot be one the peers then refuse.
|
||||
if err := (nbconfig.DebugUpload{URL: *req.Settings.DebugBundleUploadUrl}).Validate(); err != nil {
|
||||
return nil, status.Errorf(status.InvalidArgument, "invalid debug bundle upload URL: %v", err)
|
||||
}
|
||||
returnSettings.DebugBundleUploadURL = *req.Settings.DebugBundleUploadUrl
|
||||
}
|
||||
if req.Settings.AgentNetworkOnly != nil {
|
||||
returnSettings.AgentNetworkOnly = *req.Settings.AgentNetworkOnly
|
||||
}
|
||||
@@ -432,6 +441,7 @@ func toAccountResponse(accountID string, settings *types.Settings, meta *types.A
|
||||
AutoUpdateAlways: &settings.AutoUpdateAlways,
|
||||
Ipv6EnabledGroups: &settings.IPv6EnabledGroups,
|
||||
MetricsPushEnabled: &settings.MetricsPushEnabled,
|
||||
DebugBundleUploadUrl: &settings.DebugBundleUploadURL,
|
||||
AgentNetworkOnly: &settings.AgentNetworkOnly,
|
||||
EmbeddedIdpEnabled: &settings.EmbeddedIdpEnabled,
|
||||
LocalAuthDisabled: &settings.LocalAuthDisabled,
|
||||
|
||||
@@ -12,9 +12,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.uber.org/mock/gomock"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/mock/gomock"
|
||||
|
||||
nbcontext "github.com/netbirdio/netbird/management/server/context"
|
||||
"github.com/netbirdio/netbird/management/server/mock_server"
|
||||
@@ -127,6 +127,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
RoutingPeerDnsResolutionEnabled: br(false),
|
||||
LazyConnectionEnabled: br(false),
|
||||
DnsDomain: sr(""),
|
||||
DebugBundleUploadUrl: sr(""),
|
||||
AutoUpdateAlways: br(false),
|
||||
AutoUpdateVersion: sr(""),
|
||||
MetricsPushEnabled: br(false),
|
||||
@@ -156,6 +157,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
RoutingPeerDnsResolutionEnabled: br(false),
|
||||
LazyConnectionEnabled: br(false),
|
||||
DnsDomain: sr(""),
|
||||
DebugBundleUploadUrl: sr(""),
|
||||
AutoUpdateAlways: br(false),
|
||||
AutoUpdateVersion: sr(""),
|
||||
MetricsPushEnabled: br(false),
|
||||
@@ -185,6 +187,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
RoutingPeerDnsResolutionEnabled: br(false),
|
||||
LazyConnectionEnabled: br(false),
|
||||
DnsDomain: sr(""),
|
||||
DebugBundleUploadUrl: sr(""),
|
||||
AutoUpdateAlways: br(false),
|
||||
AutoUpdateVersion: sr("latest"),
|
||||
MetricsPushEnabled: br(false),
|
||||
@@ -214,6 +217,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
RoutingPeerDnsResolutionEnabled: br(false),
|
||||
LazyConnectionEnabled: br(false),
|
||||
DnsDomain: sr(""),
|
||||
DebugBundleUploadUrl: sr(""),
|
||||
AutoUpdateAlways: br(false),
|
||||
AutoUpdateVersion: sr(""),
|
||||
MetricsPushEnabled: br(false),
|
||||
@@ -243,6 +247,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
RoutingPeerDnsResolutionEnabled: br(false),
|
||||
LazyConnectionEnabled: br(false),
|
||||
DnsDomain: sr(""),
|
||||
DebugBundleUploadUrl: sr(""),
|
||||
AutoUpdateAlways: br(false),
|
||||
AutoUpdateVersion: sr(""),
|
||||
MetricsPushEnabled: br(false),
|
||||
@@ -272,6 +277,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
RoutingPeerDnsResolutionEnabled: br(false),
|
||||
LazyConnectionEnabled: br(false),
|
||||
DnsDomain: sr(""),
|
||||
DebugBundleUploadUrl: sr(""),
|
||||
AutoUpdateAlways: br(false),
|
||||
AutoUpdateVersion: sr(""),
|
||||
MetricsPushEnabled: br(false),
|
||||
@@ -301,6 +307,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
RoutingPeerDnsResolutionEnabled: br(false),
|
||||
LazyConnectionEnabled: br(false),
|
||||
DnsDomain: sr(""),
|
||||
DebugBundleUploadUrl: sr(""),
|
||||
AutoUpdateAlways: br(false),
|
||||
AutoUpdateVersion: sr(""),
|
||||
MetricsPushEnabled: br(false),
|
||||
@@ -315,6 +322,48 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
expectedArray: false,
|
||||
expectedID: accountID,
|
||||
},
|
||||
{
|
||||
name: "PutAccount OK setting debug_bundle_upload_url",
|
||||
expectedBody: true,
|
||||
requestType: http.MethodPut,
|
||||
requestPath: "/api/accounts/" + accountID,
|
||||
requestBody: bytes.NewBufferString("{\"settings\": {\"peer_login_expiration\": 15552000,\"peer_login_expiration_enabled\": true,\"debug_bundle_upload_url\": \"https://upload.example.com/upload-url\"},\"onboarding\": {\"onboarding_flow_pending\": true,\"signup_form_pending\": true}}"),
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedSettings: api.AccountSettings{
|
||||
PeerLoginExpiration: 15552000,
|
||||
PeerLoginExpirationEnabled: true,
|
||||
GroupsPropagationEnabled: br(false),
|
||||
JwtGroupsClaimName: sr(""),
|
||||
JwtGroupsEnabled: br(false),
|
||||
JwtAllowGroups: &[]string{},
|
||||
RegularUsersViewBlocked: false,
|
||||
RoutingPeerDnsResolutionEnabled: br(false),
|
||||
LazyConnectionEnabled: br(false),
|
||||
DnsDomain: sr(""),
|
||||
DebugBundleUploadUrl: sr("https://upload.example.com/upload-url"),
|
||||
AutoUpdateAlways: br(false),
|
||||
AutoUpdateVersion: sr(""),
|
||||
MetricsPushEnabled: br(false),
|
||||
AgentNetworkOnly: br(false),
|
||||
EmbeddedIdpEnabled: br(false),
|
||||
LocalAuthDisabled: br(false),
|
||||
LocalMfaEnabled: br(false),
|
||||
},
|
||||
expectedArray: false,
|
||||
expectedID: accountID,
|
||||
},
|
||||
{
|
||||
// The peers fetch an upload URL from this endpoint and then PUT the
|
||||
// bundle to whatever comes back, so a plaintext destination must not
|
||||
// be storable at all.
|
||||
name: "PutAccount fails on a plaintext debug_bundle_upload_url",
|
||||
expectedBody: true,
|
||||
requestType: http.MethodPut,
|
||||
requestPath: "/api/accounts/" + accountID,
|
||||
requestBody: bytes.NewBufferString("{\"settings\": {\"peer_login_expiration\": 15552000,\"peer_login_expiration_enabled\": true,\"debug_bundle_upload_url\": \"http://upload.example.com/upload-url\"},\"onboarding\": {\"onboarding_flow_pending\": true,\"signup_form_pending\": true}}"),
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
expectedArray: false,
|
||||
},
|
||||
{
|
||||
name: "PutAccount fails enabling agent_network_only without dashboard_features",
|
||||
expectedBody: true,
|
||||
@@ -342,6 +391,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
RoutingPeerDnsResolutionEnabled: br(false),
|
||||
LazyConnectionEnabled: br(false),
|
||||
DnsDomain: sr(""),
|
||||
DebugBundleUploadUrl: sr(""),
|
||||
AutoUpdateAlways: br(false),
|
||||
AutoUpdateVersion: sr(""),
|
||||
MetricsPushEnabled: br(false),
|
||||
@@ -374,6 +424,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
RoutingPeerDnsResolutionEnabled: br(false),
|
||||
LazyConnectionEnabled: br(false),
|
||||
DnsDomain: sr(""),
|
||||
DebugBundleUploadUrl: sr(""),
|
||||
AutoUpdateAlways: br(false),
|
||||
AutoUpdateVersion: sr(""),
|
||||
MetricsPushEnabled: br(false),
|
||||
|
||||
@@ -1652,7 +1652,8 @@ func (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Acc
|
||||
settings_jwt_groups_enabled, settings_jwt_groups_claim_name, settings_jwt_allow_groups,
|
||||
settings_routing_peer_dns_resolution_enabled, settings_dns_domain, settings_network_range,
|
||||
settings_network_range_v6, settings_ipv6_enabled_groups, settings_lazy_connection_enabled,
|
||||
settings_local_mfa_enabled, settings_metrics_push_enabled, settings_agent_network_only,
|
||||
settings_local_mfa_enabled, settings_metrics_push_enabled, settings_debug_bundle_upload_url,
|
||||
settings_agent_network_only,
|
||||
settings_dashboard_features, settings_auto_update_version, settings_auto_update_always,
|
||||
settings_peer_expose_enabled, settings_peer_expose_groups,
|
||||
-- Embedded ExtraSettings
|
||||
@@ -1678,6 +1679,7 @@ func (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Acc
|
||||
sLazyConnectionEnabled sql.NullBool
|
||||
sLocalMFAEnabled sql.NullBool
|
||||
sMetricsPushEnabled sql.NullBool
|
||||
sDebugBundleUploadURL sql.NullString
|
||||
sAgentNetworkOnly sql.NullBool
|
||||
sDashboardFeatures sql.NullString
|
||||
autoUpdateVersion sql.NullString
|
||||
@@ -1706,7 +1708,7 @@ func (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Acc
|
||||
&sJWTGroupsEnabled, &sJWTGroupsClaimName, &sJWTAllowGroups,
|
||||
&sRoutingPeerDNSResolutionEnabled, &sDNSDomain, &sNetworkRange,
|
||||
&sNetworkRangeV6, &sIPv6EnabledGroups, &sLazyConnectionEnabled,
|
||||
&sLocalMFAEnabled, &sMetricsPushEnabled, &sAgentNetworkOnly,
|
||||
&sLocalMFAEnabled, &sMetricsPushEnabled, &sDebugBundleUploadURL, &sAgentNetworkOnly,
|
||||
&sDashboardFeatures, &autoUpdateVersion, &autoUpdateAlways,
|
||||
&peerExposeEnabled, &peerExposeGroups,
|
||||
&sExtraPeerApprovalEnabled, &sExtraUserApprovalRequired,
|
||||
@@ -1777,6 +1779,9 @@ func (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Acc
|
||||
if sMetricsPushEnabled.Valid {
|
||||
account.Settings.MetricsPushEnabled = sMetricsPushEnabled.Bool
|
||||
}
|
||||
if sDebugBundleUploadURL.Valid {
|
||||
account.Settings.DebugBundleUploadURL = sDebugBundleUploadURL.String
|
||||
}
|
||||
if sAgentNetworkOnly.Valid {
|
||||
account.Settings.AgentNetworkOnly = sAgentNetworkOnly.Bool
|
||||
}
|
||||
|
||||
@@ -582,6 +582,7 @@ func TwinAccountSettings(s *Settings) *nmdata.AccountSettingsInfo {
|
||||
AutoUpdateVersion: s.AutoUpdateVersion,
|
||||
AutoUpdateAlways: s.AutoUpdateAlways,
|
||||
MetricsPushEnabled: s.MetricsPushEnabled,
|
||||
DebugBundleUploadURL: s.DebugBundleUploadURL,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,6 +76,15 @@ type Settings struct {
|
||||
// MetricsPushEnabled globally enables or disables client metrics push for the account
|
||||
MetricsPushEnabled bool `gorm:"default:false"`
|
||||
|
||||
// DebugBundleUploadURL is the debug-bundle upload service the peers of this
|
||||
// account send their bundles to. A bundle carries peer logs, routes, DNS and
|
||||
// firewall state, so the destination decides whose infrastructure that data
|
||||
// lands on; setting it keeps a self-hosted account's bundles inside its own
|
||||
// control sphere. Empty falls back to the deployment-wide DebugUpload.URL
|
||||
// from the management server config, and with neither only a peer enrolled
|
||||
// with NetBird's cloud uploads at all. Must be an https URL with a host.
|
||||
DebugBundleUploadURL string
|
||||
|
||||
// AgentNetworkOnly limits the dashboard to the Agent Network surface for this account.
|
||||
// Set for accounts created via netbird.ai signups; users can disable it later.
|
||||
AgentNetworkOnly bool `gorm:"default:false"`
|
||||
@@ -123,6 +132,7 @@ func (s *Settings) Copy() *Settings {
|
||||
AutoUpdateAlways: s.AutoUpdateAlways,
|
||||
IPv6EnabledGroups: slices.Clone(s.IPv6EnabledGroups),
|
||||
MetricsPushEnabled: s.MetricsPushEnabled,
|
||||
DebugBundleUploadURL: s.DebugBundleUploadURL,
|
||||
AgentNetworkOnly: s.AgentNetworkOnly,
|
||||
EmbeddedIdpEnabled: s.EmbeddedIdpEnabled,
|
||||
LocalAuthDisabled: s.LocalAuthDisabled,
|
||||
|
||||
@@ -383,6 +383,14 @@ components:
|
||||
description: Enables or disables client metrics push for all peers in the account
|
||||
type: boolean
|
||||
example: false
|
||||
debug_bundle_upload_url:
|
||||
description: |
|
||||
Upload service the peers of this account send debug bundles to. A bundle carries peer logs, routes, DNS and
|
||||
firewall state, so setting this keeps that data inside infrastructure the account controls instead of the
|
||||
upload service NetBird runs. Must be an https URL with a host. Empty falls back to the deployment-wide value
|
||||
configured on the management server; with neither, only peers enrolled with NetBird's cloud upload at all.
|
||||
type: string
|
||||
example: "https://upload.example.com/upload-url"
|
||||
agent_network_only:
|
||||
description: Limits the dashboard to the Agent Network surface for this account. Set for accounts created via netbird.ai signups and can be disabled later. Enabling this requires dashboard_features.agent_network to be true in the same request.
|
||||
type: boolean
|
||||
|
||||
@@ -1686,6 +1686,12 @@ type AccountSettings struct {
|
||||
// DashboardFeatures Per-account dashboard section visibility overrides. Omitted keys follow the default dashboard behavior.
|
||||
DashboardFeatures *AccountDashboardFeatures `json:"dashboard_features,omitempty"`
|
||||
|
||||
// DebugBundleUploadUrl Upload service the peers of this account send debug bundles to. A bundle carries peer logs, routes, DNS and
|
||||
// firewall state, so setting this keeps that data inside infrastructure the account controls instead of the
|
||||
// upload service NetBird runs. Must be an https URL with a host. Empty falls back to the deployment-wide value
|
||||
// configured on the management server; with neither, only peers enrolled with NetBird's cloud upload at all.
|
||||
DebugBundleUploadUrl *string `json:"debug_bundle_upload_url,omitempty"`
|
||||
|
||||
// DnsDomain Allows to define a custom dns domain for the account
|
||||
DnsDomain *string `json:"dns_domain,omitempty"`
|
||||
|
||||
|
||||
@@ -15,4 +15,5 @@ type AccountSettingsInfo struct {
|
||||
AutoUpdateVersion string
|
||||
AutoUpdateAlways bool
|
||||
MetricsPushEnabled bool
|
||||
DebugBundleUploadURL string
|
||||
}
|
||||
|
||||
+1352
-1268
File diff suppressed because it is too large
Load Diff
@@ -337,6 +337,8 @@ message NetbirdConfig {
|
||||
FlowConfig flow = 5;
|
||||
|
||||
MetricsConfig metrics = 6;
|
||||
|
||||
DebugConfig debug = 7;
|
||||
}
|
||||
|
||||
// HostConfig describes connection properties of some server (e.g. STUN, Signal, Management)
|
||||
@@ -379,6 +381,19 @@ message MetricsConfig {
|
||||
bool enabled = 1;
|
||||
}
|
||||
|
||||
// DebugConfig carries the deployment-wide debug settings the operator of this
|
||||
// management server publishes to its peers.
|
||||
message DebugConfig {
|
||||
// upload_url is the debug-bundle upload service this peer's account uses,
|
||||
// taken from the account settings or, failing that, from the management
|
||||
// server config. Publishing it keeps a self-hosted deployment's bundles inside
|
||||
// its own control sphere instead of falling back to the upload service NetBird
|
||||
// runs. An empty value means no destination is published: the peer then
|
||||
// uploads only when it is enrolled with NetBird's cloud, and otherwise keeps
|
||||
// the bundle local.
|
||||
string upload_url = 1;
|
||||
}
|
||||
|
||||
// JWTConfig represents JWT authentication configuration for validating tokens.
|
||||
message JWTConfig {
|
||||
string issuer = 1;
|
||||
|
||||
Reference in New Issue
Block a user