mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-24 16:41:30 +02:00
The SSH server flags force `netbird up` through sudo, but the CLI resolved every per-user path with the process user. As root that reads root's own (empty) local state, so a `sudo netbird up` silently switched the daemon from the user's profile to the default one — cancelling any login already waiting in the browser — and then ran an SSO login for the default profile's config. Whichever account that login returned, the default profile's peer belongs to someone else, so every attempt ended in "peer is already registered by a different User or a Setup Key", with nothing telling the user why. Resolve the acting user through SUDO_USER when running as root: the active profile, the profile config paths and the stored account email now come from the invoking user's directories. Privilege decisions are untouched — they stay on the kernel credentials of the daemon connection, which an environment variable can never influence; a forged SUDO_USER only selects a profile root could select anyway. The invoking user's directories are strictly read-only under sudo. Anything root wrote there would be root-owned and break the user's own runs, so instead of chowning files back, the local writes are skipped: the active-profile bookkeeping and the account-email state simply do not update from a sudo run (the daemon records the switch on its side; a skipped email write costs at most one extra account prompt later). Plain root — no sudo context — has no user to act for, so the ambiguity is refused instead of guessed at: when the daemon's active profile differs from what root resolves and no --profile was given, up fails with a message naming both profiles, instead of silently switching the daemon and failing later with the ownership error.
550 lines
17 KiB
Go
550 lines
17 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/spf13/cobra"
|
|
"google.golang.org/grpc/status"
|
|
"google.golang.org/protobuf/encoding/protojson"
|
|
"google.golang.org/protobuf/types/known/durationpb"
|
|
|
|
"github.com/netbirdio/netbird/client/internal"
|
|
"github.com/netbirdio/netbird/client/internal/debug"
|
|
"github.com/netbirdio/netbird/client/internal/peer"
|
|
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
|
"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"
|
|
|
|
var (
|
|
logFileCount uint32
|
|
systemInfoFlag bool
|
|
uploadBundleFlag bool
|
|
uploadBundleURLFlag string
|
|
uploadBundleInsecureFlag bool
|
|
)
|
|
|
|
var debugCmd = &cobra.Command{
|
|
Use: "debug",
|
|
Short: "Debugging commands",
|
|
Long: "Commands for debugging and logging within the NetBird daemon.",
|
|
}
|
|
|
|
var debugBundleCmd = &cobra.Command{
|
|
Use: "bundle",
|
|
Example: " netbird debug bundle",
|
|
Short: "Create a debug bundle",
|
|
Long: "Generates a compressed archive of the daemon's logs and status for debugging purposes.",
|
|
RunE: debugBundle,
|
|
}
|
|
|
|
var logCmd = &cobra.Command{
|
|
Use: "log",
|
|
Short: "Manage logging for the NetBird daemon",
|
|
Long: `Commands to manage logging settings for the NetBird daemon, including ICE, gRPC, and general log levels.`,
|
|
}
|
|
|
|
var logLevelCmd = &cobra.Command{
|
|
Use: "level <level>",
|
|
Short: "Set the logging level for this session",
|
|
Long: `Sets the logging level for the current session. This setting is temporary and will revert to the default on daemon restart.
|
|
Available log levels are:
|
|
panic: for panic level, highest level of severity
|
|
fatal: for fatal level errors that cause the program to exit
|
|
error: for error conditions
|
|
warn: for warning conditions
|
|
info: for informational messages
|
|
debug: for debug-level messages
|
|
trace: for trace-level messages, which include more fine-grained information than debug`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: setLogLevel,
|
|
}
|
|
|
|
var forCmd = &cobra.Command{
|
|
Use: "for <time>",
|
|
Short: "Run debug logs for a specified duration and create a debug bundle",
|
|
Long: `Sets the logging level to trace, runs for the specified duration, and then generates a debug bundle.`,
|
|
Example: " netbird debug for 5m",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runForDuration,
|
|
}
|
|
|
|
var persistenceCmd = &cobra.Command{
|
|
Use: "persistence [on|off]",
|
|
Short: "Set sync response memory persistence",
|
|
Long: `Configure whether the latest sync response should persist in memory. When enabled, the last known sync response will be kept in memory.`,
|
|
Example: " netbird debug persistence on",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: setSyncResponsePersistence,
|
|
}
|
|
|
|
var debugConfigCmd = &cobra.Command{
|
|
Use: "config",
|
|
Example: " netbird debug config",
|
|
Short: "Dump the effective configuration",
|
|
Long: "Prints the daemon's resolved configuration (after applying defaults, file, env, CLI input, and MDM policy overrides) as JSON. Includes the list of MDM-managed fields.",
|
|
RunE: debugConfigDump,
|
|
}
|
|
|
|
// debugConfigDump implements `netbird debug config`. It resolves the
|
|
// active profile, queries the daemon for the effective configuration
|
|
// via GetConfig, and prints the resulting GetConfigResponse as JSON
|
|
// (via protojson with EmitUnpopulated=true so the output is stable
|
|
// across runs and includes zero-valued fields).
|
|
//
|
|
// Useful for verifying MDM enforcement end-to-end: the response's
|
|
// mDMManagedFields array is the single source of truth for "which
|
|
// fields is the daemon currently enforcing from the MDM source", and
|
|
// every config field side-by-side with that list confirms the merge
|
|
// result. Secrets in the response (e.g. PreSharedKey) are already
|
|
// redacted by the daemon-side handler.
|
|
func debugConfigDump(cmd *cobra.Command, _ []string) error {
|
|
pm := profilemanager.NewProfileManager()
|
|
activeProf, err := pm.GetActiveProfile()
|
|
if err != nil {
|
|
return fmt.Errorf("get active profile: %v", err)
|
|
}
|
|
currUser, err := profilemanager.InvokingUser()
|
|
if err != nil {
|
|
return fmt.Errorf("get current user: %v", err)
|
|
}
|
|
|
|
conn, err := getClient(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if err := conn.Close(); err != nil {
|
|
log.Errorf(errCloseConnection, err)
|
|
}
|
|
}()
|
|
|
|
client := proto.NewDaemonServiceClient(conn)
|
|
resp, err := client.GetConfig(cmd.Context(), &proto.GetConfigRequest{
|
|
ProfileName: string(activeProf.ID),
|
|
Username: currUser.Username,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get config: %v", status.Convert(err).Message())
|
|
}
|
|
|
|
// Use protojson so well-known fields render correctly; emit defaults so
|
|
// the operator sees every field even when zero/empty.
|
|
m := protojson.MarshalOptions{Multiline: true, Indent: " ", EmitUnpopulated: true}
|
|
out, err := m.Marshal(resp)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal config: %w", err)
|
|
}
|
|
cmd.Println(string(out))
|
|
return nil
|
|
}
|
|
|
|
// debugBundle requests the daemon to create a debug bundle and prints
|
|
// the resulting local file path and, if uploaded, the uploaded file
|
|
// key. It uses the package flags (anonymize, system info, log file
|
|
// count, CLI version, optional upload URL) to configure the bundle
|
|
// request. Returns an error if the RPC fails or if the daemon reports
|
|
// an upload failure reason.
|
|
func debugBundle(cmd *cobra.Command, _ []string) error {
|
|
anonymizeEnabled, anonymizeLevel, err := effectiveAnonymize()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
conn, err := getClient(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if err := conn.Close(); err != nil {
|
|
log.Errorf(errCloseConnection, err)
|
|
}
|
|
}()
|
|
|
|
client := proto.NewDaemonServiceClient(conn)
|
|
request := &proto.DebugBundleRequest{
|
|
Anonymize: anonymizeEnabled,
|
|
AnonymizeLevel: anonymizeLevel.String(),
|
|
SystemInfo: systemInfoFlag,
|
|
LogFileCount: logFileCount,
|
|
CliVersion: version.NetbirdVersion(),
|
|
}
|
|
if uploadBundleFlag {
|
|
request.UploadURL = uploadBundleURLFlag
|
|
request.UploadInsecure = uploadBundleInsecureFlag
|
|
}
|
|
resp, err := client.DebugBundle(cmd.Context(), request)
|
|
if err != nil {
|
|
return daemonCallError("bundle debug", err)
|
|
}
|
|
cmd.Printf("Local file:\n%s\n", resp.GetPath())
|
|
|
|
if resp.GetUploadFailureReason() != "" {
|
|
return fmt.Errorf("upload failed: %s", resp.GetUploadFailureReason())
|
|
}
|
|
|
|
if uploadBundleFlag {
|
|
cmd.Printf("Upload file key:\n%s\n", resp.GetUploadedKey())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func setLogLevel(cmd *cobra.Command, args []string) error {
|
|
conn, err := getClient(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if err := conn.Close(); err != nil {
|
|
log.Errorf(errCloseConnection, err)
|
|
}
|
|
}()
|
|
|
|
client := proto.NewDaemonServiceClient(conn)
|
|
level := server.ParseLogLevel(args[0])
|
|
if level == proto.LogLevel_UNKNOWN {
|
|
//nolint
|
|
return fmt.Errorf("unknown log level: %s. Available levels are: panic, fatal, error, warn, info, debug, trace\n", args[0])
|
|
}
|
|
|
|
_, err = client.SetLogLevel(cmd.Context(), &proto.SetLogLevelRequest{
|
|
Level: level,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to set log level: %v", status.Convert(err).Message())
|
|
}
|
|
|
|
cmd.Println("Log level set successfully to", args[0])
|
|
return nil
|
|
}
|
|
|
|
func runForDuration(cmd *cobra.Command, args []string) error {
|
|
duration, err := time.ParseDuration(args[0])
|
|
if err != nil {
|
|
return fmt.Errorf("invalid duration format: %v", err)
|
|
}
|
|
|
|
anonymizeEnabled, anonymizeLevel, err := effectiveAnonymize()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
conn, err := getClient(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if err := conn.Close(); err != nil {
|
|
log.Errorf(errCloseConnection, err)
|
|
}
|
|
}()
|
|
|
|
client := proto.NewDaemonServiceClient(conn)
|
|
|
|
stat, err := client.Status(cmd.Context(), &proto.StatusRequest{ShouldRunProbes: true})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get status: %v", status.Convert(err).Message())
|
|
}
|
|
|
|
stateWasDown := stat.Status != string(internal.StatusConnected) && stat.Status != string(internal.StatusConnecting)
|
|
|
|
initialLogLevel, err := client.GetLogLevel(cmd.Context(), &proto.GetLogLevelRequest{})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get log level: %v", status.Convert(err).Message())
|
|
}
|
|
|
|
if stateWasDown {
|
|
if _, err := client.Up(cmd.Context(), &proto.UpRequest{}); err != nil {
|
|
cmd.PrintErrf("Failed to bring service up: %v\n", status.Convert(err).Message())
|
|
} else {
|
|
cmd.Println("netbird up")
|
|
time.Sleep(time.Second * 10)
|
|
}
|
|
}
|
|
|
|
initialLevelTrace := initialLogLevel.GetLevel() >= proto.LogLevel_TRACE
|
|
if !initialLevelTrace {
|
|
_, err = client.SetLogLevel(cmd.Context(), &proto.SetLogLevelRequest{
|
|
Level: proto.LogLevel_TRACE,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to set log level to TRACE: %v", status.Convert(err).Message())
|
|
}
|
|
cmd.Println("Log level set to trace.")
|
|
}
|
|
|
|
needsRestoreUp := false
|
|
if _, err := client.Down(cmd.Context(), &proto.DownRequest{}); err != nil {
|
|
cmd.PrintErrf("Failed to bring service down: %v\n", status.Convert(err).Message())
|
|
} else {
|
|
needsRestoreUp = !stateWasDown
|
|
cmd.Println("netbird down")
|
|
}
|
|
|
|
time.Sleep(1 * time.Second)
|
|
|
|
// Enable sync response persistence before bringing the service up
|
|
if _, err := client.SetSyncResponsePersistence(cmd.Context(), &proto.SetSyncResponsePersistenceRequest{
|
|
Enabled: true,
|
|
}); err != nil {
|
|
cmd.PrintErrf("Failed to enable sync response persistence: %v\n", status.Convert(err).Message())
|
|
}
|
|
|
|
if _, err := client.Up(cmd.Context(), &proto.UpRequest{}); err != nil {
|
|
cmd.PrintErrf("Failed to bring service up: %v\n", status.Convert(err).Message())
|
|
} else {
|
|
needsRestoreUp = false
|
|
cmd.Println("netbird up")
|
|
}
|
|
|
|
time.Sleep(3 * time.Second)
|
|
|
|
cpuProfilingStarted := false
|
|
if _, err := client.StartCPUProfile(cmd.Context(), &proto.StartCPUProfileRequest{}); err != nil {
|
|
cmd.PrintErrf("Failed to start CPU profiling: %v\n", err)
|
|
} else {
|
|
cpuProfilingStarted = true
|
|
defer func() {
|
|
if cpuProfilingStarted {
|
|
if _, err := client.StopCPUProfile(cmd.Context(), &proto.StopCPUProfileRequest{}); err != nil {
|
|
cmd.PrintErrf("Failed to stop CPU profiling: %v\n", err)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
captureStarted := false
|
|
if wantCapture, _ := cmd.Flags().GetBool("capture"); wantCapture {
|
|
captureTimeout := duration + 30*time.Second
|
|
const maxBundleCapture = 10 * time.Minute
|
|
if captureTimeout > maxBundleCapture {
|
|
captureTimeout = maxBundleCapture
|
|
}
|
|
_, err := client.StartBundleCapture(cmd.Context(), &proto.StartBundleCaptureRequest{
|
|
Timeout: durationpb.New(captureTimeout),
|
|
})
|
|
if err != nil {
|
|
cmd.PrintErrf("Failed to start packet capture: %v\n", status.Convert(err).Message())
|
|
} else {
|
|
captureStarted = true
|
|
cmd.Println("Packet capture started.")
|
|
// Safety: always stop on exit, even if the normal stop below runs too.
|
|
defer func() {
|
|
if captureStarted {
|
|
stopCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
if _, err := client.StopBundleCapture(stopCtx, &proto.StopBundleCaptureRequest{}); err != nil {
|
|
cmd.PrintErrf("Failed to stop packet capture: %v\n", err)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
if waitErr := waitForDurationOrCancel(cmd.Context(), duration, cmd); waitErr != nil {
|
|
return waitErr
|
|
}
|
|
cmd.Println("\nDuration completed")
|
|
|
|
if captureStarted {
|
|
stopCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
if _, err := client.StopBundleCapture(stopCtx, &proto.StopBundleCaptureRequest{}); err != nil {
|
|
cmd.PrintErrf("Failed to stop packet capture: %v\n", err)
|
|
} else {
|
|
captureStarted = false
|
|
cmd.Println("Packet capture stopped.")
|
|
}
|
|
}
|
|
|
|
if cpuProfilingStarted {
|
|
if _, err := client.StopCPUProfile(cmd.Context(), &proto.StopCPUProfileRequest{}); err != nil {
|
|
cmd.PrintErrf("Failed to stop CPU profiling: %v\n", err)
|
|
} else {
|
|
cpuProfilingStarted = false
|
|
}
|
|
}
|
|
|
|
cmd.Println("Creating debug bundle...")
|
|
|
|
request := &proto.DebugBundleRequest{
|
|
Anonymize: anonymizeEnabled,
|
|
AnonymizeLevel: anonymizeLevel.String(),
|
|
SystemInfo: systemInfoFlag,
|
|
LogFileCount: logFileCount,
|
|
CliVersion: version.NetbirdVersion(),
|
|
}
|
|
if uploadBundleFlag {
|
|
request.UploadURL = uploadBundleURLFlag
|
|
request.UploadInsecure = uploadBundleInsecureFlag
|
|
}
|
|
resp, err := client.DebugBundle(cmd.Context(), request)
|
|
if err != nil {
|
|
return daemonCallError("bundle debug", err)
|
|
}
|
|
|
|
if needsRestoreUp {
|
|
if _, err := client.Up(cmd.Context(), &proto.UpRequest{}); err != nil {
|
|
cmd.PrintErrf("Failed to restore service up state: %v\n", status.Convert(err).Message())
|
|
} else {
|
|
cmd.Println("netbird up (restored)")
|
|
}
|
|
}
|
|
|
|
if stateWasDown {
|
|
if _, err := client.Down(cmd.Context(), &proto.DownRequest{}); err != nil {
|
|
cmd.PrintErrf("Failed to restore service down state: %v\n", status.Convert(err).Message())
|
|
} else {
|
|
cmd.Println("netbird down")
|
|
}
|
|
}
|
|
|
|
if !initialLevelTrace {
|
|
if _, err := client.SetLogLevel(cmd.Context(), &proto.SetLogLevelRequest{Level: initialLogLevel.GetLevel()}); err != nil {
|
|
cmd.PrintErrf("Failed to restore log level: %v\n", status.Convert(err).Message())
|
|
} else {
|
|
cmd.Println("Log level restored to", initialLogLevel.GetLevel())
|
|
}
|
|
}
|
|
|
|
cmd.Printf("Local file:\n%s\n", resp.GetPath())
|
|
|
|
if resp.GetUploadFailureReason() != "" {
|
|
return fmt.Errorf("upload failed: %s", resp.GetUploadFailureReason())
|
|
}
|
|
|
|
if uploadBundleFlag {
|
|
cmd.Printf("Upload file key:\n%s\n", resp.GetUploadedKey())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func setSyncResponsePersistence(cmd *cobra.Command, args []string) error {
|
|
conn, err := getClient(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if err := conn.Close(); err != nil {
|
|
log.Errorf(errCloseConnection, err)
|
|
}
|
|
}()
|
|
|
|
persistence := strings.ToLower(args[0])
|
|
if persistence != "on" && persistence != "off" {
|
|
return fmt.Errorf("invalid persistence value: %s. Use 'on' or 'off'", args[0])
|
|
}
|
|
|
|
client := proto.NewDaemonServiceClient(conn)
|
|
_, err = client.SetSyncResponsePersistence(cmd.Context(), &proto.SetSyncResponsePersistenceRequest{
|
|
Enabled: persistence == "on",
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to set sync response persistence: %v", status.Convert(err).Message())
|
|
}
|
|
|
|
cmd.Printf("Sync response persistence set to: %s\n", persistence)
|
|
return nil
|
|
}
|
|
|
|
func waitForDurationOrCancel(ctx context.Context, duration time.Duration, cmd *cobra.Command) error {
|
|
ticker := time.NewTicker(1 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
startTime := time.Now()
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
elapsed := time.Since(startTime)
|
|
if elapsed >= duration {
|
|
return
|
|
}
|
|
remaining := duration - elapsed
|
|
cmd.Printf("\rRemaining time: %s", formatDuration(remaining))
|
|
}
|
|
}
|
|
}()
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-done:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func formatDuration(d time.Duration) string {
|
|
d = d.Round(time.Second)
|
|
h := d / time.Hour
|
|
d %= time.Hour
|
|
m := d / time.Minute
|
|
d %= time.Minute
|
|
s := d / time.Second
|
|
return fmt.Sprintf("%02d:%02d:%02d", h, m, s)
|
|
}
|
|
|
|
func generateDebugBundle(config *profilemanager.Config, recorder *peer.Status, connectClient *internal.ConnectClient, logFilePath string) {
|
|
var syncResponse *mgmProto.SyncResponse
|
|
var err error
|
|
|
|
if connectClient != nil {
|
|
syncResponse, err = connectClient.GetLatestSyncResponse()
|
|
if err != nil {
|
|
log.Warnf("Failed to get latest sync response: %v", err)
|
|
}
|
|
}
|
|
|
|
bundleGenerator := debug.NewBundleGenerator(
|
|
debug.GeneratorDependencies{
|
|
InternalConfig: config,
|
|
StatusRecorder: recorder,
|
|
SyncResponse: syncResponse,
|
|
LogPath: logFilePath,
|
|
CPUProfile: nil,
|
|
DaemonVersion: version.NetbirdVersion(), // acting as daemon
|
|
},
|
|
debug.BundleConfig{
|
|
IncludeSystemInfo: true,
|
|
},
|
|
)
|
|
|
|
path, err := bundleGenerator.Generate()
|
|
if err != nil {
|
|
log.Errorf("Failed to generate debug bundle: %v", err)
|
|
return
|
|
}
|
|
log.Infof("Generated debug bundle from SIGUSR1 at: %s", path)
|
|
}
|
|
|
|
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().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().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")
|
|
}
|