mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-27 23:09:08 +01: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/formatter"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
"github.com/netbirdio/netbird/shared/management/domain"
|
"github.com/netbirdio/netbird/shared/management/domain"
|
||||||
types "github.com/netbirdio/netbird/upload-server/types"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// AnonymizeLevelDefault and AnonymizeLevelStrict are the accepted
|
// AnonymizeLevelDefault and AnonymizeLevelStrict are the accepted
|
||||||
@@ -349,6 +348,11 @@ func (c *Client) DebugBundle(platformFiles PlatformFiles, anonymize bool, anonym
|
|||||||
StatePath: platformFiles.StateFilePath(),
|
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 {
|
if cc != nil {
|
||||||
resp, err := cc.GetLatestSyncResponse()
|
resp, err := cc.GetLatestSyncResponse()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -357,6 +361,7 @@ func (c *Client) DebugBundle(platformFiles PlatformFiles, anonymize bool, anonym
|
|||||||
deps.SyncResponse = resp
|
deps.SyncResponse = resp
|
||||||
|
|
||||||
if e := cc.Engine(); e != nil {
|
if e := cc.Engine(); e != nil {
|
||||||
|
publishedUploadURL = e.DebugUploadURL()
|
||||||
deps.RefreshStatus = func() {
|
deps.RefreshStatus = func() {
|
||||||
e.RunHealthProbes(context.Background(), true)
|
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()
|
path, err := bundleGenerator.Generate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("generate debug bundle: %w", err)
|
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)
|
uploadCtx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||||
defer cancel()
|
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 {
|
if err != nil {
|
||||||
return "", fmt.Errorf("upload debug bundle: %w", err)
|
return "", fmt.Errorf("upload debug bundle: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-7
@@ -2,6 +2,7 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -19,12 +20,19 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/proto"
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
"github.com/netbirdio/netbird/client/server"
|
"github.com/netbirdio/netbird/client/server"
|
||||||
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
|
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
|
||||||
"github.com/netbirdio/netbird/upload-server/types"
|
|
||||||
"github.com/netbirdio/netbird/version"
|
"github.com/netbirdio/netbird/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
const errCloseConnection = "Failed to close connection: %v"
|
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 (
|
var (
|
||||||
logFileCount uint32
|
logFileCount uint32
|
||||||
systemInfoFlag bool
|
systemInfoFlag bool
|
||||||
@@ -179,6 +187,7 @@ func debugBundle(cmd *cobra.Command, _ []string) error {
|
|||||||
CliVersion: version.NetbirdVersion(),
|
CliVersion: version.NetbirdVersion(),
|
||||||
}
|
}
|
||||||
if uploadBundleFlag {
|
if uploadBundleFlag {
|
||||||
|
request.Upload = true
|
||||||
request.UploadURL = uploadBundleURLFlag
|
request.UploadURL = uploadBundleURLFlag
|
||||||
request.UploadInsecure = uploadBundleInsecureFlag
|
request.UploadInsecure = uploadBundleInsecureFlag
|
||||||
}
|
}
|
||||||
@@ -192,8 +201,8 @@ func debugBundle(cmd *cobra.Command, _ []string) error {
|
|||||||
return fmt.Errorf("upload failed: %s", resp.GetUploadFailureReason())
|
return fmt.Errorf("upload failed: %s", resp.GetUploadFailureReason())
|
||||||
}
|
}
|
||||||
|
|
||||||
if uploadBundleFlag {
|
if err := printUploadKey(cmd, resp); err != nil {
|
||||||
cmd.Printf("Upload file key:\n%s\n", resp.GetUploadedKey())
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -385,6 +394,7 @@ func runForDuration(cmd *cobra.Command, args []string) error {
|
|||||||
CliVersion: version.NetbirdVersion(),
|
CliVersion: version.NetbirdVersion(),
|
||||||
}
|
}
|
||||||
if uploadBundleFlag {
|
if uploadBundleFlag {
|
||||||
|
request.Upload = true
|
||||||
request.UploadURL = uploadBundleURLFlag
|
request.UploadURL = uploadBundleURLFlag
|
||||||
request.UploadInsecure = uploadBundleInsecureFlag
|
request.UploadInsecure = uploadBundleInsecureFlag
|
||||||
}
|
}
|
||||||
@@ -423,8 +433,8 @@ func runForDuration(cmd *cobra.Command, args []string) error {
|
|||||||
return fmt.Errorf("upload failed: %s", resp.GetUploadFailureReason())
|
return fmt.Errorf("upload failed: %s", resp.GetUploadFailureReason())
|
||||||
}
|
}
|
||||||
|
|
||||||
if uploadBundleFlag {
|
if err := printUploadKey(cmd, resp); err != nil {
|
||||||
cmd.Printf("Upload file key:\n%s\n", resp.GetUploadedKey())
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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)
|
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() {
|
func init() {
|
||||||
debugBundleCmd.Flags().Uint32VarP(&logFileCount, "log-file-count", "C", 1, "Number of rotated log files to include in debug bundle")
|
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(&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().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")
|
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().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(&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().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().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")
|
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"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
@@ -226,6 +227,14 @@ type Engine struct {
|
|||||||
TURNs []*stun.URI
|
TURNs []*stun.URI
|
||||||
stunTurn icemaker.StunTurn
|
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
|
clientCtx context.Context
|
||||||
clientCancel context.CancelFunc
|
clientCancel context.CancelFunc
|
||||||
|
|
||||||
@@ -1144,6 +1153,8 @@ func (e *Engine) updateNetbirdConfig(wCfg *mgmProto.NetbirdConfig) error {
|
|||||||
|
|
||||||
e.handleMetricsUpdate(wCfg.GetMetrics())
|
e.handleMetricsUpdate(wCfg.GetMetrics())
|
||||||
|
|
||||||
|
e.handleDebugUploadUpdate(wCfg.GetDebug())
|
||||||
|
|
||||||
if err := e.PopulateNetbirdConfig(wCfg, nil); err != nil {
|
if err := e.PopulateNetbirdConfig(wCfg, nil); err != nil {
|
||||||
log.Warnf("Failed to update DNS server config: %v", err)
|
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())
|
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) {
|
func toFlowLoggerConfig(config *mgmProto.FlowConfig) (*nftypes.FlowConfig, error) {
|
||||||
if config.GetInterval() == nil {
|
if config.GetInterval() == nil {
|
||||||
return nil, errors.New("flow interval is 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())
|
params.GetAnonymize(), params.GetAnonymizeLevel(), params.GetLogFileCount(), params.GetBundleFor(), params.GetBundleForTime())
|
||||||
log.Debugf("remote debug bundle request parameters: %s", params.String())
|
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
|
// Resolve the upload destination: an MDM override, when set, takes
|
||||||
// precedence over the management-supplied URL. Both are validated the same
|
// precedence over the job's URL. Both are validated the same way. With
|
||||||
// way; an empty result falls back to the default upload server downstream.
|
// 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()
|
uploadURL := params.GetUploadUrl()
|
||||||
if override := e.config.ProfileConfig.DebugBundleUploadURL; override != "" {
|
if override := e.config.ProfileConfig.DebugBundleUploadURL; override != "" {
|
||||||
log.Infof("using MDM debug bundle upload URL override instead of the management-supplied value")
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
syncResponse, err := e.GetLatestSyncResponse()
|
uploadURL, err = debug.ResolveUploadURL(uploadURL, e.DebugUploadURL(), e.config.ProfileConfig.ManagementURL.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("get latest sync response: %v", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
bundleDeps := debug.GeneratorDependencies{
|
bundleDeps := debug.GeneratorDependencies{
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestValidateBundleUploadURL covers the sanity check applied to a
|
// TestValidateBundleUploadURL covers the sanity check applied to a
|
||||||
@@ -15,7 +17,7 @@ func TestValidateBundleUploadURL(t *testing.T) {
|
|||||||
raw string
|
raw string
|
||||||
wantErr bool
|
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 with host", raw: "https://upload.debug.netbird.io/upload"},
|
||||||
{name: "https self-hosted host", raw: "https://upload.example.com"},
|
{name: "https self-hosted host", raw: "https://upload.example.com"},
|
||||||
{name: "plaintext rejected", raw: "http://upload.example.com", wantErr: true},
|
{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/formatter"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
"github.com/netbirdio/netbird/shared/management/domain"
|
"github.com/netbirdio/netbird/shared/management/domain"
|
||||||
types "github.com/netbirdio/netbird/upload-server/types"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// AnonymizeLevelDefault and AnonymizeLevelStrict are the accepted
|
// AnonymizeLevelDefault and AnonymizeLevelStrict are the accepted
|
||||||
@@ -298,6 +297,11 @@ func (c *Client) DebugBundle(anonymize bool, anonymizeLevel string) (string, err
|
|||||||
LogPath: c.logFilePath,
|
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 {
|
if cc != nil {
|
||||||
resp, err := cc.GetLatestSyncResponse()
|
resp, err := cc.GetLatestSyncResponse()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -306,6 +310,7 @@ func (c *Client) DebugBundle(anonymize bool, anonymizeLevel string) (string, err
|
|||||||
deps.SyncResponse = resp
|
deps.SyncResponse = resp
|
||||||
|
|
||||||
if e := cc.Engine(); e != nil {
|
if e := cc.Engine(); e != nil {
|
||||||
|
publishedUploadURL = e.DebugUploadURL()
|
||||||
deps.RefreshStatus = func() {
|
deps.RefreshStatus = func() {
|
||||||
e.RunHealthProbes(context.Background(), true)
|
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()
|
path, err := bundleGenerator.Generate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("generate debug bundle: %w", err)
|
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)
|
uploadCtx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||||
defer cancel()
|
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 {
|
if err != nil {
|
||||||
return "", fmt.Errorf("upload debug bundle: %w", err)
|
return "", fmt.Errorf("upload debug bundle: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal/debug"
|
"github.com/netbirdio/netbird/client/internal/debug"
|
||||||
"github.com/netbirdio/netbird/upload-server/types"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -28,9 +27,14 @@ func NewExecutor() *Executor {
|
|||||||
return &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) {
|
func (e *Executor) BundleJob(ctx context.Context, debugBundleDependencies debug.GeneratorDependencies, params debug.BundleConfig, waitForDuration time.Duration, mgmURL, uploadURL string) (string, error) {
|
||||||
if uploadURL == "" {
|
if uploadURL == "" {
|
||||||
uploadURL = types.DefaultBundleURL
|
return "", debug.ErrNoUploadDestination
|
||||||
}
|
}
|
||||||
|
|
||||||
if waitForDuration > MaxBundleWaitTime {
|
if waitForDuration > MaxBundleWaitTime {
|
||||||
|
|||||||
@@ -2820,8 +2820,15 @@ type DebugBundleRequest struct {
|
|||||||
// Unknown values are treated as "strict". Only meaningful with anonymize;
|
// Unknown values are treated as "strict". Only meaningful with anonymize;
|
||||||
// "strict" implies it.
|
// "strict" implies it.
|
||||||
AnonymizeLevel string `protobuf:"bytes,8,opt,name=anonymizeLevel,proto3" json:"anonymizeLevel,omitempty"`
|
AnonymizeLevel string `protobuf:"bytes,8,opt,name=anonymizeLevel,proto3" json:"anonymizeLevel,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
// upload asks the daemon to upload the bundle. When uploadURL is empty the
|
||||||
sizeCache protoimpl.SizeCache
|
// 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() {
|
func (x *DebugBundleRequest) Reset() {
|
||||||
@@ -2903,6 +2910,13 @@ func (x *DebugBundleRequest) GetAnonymizeLevel() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *DebugBundleRequest) GetUpload() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.Upload
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type DebugBundleResponse struct {
|
type DebugBundleResponse struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"`
|
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" +
|
"\x12translatedHostname\x18\x04 \x01(\tR\x12translatedHostname\x128\n" +
|
||||||
"\x0etranslatedPort\x18\x05 \x01(\v2\x10.daemon.PortInfoR\x0etranslatedPort\"G\n" +
|
"\x0etranslatedPort\x18\x05 \x01(\v2\x10.daemon.PortInfoR\x0etranslatedPort\"G\n" +
|
||||||
"\x17ForwardingRulesResponse\x12,\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" +
|
"\x12DebugBundleRequest\x12\x1c\n" +
|
||||||
"\tanonymize\x18\x01 \x01(\bR\tanonymize\x12\x1e\n" +
|
"\tanonymize\x18\x01 \x01(\bR\tanonymize\x12\x1e\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
@@ -7366,7 +7380,8 @@ const file_daemon_proto_rawDesc = "" +
|
|||||||
"cliVersion\x18\x06 \x01(\tR\n" +
|
"cliVersion\x18\x06 \x01(\tR\n" +
|
||||||
"cliVersion\x12&\n" +
|
"cliVersion\x12&\n" +
|
||||||
"\x0euploadInsecure\x18\a \x01(\bR\x0euploadInsecure\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" +
|
"\x13DebugBundleResponse\x12\x12\n" +
|
||||||
"\x04path\x18\x01 \x01(\tR\x04path\x12 \n" +
|
"\x04path\x18\x01 \x01(\tR\x04path\x12 \n" +
|
||||||
"\vuploadedKey\x18\x02 \x01(\tR\vuploadedKey\x120\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;
|
// Unknown values are treated as "strict". Only meaningful with anonymize;
|
||||||
// "strict" implies it.
|
// "strict" implies it.
|
||||||
string anonymizeLevel = 8;
|
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 {
|
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.
|
// socket that carries no identity, which skips the UI log.
|
||||||
callerID, callerIdentified := ipcauth.CallerIdentity(callerCtx)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.GetUploadURL() == "" {
|
if !req.GetUpload() && req.GetUploadURL() == "" {
|
||||||
return &proto.DebugBundleResponse{Path: path}, nil
|
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
|
// 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
|
// slow destination and must not block the other RPCs that take the lock. The
|
||||||
// bounded context is a backstop against a hung connection.
|
// bounded context is a backstop against a hung connection.
|
||||||
uploadCtx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
uploadCtx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||||
defer cancel()
|
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 {
|
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
|
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
|
return &proto.DebugBundleResponse{Path: path, UploadedKey: key}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateDebugBundle builds the bundle under s.mutex and returns its path plus
|
// 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
|
// the management URL and the upload service the management server publishes,
|
||||||
// without holding the lock.
|
// both captured under the lock, so the caller can run the upload without
|
||||||
func (s *Server) generateDebugBundle(req *proto.DebugBundleRequest, uiOpener debug.LogOpener) (path string, managementURL string, err error) {
|
// holding the lock.
|
||||||
|
func (s *Server) generateDebugBundle(req *proto.DebugBundleRequest, uiOpener debug.LogOpener) (path string, managementURL string, publishedUploadURL string, err error) {
|
||||||
s.mutex.Lock()
|
s.mutex.Lock()
|
||||||
defer s.mutex.Unlock()
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
@@ -78,6 +89,7 @@ func (s *Server) generateDebugBundle(req *proto.DebugBundleRequest, uiOpener deb
|
|||||||
if cm := engine.GetClientMetrics(); cm != nil {
|
if cm := engine.GetClientMetrics(); cm != nil {
|
||||||
clientMetrics = cm
|
clientMetrics = cm
|
||||||
}
|
}
|
||||||
|
publishedUploadURL = engine.DebugUploadURL()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,14 +143,14 @@ func (s *Server) generateDebugBundle(req *proto.DebugBundleRequest, uiOpener deb
|
|||||||
|
|
||||||
path, err = bundleGenerator.Generate()
|
path, err = bundleGenerator.Generate()
|
||||||
if err != nil {
|
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 {
|
if s.config != nil && s.config.ManagementURL != nil {
|
||||||
managementURL = s.config.ManagementURL.String()
|
managementURL = s.config.ManagementURL.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
return path, managementURL, nil
|
return path, managementURL, publishedUploadURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLogLevel gets the current logging level for the server.
|
// 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.
|
**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`
|
## `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).
|
`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 }`.
|
`DebugBundleResult`: `{ path, uploadedKey, uploadFailureReason: string }`.
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import i18next from "@/lib/i18n";
|
|||||||
import { errorDialog, formatErrorMessage } from "@/lib/errors.ts";
|
import { errorDialog, formatErrorMessage } from "@/lib/errors.ts";
|
||||||
import { startConnection } from "@/lib/connection.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 TRACE_LOG_FILE_COUNT = 5;
|
||||||
const PLAIN_LOG_FILE_COUNT = 1;
|
const PLAIN_LOG_FILE_COUNT = 1;
|
||||||
const TRACE_LOG_LEVEL = "trace";
|
const TRACE_LOG_LEVEL = "trace";
|
||||||
@@ -70,7 +69,12 @@ type BundleOptions = {
|
|||||||
capturePackets: boolean;
|
capturePackets: boolean;
|
||||||
hasWindow: boolean;
|
hasWindow: boolean;
|
||||||
totalSec: number;
|
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;
|
anonymizeLevel: AnonymizeLevel;
|
||||||
systemInfo: boolean;
|
systemInfo: boolean;
|
||||||
};
|
};
|
||||||
@@ -187,19 +191,19 @@ const runBundleFlow = async (
|
|||||||
setStage({ kind: "bundling" });
|
setStage({ kind: "bundling" });
|
||||||
const logFileCount = opts.trace ? TRACE_LOG_FILE_COUNT : PLAIN_LOG_FILE_COUNT;
|
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({
|
const result = await DebugSvc.Bundle({
|
||||||
anonymize: opts.anonymizeLevel !== "none",
|
anonymize: opts.anonymizeLevel !== "none",
|
||||||
// The daemon only knows "default" and "strict"; "none" is expressed
|
// The daemon only knows "default" and "strict"; "none" is expressed
|
||||||
// through the anonymize flag being off.
|
// through the anonymize flag being off.
|
||||||
anonymizeLevel: opts.anonymizeLevel === "strict" ? "strict" : "default",
|
anonymizeLevel: opts.anonymizeLevel === "strict" ? "strict" : "default",
|
||||||
systemInfo: opts.systemInfo,
|
systemInfo: opts.systemInfo,
|
||||||
uploadUrl: opts.uploadUrl,
|
upload: opts.upload,
|
||||||
logFileCount,
|
logFileCount,
|
||||||
});
|
});
|
||||||
throwIfAborted(signal);
|
throwIfAborted(signal);
|
||||||
if (result.path) setLastBundlePath(result.path);
|
if (result.path) setLastBundlePath(result.path);
|
||||||
setStage({ kind: "done", result, uploadAttempted: Boolean(opts.uploadUrl) });
|
setStage({ kind: "done", result, uploadAttempted: opts.upload });
|
||||||
};
|
};
|
||||||
|
|
||||||
const useDebugBundle = () => {
|
const useDebugBundle = () => {
|
||||||
@@ -244,7 +248,7 @@ const useDebugBundle = () => {
|
|||||||
capturePackets,
|
capturePackets,
|
||||||
hasWindow: capture && totalSec > 0,
|
hasWindow: capture && totalSec > 0,
|
||||||
totalSec,
|
totalSec,
|
||||||
uploadUrl: upload ? NETBIRD_UPLOAD_URL : "",
|
upload,
|
||||||
anonymizeLevel,
|
anonymizeLevel,
|
||||||
systemInfo,
|
systemInfo,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,8 +20,12 @@ type DebugBundleParams struct {
|
|||||||
// private IP ranges, peer names, and WireGuard public keys.
|
// private IP ranges, peer names, and WireGuard public keys.
|
||||||
AnonymizeLevel string `json:"anonymizeLevel"`
|
AnonymizeLevel string `json:"anonymizeLevel"`
|
||||||
SystemInfo bool `json:"systemInfo"`
|
SystemInfo bool `json:"systemInfo"`
|
||||||
UploadURL string `json:"uploadUrl"`
|
// Upload asks the daemon to upload the bundle. The UI carries no
|
||||||
LogFileCount uint32 `json:"logFileCount"`
|
// 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
|
// 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,
|
Anonymize: p.Anonymize,
|
||||||
AnonymizeLevel: p.AnonymizeLevel,
|
AnonymizeLevel: p.AnonymizeLevel,
|
||||||
SystemInfo: p.SystemInfo,
|
SystemInfo: p.SystemInfo,
|
||||||
UploadURL: p.UploadURL,
|
Upload: p.Upload,
|
||||||
LogFileCount: p.LogFileCount,
|
LogFileCount: p.LogFileCount,
|
||||||
CliVersion: version.NetbirdVersion(),
|
CliVersion: version.NetbirdVersion(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -184,6 +184,10 @@ func LoadMgmtConfig(ctx context.Context, mgmtConfigPath string) (*nbconfig.Confi
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := loadedConfig.DebugUpload.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
for account, version := range loadedConfig.PerAccountHighestSupportedSyncMessageVersion {
|
for account, version := range loadedConfig.PerAccountHighestSupportedSyncMessageVersion {
|
||||||
err := grpc.ValidateSyncMessageVersion(&version)
|
err := grpc.ValidateSyncMessageVersion(&version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ func accountSettings(s *nmdata.AccountSettingsInfo) *types.Settings {
|
|||||||
AutoUpdateVersion: s.AutoUpdateVersion,
|
AutoUpdateVersion: s.AutoUpdateVersion,
|
||||||
AutoUpdateAlways: s.AutoUpdateAlways,
|
AutoUpdateAlways: s.AutoUpdateAlways,
|
||||||
MetricsPushEnabled: s.MetricsPushEnabled,
|
MetricsPushEnabled: s.MetricsPushEnabled,
|
||||||
|
DebugBundleUploadURL: s.DebugBundleUploadURL,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ const (
|
|||||||
settings_lazy_connection_enabled as lazy_connection_enabled,
|
settings_lazy_connection_enabled as lazy_connection_enabled,
|
||||||
settings_auto_update_version as auto_update_version,
|
settings_auto_update_version as auto_update_version,
|
||||||
settings_auto_update_always as auto_update_always,
|
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
|
from accounts
|
||||||
where id=$1
|
where id=$1
|
||||||
`
|
`
|
||||||
@@ -50,6 +51,7 @@ func (pgc *PgStoreConn) GetAccountSettings(ctx context.Context, accountId string
|
|||||||
AutoUpdateVersion: settings.AutoUpdateVersion.String,
|
AutoUpdateVersion: settings.AutoUpdateVersion.String,
|
||||||
AutoUpdateAlways: settings.AutoUpdateAlways.Bool,
|
AutoUpdateAlways: settings.AutoUpdateAlways.Bool,
|
||||||
MetricsPushEnabled: settings.MetricsPushEnabled.Bool,
|
MetricsPushEnabled: settings.MetricsPushEnabled.Bool,
|
||||||
|
DebugBundleUploadURL: settings.DebugBundleUploadURL.String,
|
||||||
}
|
}
|
||||||
if settings.IPv6EnabledGroups != nil {
|
if settings.IPv6EnabledGroups != nil {
|
||||||
if err := json.Unmarshal(settings.IPv6EnabledGroups, &settingsInfo.IPv6EnabledGroups); err != nil {
|
if err := json.Unmarshal(settings.IPv6EnabledGroups, &settingsInfo.IPv6EnabledGroups); err != nil {
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ type Account struct {
|
|||||||
AutoUpdateVersion sql.NullString
|
AutoUpdateVersion sql.NullString
|
||||||
AutoUpdateAlways sql.NullBool
|
AutoUpdateAlways sql.NullBool
|
||||||
MetricsPushEnabled sql.NullBool
|
MetricsPushEnabled sql.NullBool
|
||||||
|
DebugBundleUploadURL sql.NullString
|
||||||
}
|
}
|
||||||
|
|
||||||
type Domain struct {
|
type Domain struct {
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ const (
|
|||||||
settings_lazy_connection_enabled as lazy_connection_enabled,
|
settings_lazy_connection_enabled as lazy_connection_enabled,
|
||||||
settings_auto_update_version as auto_update_version,
|
settings_auto_update_version as auto_update_version,
|
||||||
settings_auto_update_always as auto_update_always,
|
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
|
from accounts
|
||||||
where id=?
|
where id=?
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/server/idp"
|
"github.com/netbirdio/netbird/management/server/idp"
|
||||||
"github.com/netbirdio/netbird/management/server/types"
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
@@ -57,6 +60,10 @@ type Config struct {
|
|||||||
|
|
||||||
AgentNetwork AgentNetwork
|
AgentNetwork AgentNetwork
|
||||||
|
|
||||||
|
// DebugUpload configures where the peers of this deployment send their
|
||||||
|
// debug bundles. See DebugUpload.
|
||||||
|
DebugUpload DebugUpload
|
||||||
|
|
||||||
// disable default all-to-all policy
|
// disable default all-to-all policy
|
||||||
DisableDefaultPolicy bool
|
DisableDefaultPolicy bool
|
||||||
|
|
||||||
@@ -206,6 +213,48 @@ type AgentNetwork struct {
|
|||||||
PricingDefaultsFile string
|
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.
|
// ReverseProxy contains reverse proxy configuration in front of management.
|
||||||
type ReverseProxy struct {
|
type ReverseProxy struct {
|
||||||
// TrustedHTTPProxies represents a list of trusted HTTP proxies by their IP prefixes.
|
// 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
|
return nbConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -363,7 +363,8 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco
|
|||||||
oldSettings.AutoUpdateAlways != newSettings.AutoUpdateAlways ||
|
oldSettings.AutoUpdateAlways != newSettings.AutoUpdateAlways ||
|
||||||
oldSettings.PeerLoginExpirationEnabled != newSettings.PeerLoginExpirationEnabled ||
|
oldSettings.PeerLoginExpirationEnabled != newSettings.PeerLoginExpirationEnabled ||
|
||||||
oldSettings.PeerLoginExpiration != newSettings.PeerLoginExpiration ||
|
oldSettings.PeerLoginExpiration != newSettings.PeerLoginExpiration ||
|
||||||
oldSettings.MetricsPushEnabled != newSettings.MetricsPushEnabled {
|
oldSettings.MetricsPushEnabled != newSettings.MetricsPushEnabled ||
|
||||||
|
oldSettings.DebugBundleUploadURL != newSettings.DebugBundleUploadURL {
|
||||||
// Session deadline is derived from LastLogin + PeerLoginExpiration
|
// Session deadline is derived from LastLogin + PeerLoginExpiration
|
||||||
// on every Login/Sync response. Without a fan-out push, connected
|
// on every Login/Sync response. Without a fan-out push, connected
|
||||||
// peers keep the deadline they received at login time and only see
|
// 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.handleAutoUpdateAlwaysSettings(ctx, oldSettings, newSettings, userID, accountID)
|
||||||
am.handlePeerExposeSettings(ctx, oldSettings, newSettings, userID, accountID)
|
am.handlePeerExposeSettings(ctx, oldSettings, newSettings, userID, accountID)
|
||||||
am.handleMetricsPushSettings(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 {
|
if err = am.handleInactivityExpirationSettings(ctx, oldSettings, newSettings, userID, accountID); err != nil {
|
||||||
return nil, err
|
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) {
|
func (am *DefaultAccountManager) handlePeerLoginExpirationSettings(ctx context.Context, oldSettings, newSettings *types.Settings, userID, accountID string) {
|
||||||
if oldSettings.PeerLoginExpirationEnabled != newSettings.PeerLoginExpirationEnabled {
|
if oldSettings.PeerLoginExpirationEnabled != newSettings.PeerLoginExpirationEnabled {
|
||||||
event := activity.AccountPeerLoginExpirationEnabled
|
event := activity.AccountPeerLoginExpirationEnabled
|
||||||
|
|||||||
@@ -284,6 +284,10 @@ const (
|
|||||||
// AgentNetworkSettingsDeleted indicates that a user deleted the Agent Network account settings, releasing the endpoint
|
// AgentNetworkSettingsDeleted indicates that a user deleted the Agent Network account settings, releasing the endpoint
|
||||||
AgentNetworkSettingsDeleted Activity = 142
|
AgentNetworkSettingsDeleted Activity = 142
|
||||||
|
|
||||||
|
// AccountDebugBundleUploadURLUpdated indicates that a user changed where the
|
||||||
|
// account's peers upload their debug bundles
|
||||||
|
AccountDebugBundleUploadURLUpdated Activity = 143
|
||||||
|
|
||||||
AccountDeleted Activity = 99999
|
AccountDeleted Activity = 99999
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -455,8 +459,9 @@ var activityMap = map[Activity]Code{
|
|||||||
AgentNetworkBudgetRuleUpdated: {"Agent Network budget rule updated", "agent_network.budget_rule.update"},
|
AgentNetworkBudgetRuleUpdated: {"Agent Network budget rule updated", "agent_network.budget_rule.update"},
|
||||||
AgentNetworkBudgetRuleDeleted: {"Agent Network budget rule deleted", "agent_network.budget_rule.delete"},
|
AgentNetworkBudgetRuleDeleted: {"Agent Network budget rule deleted", "agent_network.budget_rule.delete"},
|
||||||
|
|
||||||
AgentNetworkSettingsUpdated: {"Agent Network settings updated", "agent_network.settings.update"},
|
AgentNetworkSettingsUpdated: {"Agent Network settings updated", "agent_network.settings.update"},
|
||||||
AgentNetworkSettingsDeleted: {"Agent Network settings deleted", "agent_network.settings.delete"},
|
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"},
|
AccountMetricsPushEnabled: {"Account metrics push enabled", "account.setting.metrics.push.enable"},
|
||||||
AccountMetricsPushDisabled: {"Account metrics push disabled", "account.setting.metrics.push.disable"},
|
AccountMetricsPushDisabled: {"Account metrics push disabled", "account.setting.metrics.push.disable"},
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
|
|
||||||
goversion "github.com/hashicorp/go-version"
|
goversion "github.com/hashicorp/go-version"
|
||||||
|
|
||||||
|
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
||||||
"github.com/netbirdio/netbird/management/server/account"
|
"github.com/netbirdio/netbird/management/server/account"
|
||||||
nbcontext "github.com/netbirdio/netbird/management/server/context"
|
nbcontext "github.com/netbirdio/netbird/management/server/context"
|
||||||
"github.com/netbirdio/netbird/management/server/settings"
|
"github.com/netbirdio/netbird/management/server/settings"
|
||||||
@@ -286,6 +287,14 @@ func (h *handler) updateAccountRequestSettings(req api.PutApiAccountsAccountIdJS
|
|||||||
if req.Settings.MetricsPushEnabled != nil {
|
if req.Settings.MetricsPushEnabled != nil {
|
||||||
returnSettings.MetricsPushEnabled = *req.Settings.MetricsPushEnabled
|
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 {
|
if req.Settings.AgentNetworkOnly != nil {
|
||||||
returnSettings.AgentNetworkOnly = *req.Settings.AgentNetworkOnly
|
returnSettings.AgentNetworkOnly = *req.Settings.AgentNetworkOnly
|
||||||
}
|
}
|
||||||
@@ -432,6 +441,7 @@ func toAccountResponse(accountID string, settings *types.Settings, meta *types.A
|
|||||||
AutoUpdateAlways: &settings.AutoUpdateAlways,
|
AutoUpdateAlways: &settings.AutoUpdateAlways,
|
||||||
Ipv6EnabledGroups: &settings.IPv6EnabledGroups,
|
Ipv6EnabledGroups: &settings.IPv6EnabledGroups,
|
||||||
MetricsPushEnabled: &settings.MetricsPushEnabled,
|
MetricsPushEnabled: &settings.MetricsPushEnabled,
|
||||||
|
DebugBundleUploadUrl: &settings.DebugBundleUploadURL,
|
||||||
AgentNetworkOnly: &settings.AgentNetworkOnly,
|
AgentNetworkOnly: &settings.AgentNetworkOnly,
|
||||||
EmbeddedIdpEnabled: &settings.EmbeddedIdpEnabled,
|
EmbeddedIdpEnabled: &settings.EmbeddedIdpEnabled,
|
||||||
LocalAuthDisabled: &settings.LocalAuthDisabled,
|
LocalAuthDisabled: &settings.LocalAuthDisabled,
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.uber.org/mock/gomock"
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"go.uber.org/mock/gomock"
|
||||||
|
|
||||||
nbcontext "github.com/netbirdio/netbird/management/server/context"
|
nbcontext "github.com/netbirdio/netbird/management/server/context"
|
||||||
"github.com/netbirdio/netbird/management/server/mock_server"
|
"github.com/netbirdio/netbird/management/server/mock_server"
|
||||||
@@ -127,6 +127,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
|||||||
RoutingPeerDnsResolutionEnabled: br(false),
|
RoutingPeerDnsResolutionEnabled: br(false),
|
||||||
LazyConnectionEnabled: br(false),
|
LazyConnectionEnabled: br(false),
|
||||||
DnsDomain: sr(""),
|
DnsDomain: sr(""),
|
||||||
|
DebugBundleUploadUrl: sr(""),
|
||||||
AutoUpdateAlways: br(false),
|
AutoUpdateAlways: br(false),
|
||||||
AutoUpdateVersion: sr(""),
|
AutoUpdateVersion: sr(""),
|
||||||
MetricsPushEnabled: br(false),
|
MetricsPushEnabled: br(false),
|
||||||
@@ -156,6 +157,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
|||||||
RoutingPeerDnsResolutionEnabled: br(false),
|
RoutingPeerDnsResolutionEnabled: br(false),
|
||||||
LazyConnectionEnabled: br(false),
|
LazyConnectionEnabled: br(false),
|
||||||
DnsDomain: sr(""),
|
DnsDomain: sr(""),
|
||||||
|
DebugBundleUploadUrl: sr(""),
|
||||||
AutoUpdateAlways: br(false),
|
AutoUpdateAlways: br(false),
|
||||||
AutoUpdateVersion: sr(""),
|
AutoUpdateVersion: sr(""),
|
||||||
MetricsPushEnabled: br(false),
|
MetricsPushEnabled: br(false),
|
||||||
@@ -185,6 +187,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
|||||||
RoutingPeerDnsResolutionEnabled: br(false),
|
RoutingPeerDnsResolutionEnabled: br(false),
|
||||||
LazyConnectionEnabled: br(false),
|
LazyConnectionEnabled: br(false),
|
||||||
DnsDomain: sr(""),
|
DnsDomain: sr(""),
|
||||||
|
DebugBundleUploadUrl: sr(""),
|
||||||
AutoUpdateAlways: br(false),
|
AutoUpdateAlways: br(false),
|
||||||
AutoUpdateVersion: sr("latest"),
|
AutoUpdateVersion: sr("latest"),
|
||||||
MetricsPushEnabled: br(false),
|
MetricsPushEnabled: br(false),
|
||||||
@@ -214,6 +217,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
|||||||
RoutingPeerDnsResolutionEnabled: br(false),
|
RoutingPeerDnsResolutionEnabled: br(false),
|
||||||
LazyConnectionEnabled: br(false),
|
LazyConnectionEnabled: br(false),
|
||||||
DnsDomain: sr(""),
|
DnsDomain: sr(""),
|
||||||
|
DebugBundleUploadUrl: sr(""),
|
||||||
AutoUpdateAlways: br(false),
|
AutoUpdateAlways: br(false),
|
||||||
AutoUpdateVersion: sr(""),
|
AutoUpdateVersion: sr(""),
|
||||||
MetricsPushEnabled: br(false),
|
MetricsPushEnabled: br(false),
|
||||||
@@ -243,6 +247,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
|||||||
RoutingPeerDnsResolutionEnabled: br(false),
|
RoutingPeerDnsResolutionEnabled: br(false),
|
||||||
LazyConnectionEnabled: br(false),
|
LazyConnectionEnabled: br(false),
|
||||||
DnsDomain: sr(""),
|
DnsDomain: sr(""),
|
||||||
|
DebugBundleUploadUrl: sr(""),
|
||||||
AutoUpdateAlways: br(false),
|
AutoUpdateAlways: br(false),
|
||||||
AutoUpdateVersion: sr(""),
|
AutoUpdateVersion: sr(""),
|
||||||
MetricsPushEnabled: br(false),
|
MetricsPushEnabled: br(false),
|
||||||
@@ -272,6 +277,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
|||||||
RoutingPeerDnsResolutionEnabled: br(false),
|
RoutingPeerDnsResolutionEnabled: br(false),
|
||||||
LazyConnectionEnabled: br(false),
|
LazyConnectionEnabled: br(false),
|
||||||
DnsDomain: sr(""),
|
DnsDomain: sr(""),
|
||||||
|
DebugBundleUploadUrl: sr(""),
|
||||||
AutoUpdateAlways: br(false),
|
AutoUpdateAlways: br(false),
|
||||||
AutoUpdateVersion: sr(""),
|
AutoUpdateVersion: sr(""),
|
||||||
MetricsPushEnabled: br(false),
|
MetricsPushEnabled: br(false),
|
||||||
@@ -301,6 +307,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
|||||||
RoutingPeerDnsResolutionEnabled: br(false),
|
RoutingPeerDnsResolutionEnabled: br(false),
|
||||||
LazyConnectionEnabled: br(false),
|
LazyConnectionEnabled: br(false),
|
||||||
DnsDomain: sr(""),
|
DnsDomain: sr(""),
|
||||||
|
DebugBundleUploadUrl: sr(""),
|
||||||
AutoUpdateAlways: br(false),
|
AutoUpdateAlways: br(false),
|
||||||
AutoUpdateVersion: sr(""),
|
AutoUpdateVersion: sr(""),
|
||||||
MetricsPushEnabled: br(false),
|
MetricsPushEnabled: br(false),
|
||||||
@@ -315,6 +322,48 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
|||||||
expectedArray: false,
|
expectedArray: false,
|
||||||
expectedID: accountID,
|
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",
|
name: "PutAccount fails enabling agent_network_only without dashboard_features",
|
||||||
expectedBody: true,
|
expectedBody: true,
|
||||||
@@ -342,6 +391,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
|||||||
RoutingPeerDnsResolutionEnabled: br(false),
|
RoutingPeerDnsResolutionEnabled: br(false),
|
||||||
LazyConnectionEnabled: br(false),
|
LazyConnectionEnabled: br(false),
|
||||||
DnsDomain: sr(""),
|
DnsDomain: sr(""),
|
||||||
|
DebugBundleUploadUrl: sr(""),
|
||||||
AutoUpdateAlways: br(false),
|
AutoUpdateAlways: br(false),
|
||||||
AutoUpdateVersion: sr(""),
|
AutoUpdateVersion: sr(""),
|
||||||
MetricsPushEnabled: br(false),
|
MetricsPushEnabled: br(false),
|
||||||
@@ -374,6 +424,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
|||||||
RoutingPeerDnsResolutionEnabled: br(false),
|
RoutingPeerDnsResolutionEnabled: br(false),
|
||||||
LazyConnectionEnabled: br(false),
|
LazyConnectionEnabled: br(false),
|
||||||
DnsDomain: sr(""),
|
DnsDomain: sr(""),
|
||||||
|
DebugBundleUploadUrl: sr(""),
|
||||||
AutoUpdateAlways: br(false),
|
AutoUpdateAlways: br(false),
|
||||||
AutoUpdateVersion: sr(""),
|
AutoUpdateVersion: sr(""),
|
||||||
MetricsPushEnabled: br(false),
|
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_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_routing_peer_dns_resolution_enabled, settings_dns_domain, settings_network_range,
|
||||||
settings_network_range_v6, settings_ipv6_enabled_groups, settings_lazy_connection_enabled,
|
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_dashboard_features, settings_auto_update_version, settings_auto_update_always,
|
||||||
settings_peer_expose_enabled, settings_peer_expose_groups,
|
settings_peer_expose_enabled, settings_peer_expose_groups,
|
||||||
-- Embedded ExtraSettings
|
-- Embedded ExtraSettings
|
||||||
@@ -1678,6 +1679,7 @@ func (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Acc
|
|||||||
sLazyConnectionEnabled sql.NullBool
|
sLazyConnectionEnabled sql.NullBool
|
||||||
sLocalMFAEnabled sql.NullBool
|
sLocalMFAEnabled sql.NullBool
|
||||||
sMetricsPushEnabled sql.NullBool
|
sMetricsPushEnabled sql.NullBool
|
||||||
|
sDebugBundleUploadURL sql.NullString
|
||||||
sAgentNetworkOnly sql.NullBool
|
sAgentNetworkOnly sql.NullBool
|
||||||
sDashboardFeatures sql.NullString
|
sDashboardFeatures sql.NullString
|
||||||
autoUpdateVersion sql.NullString
|
autoUpdateVersion sql.NullString
|
||||||
@@ -1706,7 +1708,7 @@ func (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Acc
|
|||||||
&sJWTGroupsEnabled, &sJWTGroupsClaimName, &sJWTAllowGroups,
|
&sJWTGroupsEnabled, &sJWTGroupsClaimName, &sJWTAllowGroups,
|
||||||
&sRoutingPeerDNSResolutionEnabled, &sDNSDomain, &sNetworkRange,
|
&sRoutingPeerDNSResolutionEnabled, &sDNSDomain, &sNetworkRange,
|
||||||
&sNetworkRangeV6, &sIPv6EnabledGroups, &sLazyConnectionEnabled,
|
&sNetworkRangeV6, &sIPv6EnabledGroups, &sLazyConnectionEnabled,
|
||||||
&sLocalMFAEnabled, &sMetricsPushEnabled, &sAgentNetworkOnly,
|
&sLocalMFAEnabled, &sMetricsPushEnabled, &sDebugBundleUploadURL, &sAgentNetworkOnly,
|
||||||
&sDashboardFeatures, &autoUpdateVersion, &autoUpdateAlways,
|
&sDashboardFeatures, &autoUpdateVersion, &autoUpdateAlways,
|
||||||
&peerExposeEnabled, &peerExposeGroups,
|
&peerExposeEnabled, &peerExposeGroups,
|
||||||
&sExtraPeerApprovalEnabled, &sExtraUserApprovalRequired,
|
&sExtraPeerApprovalEnabled, &sExtraUserApprovalRequired,
|
||||||
@@ -1777,6 +1779,9 @@ func (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Acc
|
|||||||
if sMetricsPushEnabled.Valid {
|
if sMetricsPushEnabled.Valid {
|
||||||
account.Settings.MetricsPushEnabled = sMetricsPushEnabled.Bool
|
account.Settings.MetricsPushEnabled = sMetricsPushEnabled.Bool
|
||||||
}
|
}
|
||||||
|
if sDebugBundleUploadURL.Valid {
|
||||||
|
account.Settings.DebugBundleUploadURL = sDebugBundleUploadURL.String
|
||||||
|
}
|
||||||
if sAgentNetworkOnly.Valid {
|
if sAgentNetworkOnly.Valid {
|
||||||
account.Settings.AgentNetworkOnly = sAgentNetworkOnly.Bool
|
account.Settings.AgentNetworkOnly = sAgentNetworkOnly.Bool
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -582,6 +582,7 @@ func TwinAccountSettings(s *Settings) *nmdata.AccountSettingsInfo {
|
|||||||
AutoUpdateVersion: s.AutoUpdateVersion,
|
AutoUpdateVersion: s.AutoUpdateVersion,
|
||||||
AutoUpdateAlways: s.AutoUpdateAlways,
|
AutoUpdateAlways: s.AutoUpdateAlways,
|
||||||
MetricsPushEnabled: s.MetricsPushEnabled,
|
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 globally enables or disables client metrics push for the account
|
||||||
MetricsPushEnabled bool `gorm:"default:false"`
|
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.
|
// 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.
|
// Set for accounts created via netbird.ai signups; users can disable it later.
|
||||||
AgentNetworkOnly bool `gorm:"default:false"`
|
AgentNetworkOnly bool `gorm:"default:false"`
|
||||||
@@ -123,6 +132,7 @@ func (s *Settings) Copy() *Settings {
|
|||||||
AutoUpdateAlways: s.AutoUpdateAlways,
|
AutoUpdateAlways: s.AutoUpdateAlways,
|
||||||
IPv6EnabledGroups: slices.Clone(s.IPv6EnabledGroups),
|
IPv6EnabledGroups: slices.Clone(s.IPv6EnabledGroups),
|
||||||
MetricsPushEnabled: s.MetricsPushEnabled,
|
MetricsPushEnabled: s.MetricsPushEnabled,
|
||||||
|
DebugBundleUploadURL: s.DebugBundleUploadURL,
|
||||||
AgentNetworkOnly: s.AgentNetworkOnly,
|
AgentNetworkOnly: s.AgentNetworkOnly,
|
||||||
EmbeddedIdpEnabled: s.EmbeddedIdpEnabled,
|
EmbeddedIdpEnabled: s.EmbeddedIdpEnabled,
|
||||||
LocalAuthDisabled: s.LocalAuthDisabled,
|
LocalAuthDisabled: s.LocalAuthDisabled,
|
||||||
|
|||||||
@@ -383,6 +383,14 @@ components:
|
|||||||
description: Enables or disables client metrics push for all peers in the account
|
description: Enables or disables client metrics push for all peers in the account
|
||||||
type: boolean
|
type: boolean
|
||||||
example: false
|
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:
|
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.
|
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
|
type: boolean
|
||||||
|
|||||||
@@ -1686,6 +1686,12 @@ type AccountSettings struct {
|
|||||||
// DashboardFeatures Per-account dashboard section visibility overrides. Omitted keys follow the default dashboard behavior.
|
// DashboardFeatures Per-account dashboard section visibility overrides. Omitted keys follow the default dashboard behavior.
|
||||||
DashboardFeatures *AccountDashboardFeatures `json:"dashboard_features,omitempty"`
|
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 Allows to define a custom dns domain for the account
|
||||||
DnsDomain *string `json:"dns_domain,omitempty"`
|
DnsDomain *string `json:"dns_domain,omitempty"`
|
||||||
|
|
||||||
|
|||||||
@@ -15,4 +15,5 @@ type AccountSettingsInfo struct {
|
|||||||
AutoUpdateVersion string
|
AutoUpdateVersion string
|
||||||
AutoUpdateAlways bool
|
AutoUpdateAlways bool
|
||||||
MetricsPushEnabled 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;
|
FlowConfig flow = 5;
|
||||||
|
|
||||||
MetricsConfig metrics = 6;
|
MetricsConfig metrics = 6;
|
||||||
|
|
||||||
|
DebugConfig debug = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
// HostConfig describes connection properties of some server (e.g. STUN, Signal, Management)
|
// HostConfig describes connection properties of some server (e.g. STUN, Signal, Management)
|
||||||
@@ -379,6 +381,19 @@ message MetricsConfig {
|
|||||||
bool enabled = 1;
|
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.
|
// JWTConfig represents JWT authentication configuration for validating tokens.
|
||||||
message JWTConfig {
|
message JWTConfig {
|
||||||
string issuer = 1;
|
string issuer = 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user