diff --git a/client/android/profile_manager.go b/client/android/profile_manager.go index 60e4d5c32..bc3c63512 100644 --- a/client/android/profile_manager.go +++ b/client/android/profile_manager.go @@ -137,7 +137,7 @@ func (pm *ProfileManager) SwitchProfile(profileName string) error { // AddProfile creates a new profile func (pm *ProfileManager) AddProfile(profileName string) error { // Use ServiceManager (creates profile in profiles/ directory) - if err := pm.serviceMgr.AddProfile(profileName, androidUsername); err != nil { + if err := pm.serviceMgr.AddProfile(profileName, androidUsername, nil); err != nil { return fmt.Errorf("failed to add profile: %w", err) } diff --git a/client/cmd/owner.go b/client/cmd/owner.go new file mode 100644 index 000000000..1dbd2db1b --- /dev/null +++ b/client/cmd/owner.go @@ -0,0 +1,84 @@ +package cmd + +import ( + "fmt" + "strconv" + + "github.com/spf13/cobra" + + "github.com/netbirdio/netbird/client/proto" +) + +var ownerCmd = &cobra.Command{ + Use: "owner", + Short: "Manage daemon owner UIDs", + Long: `Manage the list of UIDs allowed to control the NetBird daemon. + +Owners are persisted in the active profile config and survive daemon restarts. +The first call from the user logged in at the GUI / console session claims +ownership automatically; these subcommands cover the rest of the lifecycle.`, +} + +var ownerAddCmd = &cobra.Command{ + Use: "add ", + Short: "Add a UID as an owner of the daemon", + Long: `Add a UID to the active profile's owner list. Requires root or an +existing owner. Use this to grant another local user permanent access without +having them log in at the console first.`, + Args: cobra.ExactArgs(1), + RunE: addOwnerFunc, +} + +var ownerResetCmd = &cobra.Command{ + Use: "reset", + Short: "Clear the daemon's owner list", + Long: `Clear the active profile's owner list, returning the daemon to its +unconfigured state. The next call from the active console-session user will +re-claim ownership. Requires root.`, + RunE: resetOwnerFunc, +} + +func addOwnerFunc(cmd *cobra.Command, args []string) error { + if err := setupCmd(cmd); err != nil { + return err + } + + uid, err := strconv.ParseUint(args[0], 10, 32) + if err != nil { + return fmt.Errorf("parse uid %q: %w", args[0], err) + } + + conn, err := DialClientGRPCServer(cmd.Context(), daemonAddr) + if err != nil { + return fmt.Errorf("connect to daemon: %w", err) + } + defer conn.Close() + + client := proto.NewDaemonServiceClient(conn) + if _, err := client.AddOwner(cmd.Context(), &proto.AddOwnerRequest{Uid: uint32(uid)}); err != nil { + return fmt.Errorf("add owner: %w", err) + } + + cmd.Printf("UID %d added as owner\n", uid) + return nil +} + +func resetOwnerFunc(cmd *cobra.Command, _ []string) error { + if err := setupCmd(cmd); err != nil { + return err + } + + conn, err := DialClientGRPCServer(cmd.Context(), daemonAddr) + if err != nil { + return fmt.Errorf("connect to daemon: %w", err) + } + defer conn.Close() + + client := proto.NewDaemonServiceClient(conn) + if _, err := client.ResetOwner(cmd.Context(), &proto.ResetOwnerRequest{}); err != nil { + return fmt.Errorf("reset owner: %w", err) + } + + cmd.Println("daemon owner list cleared; next call from the active console user will re-claim ownership") + return nil +} diff --git a/client/cmd/root.go b/client/cmd/root.go index 0a0aa4197..e90e2b713 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -23,6 +23,7 @@ import ( "google.golang.org/grpc/credentials/insecure" daddr "github.com/netbirdio/netbird/client/internal/daemonaddr" + "github.com/netbirdio/netbird/client/internal/owner" "github.com/netbirdio/netbird/client/internal/profilemanager" ) @@ -156,8 +157,12 @@ func init() { rootCmd.AddCommand(forwardingRulesCmd) rootCmd.AddCommand(debugCmd) rootCmd.AddCommand(profileCmd) + rootCmd.AddCommand(ownerCmd) rootCmd.AddCommand(exposeCmd) + ownerCmd.AddCommand(ownerAddCmd) + ownerCmd.AddCommand(ownerResetCmd) + networksCMD.AddCommand(routesListCmd) networksCMD.AddCommand(routesSelectCmd, routesDeselectCmd) @@ -250,11 +255,24 @@ func DialClientGRPCServer(ctx context.Context, addr string) (*grpc.ClientConn, e return grpc.DialContext( ctx, strings.TrimPrefix(addr, "tcp://"), - grpc.WithTransportCredentials(insecure.NewCredentials()), + daemonDialTransportOption(addr), grpc.WithBlock(), ) } +// daemonDialTransportOption returns the appropriate transport credentials for connecting +// to the daemon. On Unix socket platforms, uses Unix transport credentials so the server +// can extract the caller's UID for owner verification. Otherwise, uses insecure credentials. +func daemonDialTransportOption(addr string) grpc.DialOption { + if strings.HasPrefix(addr, "unix://") { + creds := owner.NewUnixTransportCredentials() + if creds != nil { + return grpc.WithTransportCredentials(creds) + } + } + return grpc.WithTransportCredentials(insecure.NewCredentials()) +} + // WithBackOff execute function in backoff cycle. func WithBackOff(bf func() error) error { return backoff.RetryNotify(bf, CLIBackOffSettings, func(err error, duration time.Duration) { diff --git a/client/cmd/service_controller.go b/client/cmd/service_controller.go index 88121c067..51ddf4104 100644 --- a/client/cmd/service_controller.go +++ b/client/cmd/service_controller.go @@ -16,6 +16,7 @@ import ( "github.com/spf13/cobra" "google.golang.org/grpc" + "github.com/netbirdio/netbird/client/internal/owner" "github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/server" "github.com/netbirdio/netbird/client/system" @@ -29,9 +30,6 @@ func (p *program) Start(svc service.Service) error { // Collect static system and platform information system.UpdateStaticInfoAsync() - // in any case, even if configuration does not exists we run daemon to serve CLI gRPC API. - p.serv = grpc.NewServer() - split := strings.Split(daemonAddr, "://") switch split[0] { case "unix": @@ -47,6 +45,12 @@ func (p *program) Start(svc service.Service) error { return fmt.Errorf("unsupported daemon address protocol: %v", split[0]) } + // Set up owner enforcement for Unix sockets. + configAdapter := &owner.ConfigAdapter{} + serverOpts := ownerServerOpts(split[0], configAdapter) + + p.serv = grpc.NewServer(serverOpts...) + listen, err := net.Listen(split[0], split[1]) if err != nil { return fmt.Errorf("listen daemon interface: %w", err) @@ -65,6 +69,8 @@ func (p *program) Start(svc service.Service) error { if err := serverInstance.Start(); err != nil { log.Fatalf("failed to start daemon: %v", err) } + + configAdapter.SetBackend(serverInstance) proto.RegisterDaemonServiceServer(p.serv, serverInstance) p.serverInstanceMu.Lock() @@ -79,6 +85,32 @@ func (p *program) Start(svc service.Service) error { return nil } +// ownerServerOpts returns gRPC server options for owner enforcement. +// On Unix socket platforms, this includes transport credentials for peer credential +// extraction and interceptors that check the caller's UID. On other platforms or TCP, +// no owner enforcement is applied and a warning is logged so operators know the daemon +// is running without per-user authorization. +func ownerServerOpts(protocol string, configAdapter *owner.ConfigAdapter) []grpc.ServerOption { + if protocol != "unix" { + log.Warnf("daemon socket owner enforcement is not applied for protocol %q", protocol) + return nil + } + + creds := owner.NewUnixTransportCredentials() + if creds == nil { + log.Warnf("daemon socket owner enforcement unavailable on this platform; daemon will accept any local connection") + return nil + } + + interceptor := owner.NewInterceptor(configAdapter) + + return []grpc.ServerOption{ + grpc.Creds(creds), + grpc.ChainUnaryInterceptor(interceptor.UnaryInterceptor()), + grpc.ChainStreamInterceptor(interceptor.StreamInterceptor()), + } +} + func (p *program) Stop(srv service.Service) error { p.serverInstanceMu.Lock() if p.serverInstance != nil { diff --git a/client/cmd/up.go b/client/cmd/up.go index cabd0aacf..7143cf444 100644 --- a/client/cmd/up.go +++ b/client/cmd/up.go @@ -44,6 +44,9 @@ const ( profileNameFlag = "profile" profileNameDesc = "profile name to use for the login. If not specified, the last used profile will be used." + + claimOwnerFlag = "owner" + claimOwnerDesc = "claim owner privileges for this profile, restricting daemon control to the current user and root" ) var ( @@ -54,6 +57,7 @@ var ( showQR bool profileName string configPath string + claimOwner bool upCmd = &cobra.Command{ Use: "up", @@ -87,6 +91,7 @@ func init() { upCmd.PersistentFlags().BoolVar(&showQR, showQRFlag, false, showQRDesc) upCmd.PersistentFlags().StringVar(&profileName, profileNameFlag, "", profileNameDesc) upCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "(DEPRECATED) NetBird config file location. ") + upCmd.PersistentFlags().BoolVar(&claimOwner, claimOwnerFlag, false, claimOwnerDesc) } @@ -331,6 +336,7 @@ func doDaemonUp(ctx context.Context, cmd *cobra.Command, client proto.DaemonServ if _, err := client.Up(ctx, &proto.UpRequest{ ProfileName: &activeProf.Name, Username: &username, + ClaimOwner: claimOwner, }); err != nil { return fmt.Errorf("call service up method: %v", err) } diff --git a/client/cmd/up_daemon_test.go b/client/cmd/up_daemon_test.go index 682a45365..d86d7e5bf 100644 --- a/client/cmd/up_daemon_test.go +++ b/client/cmd/up_daemon_test.go @@ -29,7 +29,7 @@ func TestUpDaemon(t *testing.T) { } sm := profilemanager.ServiceManager{} - err = sm.AddProfile("test1", currUser.Username) + err = sm.AddProfile("test1", currUser.Username, nil) if err != nil { t.Fatalf("failed to add profile: %v", err) return diff --git a/client/internal/owner/config.go b/client/internal/owner/config.go new file mode 100644 index 000000000..9fd332d17 --- /dev/null +++ b/client/internal/owner/config.go @@ -0,0 +1,46 @@ +package owner + +import ( + "fmt" + "sync" +) + +// ConfigAdapter is a thread-safe OwnerConfig that delegates to a lazily-set backend. +// This allows the interceptor to be created before the daemon server (and its config) +// is initialized, which is necessary because gRPC interceptors are set at server creation time. +type ConfigAdapter struct { + mu sync.RWMutex + backend OwnerConfig +} + +// SetBackend sets the actual config implementation. Must be called before any RPCs are served. +func (a *ConfigAdapter) SetBackend(backend OwnerConfig) { + a.mu.Lock() + defer a.mu.Unlock() + a.backend = backend +} + +// GetOwnerUIDs delegates to the backend. +func (a *ConfigAdapter) GetOwnerUIDs() []UID { + a.mu.RLock() + defer a.mu.RUnlock() + + if a.backend == nil { + // No backend yet, return empty (root-only). + return []UID{} + } + + return a.backend.GetOwnerUIDs() +} + +// AddOwnerUID delegates to the backend. +func (a *ConfigAdapter) AddOwnerUID(uid UID) error { + a.mu.RLock() + defer a.mu.RUnlock() + + if a.backend == nil { + return fmt.Errorf("owner config backend not initialized") + } + + return a.backend.AddOwnerUID(uid) +} diff --git a/client/internal/owner/consoleuser/consoleuser.go b/client/internal/owner/consoleuser/consoleuser.go new file mode 100644 index 000000000..bae830d40 --- /dev/null +++ b/client/internal/owner/consoleuser/consoleuser.go @@ -0,0 +1,17 @@ +// Package consoleuser provides the OS-level "active console user" UID lookup +// used to gate ownership TOFU. The active console user is the local user +// physically at the machine (or in the foreground GUI session): the user that +// can legitimately claim the daemon as theirs on first run. +package consoleuser + +// ActiveUID returns the UID of the currently active console / GUI session +// user, and true if such a user exists. Returns 0, false on platforms without +// a console concept (ios, android), on headless servers with no active +// session, or on lookup failure. +// +// Implementations must fail closed: any error or ambiguity returns (0, false) +// so that the caller treats the result as "no console user" rather than +// granting access to an unverified UID. +func ActiveUID() (uint32, bool) { + return activeUID() +} diff --git a/client/internal/owner/consoleuser/consoleuser_darwin.go b/client/internal/owner/consoleuser/consoleuser_darwin.go new file mode 100644 index 000000000..5f7a8f6a0 --- /dev/null +++ b/client/internal/owner/consoleuser/consoleuser_darwin.go @@ -0,0 +1,58 @@ +package consoleuser + +import ( + "unsafe" + + "github.com/ebitengine/purego" +) + +// activeUID returns the UID of the user currently logged into the macOS GUI +// console session. Uses SCDynamicStoreCopyConsoleUser from the +// SystemConfiguration framework via purego (no cgo). +func activeUID() (uint32, bool) { + sc, err := purego.Dlopen( + "/System/Library/Frameworks/SystemConfiguration.framework/SystemConfiguration", + purego.RTLD_NOW|purego.RTLD_GLOBAL, + ) + if err != nil { + return 0, false + } + + cf, err := purego.Dlopen( + "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation", + purego.RTLD_NOW|purego.RTLD_GLOBAL, + ) + if err != nil { + return 0, false + } + + // CFStringRef SCDynamicStoreCopyConsoleUser(SCDynamicStoreRef store, + // uid_t *uid, gid_t *gid); + // + // We pass nil for the store (NULL is accepted; the framework creates a + // transient one), discard the returned CFStringRef username (we only + // need the UID), and read uid via the out-pointer. + var copyConsoleUser func(store uintptr, uidPtr, gidPtr unsafe.Pointer) uintptr + purego.RegisterLibFunc(©ConsoleUser, sc, "SCDynamicStoreCopyConsoleUser") + + var cfRelease func(uintptr) + purego.RegisterLibFunc(&cfRelease, cf, "CFRelease") + + var uid uint32 + var gid uint32 + + cfStr := copyConsoleUser(0, unsafe.Pointer(&uid), unsafe.Pointer(&gid)) + if cfStr == 0 { + return 0, false + } + cfRelease(cfStr) + + // loginwindow / no GUI session reports uid 0. We don't want the + // console-user path to grant anything to root (root is already always + // allowed by the interceptor), so treat uid 0 as "no console user". + if uid == 0 { + return 0, false + } + + return uid, true +} diff --git a/client/internal/owner/consoleuser/consoleuser_freebsd.go b/client/internal/owner/consoleuser/consoleuser_freebsd.go new file mode 100644 index 000000000..c11e4db3e --- /dev/null +++ b/client/internal/owner/consoleuser/consoleuser_freebsd.go @@ -0,0 +1,34 @@ +package consoleuser + +import ( + "fmt" + "os" + "syscall" +) + +// activeUID returns the UID of the user currently logged into the FreeBSD +// console. FreeBSD's vt(4) chowns the active virtual terminal device to the +// logged-in user, so a non-root owner of any /dev/ttyvN reliably identifies +// the console user. +// +// We scan /dev/ttyv0../dev/ttyv9 and return the first non-root owner. Network +// ptys (pts) are intentionally not considered: SSH'd users are not "at the +// console" and must not TOFU-claim ownership. +func activeUID() (uint32, bool) { + for i := 0; i < 10; i++ { + path := fmt.Sprintf("/dev/ttyv%d", i) + fi, err := os.Stat(path) + if err != nil { + continue + } + st, ok := fi.Sys().(*syscall.Stat_t) + if !ok { + continue + } + if st.Uid == 0 { + continue + } + return st.Uid, true + } + return 0, false +} diff --git a/client/internal/owner/consoleuser/consoleuser_linux.go b/client/internal/owner/consoleuser/consoleuser_linux.go new file mode 100644 index 000000000..d3df45524 --- /dev/null +++ b/client/internal/owner/consoleuser/consoleuser_linux.go @@ -0,0 +1,64 @@ +package consoleuser + +import ( + "github.com/godbus/dbus/v5" +) + +const ( + loginDest = "org.freedesktop.login1" + loginPath = dbus.ObjectPath("/org/freedesktop/login1") + loginInterface = "org.freedesktop.login1.Manager" + listSessions = loginInterface + ".ListSessions" + + sessionInterface = "org.freedesktop.login1.Session" + sessionActive = sessionInterface + ".Active" + sessionClass = sessionInterface + ".Class" +) + +// activeUID queries systemd-logind for the active local user session and +// returns that user's UID. Falls back to (0, false) on any error or when no +// active user session exists (headless box, no GUI, no login at the console). +func activeUID() (uint32, bool) { + conn, err := dbus.SystemBus() + if err != nil { + return 0, false + } + + mgr := conn.Object(loginDest, loginPath) + + // ListSessions returns []struct{ID string; UID uint32; User string; + // Seat string; Path dbus.ObjectPath}. + var sessions []struct { + ID string + UID uint32 + User string + Seat string + Path dbus.ObjectPath + } + if err := mgr.Call(listSessions, 0).Store(&sessions); err != nil { + return 0, false + } + + for _, s := range sessions { + obj := conn.Object(loginDest, s.Path) + + active, err := obj.GetProperty(sessionActive) + if err != nil || active.Value() != true { + continue + } + + class, err := obj.GetProperty(sessionClass) + if err != nil { + continue + } + // Only "user" sessions count; "greeter" / "lock-screen" / etc. are + // not someone we should grant ownership to. + if classStr, ok := class.Value().(string); !ok || classStr != "user" { + continue + } + + return s.UID, true + } + + return 0, false +} diff --git a/client/internal/owner/consoleuser/consoleuser_other.go b/client/internal/owner/consoleuser/consoleuser_other.go new file mode 100644 index 000000000..b1f959588 --- /dev/null +++ b/client/internal/owner/consoleuser/consoleuser_other.go @@ -0,0 +1,9 @@ +//go:build !linux && !darwin && !freebsd && !windows + +package consoleuser + +// activeUID has no meaning on platforms without a console-user concept +// (ios, android). Returns no-user so TOFU never fires. +func activeUID() (uint32, bool) { + return 0, false +} diff --git a/client/internal/owner/consoleuser/consoleuser_windows.go b/client/internal/owner/consoleuser/consoleuser_windows.go new file mode 100644 index 000000000..3e3d15b00 --- /dev/null +++ b/client/internal/owner/consoleuser/consoleuser_windows.go @@ -0,0 +1,59 @@ +package consoleuser + +import ( + "unsafe" + + "golang.org/x/sys/windows" +) + +// activeUID returns a synthetic UID (the user SID's RID) for the currently +// active Windows console session. The owner package treats UIDs as opaque +// uint32 identifiers; on Windows we use the user account RID, which is stable +// per-account on a given machine. +// +// Returns (0, false) when there is no active console session, the session has +// no logged-in user, or any lookup fails. +func activeUID() (uint32, bool) { + sessionID := windows.WTSGetActiveConsoleSessionId() + if sessionID == 0xFFFFFFFF { + return 0, false + } + + var token windows.Token + if err := windows.WTSQueryUserToken(sessionID, &token); err != nil { + return 0, false + } + defer token.Close() + + user, err := tokenUserSID(token) + if err != nil || user == nil { + return 0, false + } + + subCount := user.SubAuthorityCount() + if subCount == 0 { + return 0, false + } + rid := user.SubAuthority(uint32(subCount) - 1) + if rid == 0 { + return 0, false + } + return rid, true +} + +// tokenUserSID returns the user SID associated with the given access token. +func tokenUserSID(token windows.Token) (*windows.SID, error) { + var size uint32 + err := windows.GetTokenInformation(token, windows.TokenUser, nil, 0, &size) + if err != windows.ERROR_INSUFFICIENT_BUFFER { + return nil, err + } + + buf := make([]byte, size) + if err := windows.GetTokenInformation(token, windows.TokenUser, &buf[0], size, &size); err != nil { + return nil, err + } + + tu := (*windows.Tokenuser)(unsafe.Pointer(&buf[0])) + return tu.User.Sid, nil +} diff --git a/client/internal/owner/creds.go b/client/internal/owner/creds.go new file mode 100644 index 000000000..9ecb4aa47 --- /dev/null +++ b/client/internal/owner/creds.go @@ -0,0 +1,37 @@ +package owner + +import ( + "context" + + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/peer" +) + +// UnixAuthInfo implements credentials.AuthInfo carrying the peer's UID from SO_PEERCRED. +type UnixAuthInfo struct { + credentials.CommonAuthInfo + UID UID + GID uint32 + PID int32 +} + +// AuthType returns the authentication type. +func (u UnixAuthInfo) AuthType() string { + return "unix_peercred" +} + +// UIDFromContext extracts the caller's UID from the gRPC peer context. +// Returns uid and true if Unix credentials were available, 0 and false otherwise. +func UIDFromContext(ctx context.Context) (UID, bool) { + p, ok := peer.FromContext(ctx) + if !ok { + return 0, false + } + + info, ok := p.AuthInfo.(UnixAuthInfo) + if !ok { + return 0, false + } + + return info.UID, true +} diff --git a/client/internal/owner/env.go b/client/internal/owner/env.go new file mode 100644 index 000000000..832db5681 --- /dev/null +++ b/client/internal/owner/env.go @@ -0,0 +1,48 @@ +package owner + +import ( + "os" + "strconv" + "strings" + + log "github.com/sirupsen/logrus" +) + +// EnvOwnerUID is the environment variable that seeds the owner UID list for new config files. +// MDM deployments can set this (e.g. via --service-env NB_OWNER_UID=1000) so the first +// config created by the daemon pre-populates the owner without requiring "netbird up --owner". +// Multiple UIDs can be comma-separated: NB_OWNER_UID=1000,1001 +const EnvOwnerUID = "NB_OWNER_UID" + +// OwnerUIDsFromEnv parses NB_OWNER_UID into a UID slice. +// Returns nil if the variable is unset, allowing the caller to distinguish +// "not configured" from "explicitly empty". +func OwnerUIDsFromEnv() []UID { + val := os.Getenv(EnvOwnerUID) + if val == "" { + return nil + } + + parts := strings.Split(val, ",") + uids := make([]UID, 0, len(parts)) + for _, p := range parts { + p = strings.TrimSpace(p) + if p == "" { + continue + } + uid, err := strconv.ParseUint(p, 10, 32) + if err != nil { + log.Warnf("ignoring invalid UID %q in %s: %v", p, EnvOwnerUID, err) + continue + } + uids = append(uids, UID(uid)) + } + + if len(uids) == 0 { + log.Warnf("%s set but contains no valid UIDs, defaulting to root-only", EnvOwnerUID) + return []UID{} + } + + log.Infof("seeding owner UIDs from %s: %v", EnvOwnerUID, uids) + return uids +} diff --git a/client/internal/owner/env_test.go b/client/internal/owner/env_test.go new file mode 100644 index 000000000..173d01017 --- /dev/null +++ b/client/internal/owner/env_test.go @@ -0,0 +1,81 @@ +package owner + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestOwnerUIDsFromEnv(t *testing.T) { + tests := []struct { + name string + envValue string + unset bool + want []UID + }{ + { + name: "unset returns nil", + unset: true, + want: nil, + }, + { + name: "empty string returns nil", + envValue: "", + want: nil, + }, + { + name: "single UID", + envValue: "1000", + want: []UID{1000}, + }, + { + name: "multiple UIDs", + envValue: "1000,1001,1002", + want: []UID{1000, 1001, 1002}, + }, + { + name: "spaces around UIDs", + envValue: " 1000 , 1001 ", + want: []UID{1000, 1001}, + }, + { + name: "invalid UID skipped", + envValue: "1000,notanumber,1001", + want: []UID{1000, 1001}, + }, + { + name: "all invalid returns empty slice", + envValue: "abc,def", + want: []UID{}, + }, + { + name: "trailing comma", + envValue: "1000,", + want: []UID{1000}, + }, + { + name: "zero UID is valid", + envValue: "0", + want: []UID{0}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv(EnvOwnerUID, tt.envValue) + if tt.unset { + os.Unsetenv(EnvOwnerUID) + } + + got := OwnerUIDsFromEnv() + + if tt.want == nil { + require.Nil(t, got) + } else { + assert.Equal(t, tt.want, got) + } + }) + } +} diff --git a/client/internal/owner/interceptor.go b/client/internal/owner/interceptor.go new file mode 100644 index 000000000..9619839af --- /dev/null +++ b/client/internal/owner/interceptor.go @@ -0,0 +1,170 @@ +package owner + +import ( + "context" + "slices" + "sync" + + log "github.com/sirupsen/logrus" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/netbirdio/netbird/client/internal/owner/consoleuser" +) + +const servicePath = "/daemon.DaemonService/" + +// profileBypassMethods skip the active-profile owner check. They either +// operate on a specific target profile (and the handler enforces target-profile +// owner-or-root itself) or are per-user listings/creations that don't affect +// the active session and shouldn't require active-profile ownership. Peer +// credentials are still required. +var profileBypassMethods = map[string]bool{ + servicePath + "AddProfile": true, + servicePath + "ListProfiles": true, + servicePath + "RemoveProfile": true, + servicePath + "SwitchProfile": true, +} + +// Error messages returned to denied callers. They are multi-line so the +// suggested commands sit on their own line for easy triple-click copy-paste. +const ( + errNoPeerCreds = "peer credentials unavailable; rerun via the netbird CLI" + + errNoOwnerConfigured = `no daemon owner is configured and no console-session user matches your UID. +Run as root for one-off use: + sudo netbird ... +Or call from the active console session: the first call from the user logged in +at the GUI/console claims ownership automatically.` + + errOwnerRequired = `this operation requires root or the daemon owner (uid %d is not an owner). +Run as root for one-off use: + sudo netbird ... +Or ask an existing owner (or root) to add you: + sudo netbird owner add %[1]d` +) + +// consoleUIDLookup is the function used to look up the active console UID. +// Overridable in tests; defaults to the platform implementation. +var consoleUIDLookup = consoleuser.ActiveUID + +// OwnerConfig provides access to the current owner UIDs setting. +// The interceptor reads and writes through this interface so it can +// work with the profile manager's config without a direct dependency. +type OwnerConfig interface { + // GetOwnerUIDs returns the current owner UIDs. + // nil means legacy/migration TOFU (field absent from existing config). + // empty means fresh install (root-only with console-user TOFU exception). + // populated means those UIDs plus root may control the daemon. + GetOwnerUIDs() []UID + + // AddOwnerUID adds the given UID to the owner list and persists it. + AddOwnerUID(uid UID) error +} + +// Interceptor enforces owner restrictions on the daemon gRPC socket. +type Interceptor struct { + config OwnerConfig + // mu serializes the read-then-write of OwnerUIDs during TOFU/claim flows + // so two concurrent first-callers can't both end up persisted as owners. + // Holds across the OwnerConfig.AddOwnerUID call; safe because no callback + // path takes this mutex. + mu sync.Mutex +} + +// NewInterceptor creates an owner interceptor backed by the given config. +func NewInterceptor(config OwnerConfig) *Interceptor { + return &Interceptor{config: config} +} + +// UnaryInterceptor returns a gRPC unary server interceptor that enforces owner policy. +func (i *Interceptor) UnaryInterceptor() grpc.UnaryServerInterceptor { + return func( + ctx context.Context, + req any, + info *grpc.UnaryServerInfo, + handler grpc.UnaryHandler, + ) (any, error) { + if err := i.authorize(ctx, info.FullMethod); err != nil { + return nil, err + } + return handler(ctx, req) + } +} + +// StreamInterceptor returns a gRPC stream server interceptor that enforces owner policy. +func (i *Interceptor) StreamInterceptor() grpc.StreamServerInterceptor { + return func( + srv any, + ss grpc.ServerStream, + info *grpc.StreamServerInfo, + handler grpc.StreamHandler, + ) error { + if err := i.authorize(ss.Context(), info.FullMethod); err != nil { + return err + } + return handler(srv, ss) + } +} + +// authorize checks whether the caller is allowed to call the given method. +// Every RPC is gated; root is always allowed. Non-root callers are accepted +// when they are existing owners, when the config is in legacy TOFU state +// (claim on first call, preserves pre-enforcement behavior), or when the +// config is in fresh-install state and they match the active console user. +func (i *Interceptor) authorize(ctx context.Context, fullMethod string) error { + uid, ok := UIDFromContext(ctx) + if !ok { + return status.Error(codes.PermissionDenied, errNoPeerCreds) + } + + if uid == 0 { + return nil + } + + // Profile-management RPCs do their own per-target authorization in the + // handler. The interceptor only confirms peer credentials are present. + if profileBypassMethods[fullMethod] { + return nil + } + + i.mu.Lock() + defer i.mu.Unlock() + + ownerUIDs := i.config.GetOwnerUIDs() + + switch { + case ownerUIDs == nil: + // Legacy / migration TOFU: existing pre-enforcement config has no + // owners field. Any non-root local caller claims on first call so + // upgrades don't break. + return i.claim(uid, "migration TOFU") + + case len(ownerUIDs) == 0: + // Fresh-install root-only mode with a console-user exception so the + // GUI/CLI just works for the user physically at the machine. SSH'd + // or otherwise non-console callers are denied. + consoleUID, ok := consoleUIDLookup() + if ok && uint32(uid) == consoleUID { + return i.claim(uid, "console-user TOFU") + } + return status.Error(codes.PermissionDenied, errNoOwnerConfigured) + + case slices.Contains(ownerUIDs, uid): + return nil + + default: + return status.Errorf(codes.PermissionDenied, errOwnerRequired, uid) + } +} + +// claim adds uid to the owner list and persists it. The caller must hold i.mu. +func (i *Interceptor) claim(uid UID, reason string) error { + log.Infof("%s: claiming owner for UID %d", reason, uid) + if err := i.config.AddOwnerUID(uid); err != nil { + log.Errorf("persist owner UID: %v", err) + return status.Error(codes.Internal, "persist owner UID") + } + return nil +} diff --git a/client/internal/owner/interceptor_test.go b/client/internal/owner/interceptor_test.go new file mode 100644 index 000000000..b92abd0d5 --- /dev/null +++ b/client/internal/owner/interceptor_test.go @@ -0,0 +1,277 @@ +package owner + +import ( + "context" + "net" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/peer" + "google.golang.org/grpc/status" +) + +type mockOwnerConfig struct { + uids []UID + err error +} + +func (m *mockOwnerConfig) GetOwnerUIDs() []UID { + return m.uids +} + +func (m *mockOwnerConfig) AddOwnerUID(uid UID) error { + if m.err != nil { + return m.err + } + m.uids = append(m.uids, uid) + return nil +} + +func peerContext(uid UID) context.Context { + return peer.NewContext(context.Background(), &peer.Peer{ + Addr: &net.UnixAddr{Name: "/tmp/test.sock", Net: "unix"}, + AuthInfo: UnixAuthInfo{ + CommonAuthInfo: credentials.CommonAuthInfo{SecurityLevel: credentials.NoSecurity}, + UID: uid, + }, + }) +} + +func noPeerContext() context.Context { + return context.Background() +} + +// withConsoleUID overrides the platform console-user lookup for a single test. +func withConsoleUID(t *testing.T, uid uint32, ok bool) { + t.Helper() + prev := consoleUIDLookup + consoleUIDLookup = func() (uint32, bool) { return uid, ok } + t.Cleanup(func() { consoleUIDLookup = prev }) +} + +func TestInterceptor_RootAlwaysAllowed(t *testing.T) { + cfg := &mockOwnerConfig{uids: []UID{1000}} + interceptor := NewInterceptor(cfg) + + for _, method := range []string{ + "/daemon.DaemonService/Up", + "/daemon.DaemonService/Status", + "/daemon.DaemonService/Down", + } { + err := interceptor.authorize(peerContext(0), method) + assert.NoError(t, err, "root should always be allowed for %s", method) + } +} + +func TestInterceptor_NoPeerCreds_AlwaysDenies(t *testing.T) { + cfg := &mockOwnerConfig{uids: []UID{1000}} + interceptor := NewInterceptor(cfg) + + for _, method := range []string{ + "/daemon.DaemonService/Status", + "/daemon.DaemonService/Up", + "/daemon.DaemonService/SomeNewMethod", + } { + err := interceptor.authorize(noPeerContext(), method) + require.Error(t, err, "method %s should be denied without peer creds", method) + assert.Equal(t, codes.PermissionDenied, status.Code(err)) + } +} + +// TestInterceptor_LegacyMigration covers the nil-OwnerUIDs branch: +// pre-enforcement configs upgraded to this version. Any non-root local caller +// can claim on first call. +func TestInterceptor_LegacyMigration_AnyCallerClaims(t *testing.T) { + withConsoleUID(t, 0, false) // no console; should not matter for nil + cfg := &mockOwnerConfig{uids: nil} + interceptor := NewInterceptor(cfg) + + // First call from any UID claims regardless of method. + err := interceptor.authorize(peerContext(1000), "/daemon.DaemonService/Status") + require.NoError(t, err) + require.Equal(t, []UID{1000}, cfg.uids) + + // After claim, a different UID is denied. + err = interceptor.authorize(peerContext(2000), "/daemon.DaemonService/Status") + require.Error(t, err) + assert.Equal(t, codes.PermissionDenied, status.Code(err)) +} + +// TestInterceptor_FreshInstall covers the empty-OwnerUIDs branch: console-user +// can claim, others denied. +func TestInterceptor_FreshInstall_ConsoleUserClaims(t *testing.T) { + withConsoleUID(t, 1000, true) + cfg := &mockOwnerConfig{uids: []UID{}} + interceptor := NewInterceptor(cfg) + + err := interceptor.authorize(peerContext(1000), "/daemon.DaemonService/Status") + require.NoError(t, err) + require.Equal(t, []UID{1000}, cfg.uids) +} + +func TestInterceptor_FreshInstall_NonConsoleDenied(t *testing.T) { + withConsoleUID(t, 1000, true) + cfg := &mockOwnerConfig{uids: []UID{}} + interceptor := NewInterceptor(cfg) + + err := interceptor.authorize(peerContext(2000), "/daemon.DaemonService/Up") + require.Error(t, err) + assert.Equal(t, codes.PermissionDenied, status.Code(err)) + assert.Empty(t, cfg.uids, "non-console caller must not claim") +} + +func TestInterceptor_FreshInstall_NoConsole_Denied(t *testing.T) { + withConsoleUID(t, 0, false) + cfg := &mockOwnerConfig{uids: []UID{}} + interceptor := NewInterceptor(cfg) + + err := interceptor.authorize(peerContext(1000), "/daemon.DaemonService/Up") + require.Error(t, err) + assert.Equal(t, codes.PermissionDenied, status.Code(err)) +} + +func TestInterceptor_OwnerUID_AllowsOwner(t *testing.T) { + cfg := &mockOwnerConfig{uids: []UID{1000}} + interceptor := NewInterceptor(cfg) + + err := interceptor.authorize(peerContext(1000), "/daemon.DaemonService/Down") + assert.NoError(t, err) +} + +func TestInterceptor_OwnerUID_DeniesOther(t *testing.T) { + withConsoleUID(t, 9999, true) // console-user TOFU should not apply once owners exist + cfg := &mockOwnerConfig{uids: []UID{1000}} + interceptor := NewInterceptor(cfg) + + err := interceptor.authorize(peerContext(2000), "/daemon.DaemonService/Down") + require.Error(t, err) + assert.Equal(t, codes.PermissionDenied, status.Code(err)) +} + +func TestInterceptor_MultipleOwners(t *testing.T) { + cfg := &mockOwnerConfig{uids: []UID{1000, 2000}} + interceptor := NewInterceptor(cfg) + + err := interceptor.authorize(peerContext(1000), "/daemon.DaemonService/Down") + assert.NoError(t, err) + + err = interceptor.authorize(peerContext(2000), "/daemon.DaemonService/Up") + assert.NoError(t, err) + + err = interceptor.authorize(peerContext(3000), "/daemon.DaemonService/Down") + require.Error(t, err) + assert.Equal(t, codes.PermissionDenied, status.Code(err)) +} + +// TestInterceptor_UnknownMethodRequiresOwner pins the safe-by-default invariant: +// any future RPC still goes through owner enforcement. +func TestInterceptor_UnknownMethodRequiresOwner(t *testing.T) { + cfg := &mockOwnerConfig{uids: []UID{1000}} + interceptor := NewInterceptor(cfg) + + err := interceptor.authorize(peerContext(2000), "/daemon.DaemonService/SomeFutureMethod") + require.Error(t, err) + assert.Equal(t, codes.PermissionDenied, status.Code(err)) + + err = interceptor.authorize(peerContext(1000), "/daemon.DaemonService/SomeFutureMethod") + assert.NoError(t, err) +} + +func TestInterceptor_ErrorMessageActionable(t *testing.T) { + withConsoleUID(t, 9999, true) + cfg := &mockOwnerConfig{uids: []UID{1000}} + interceptor := NewInterceptor(cfg) + + err := interceptor.authorize(peerContext(2000), "/daemon.DaemonService/Down") + require.Error(t, err) + msg := status.Convert(err).Message() + assert.Contains(t, msg, "sudo netbird") + assert.Contains(t, msg, "owner add") +} + +func TestInterceptor_UnaryIntegration(t *testing.T) { + cfg := &mockOwnerConfig{uids: []UID{1000}} + interceptor := NewInterceptor(cfg) + + unary := interceptor.UnaryInterceptor() + + resp, err := unary(peerContext(1000), nil, &grpc.UnaryServerInfo{FullMethod: "/daemon.DaemonService/Down"}, func(ctx context.Context, req any) (any, error) { + return "ok", nil + }) + require.NoError(t, err) + assert.Equal(t, "ok", resp) + + _, err = unary(peerContext(2000), nil, &grpc.UnaryServerInfo{FullMethod: "/daemon.DaemonService/Down"}, func(ctx context.Context, req any) (any, error) { + t.Fatal("handler should not be called") + return nil, nil + }) + require.Error(t, err) + assert.Equal(t, codes.PermissionDenied, status.Code(err)) +} + +func TestInterceptor_StreamIntegration(t *testing.T) { + cfg := &mockOwnerConfig{uids: []UID{1000}} + interceptor := NewInterceptor(cfg) + + stream := interceptor.StreamInterceptor() + + called := false + err := stream(nil, &mockServerStream{ctx: peerContext(1000)}, + &grpc.StreamServerInfo{FullMethod: "/daemon.DaemonService/SubscribeEvents"}, + func(srv any, stream grpc.ServerStream) error { + called = true + return nil + }) + require.NoError(t, err) + assert.True(t, called) + + err = stream(nil, &mockServerStream{ctx: peerContext(2000)}, + &grpc.StreamServerInfo{FullMethod: "/daemon.DaemonService/SubscribeEvents"}, + func(srv any, stream grpc.ServerStream) error { + t.Fatal("handler should not be called") + return nil + }) + require.Error(t, err) + assert.Equal(t, codes.PermissionDenied, status.Code(err)) +} + +type mockServerStream struct { + grpc.ServerStream + ctx context.Context +} + +func (m *mockServerStream) Context() context.Context { return m.ctx } + +// TestInterceptor_ProfileBypass pins that profile-management methods reach +// the handler regardless of active-profile ownership; the handler enforces +// per-target-profile auth itself. +func TestInterceptor_ProfileBypass(t *testing.T) { + cfg := &mockOwnerConfig{uids: []UID{1000}} + interceptor := NewInterceptor(cfg) + + // Caller UID 2000 is not an owner of the active profile but must be + // allowed through for these methods. + for _, method := range []string{ + "/daemon.DaemonService/AddProfile", + "/daemon.DaemonService/ListProfiles", + "/daemon.DaemonService/RemoveProfile", + "/daemon.DaemonService/SwitchProfile", + } { + err := interceptor.authorize(peerContext(2000), method) + assert.NoError(t, err, "profile method %s should bypass active-owner check", method) + } + + // Without peer creds, even bypass methods are denied. + for _, method := range []string{ + "/daemon.DaemonService/AddProfile", + "/daemon.DaemonService/SwitchProfile", + } { + err := interceptor.authorize(noPeerContext(), method) + require.Error(t, err, "bypass method %s still requires peer creds", method) + assert.Equal(t, codes.PermissionDenied, status.Code(err)) + } +} diff --git a/client/internal/owner/transport_bsd.go b/client/internal/owner/transport_bsd.go new file mode 100644 index 000000000..3ea164bf5 --- /dev/null +++ b/client/internal/owner/transport_bsd.go @@ -0,0 +1,66 @@ +//go:build darwin || freebsd + +package owner + +import ( + "context" + "fmt" + "net" + + "golang.org/x/sys/unix" + "google.golang.org/grpc/credentials" +) + +// NewUnixTransportCredentials returns gRPC TransportCredentials that extract +// peer UID from Unix socket connections via LOCAL_PEERCRED (Xucred). +func NewUnixTransportCredentials() credentials.TransportCredentials { + return &unixCreds{} +} + +type unixCreds struct{} + +func (c *unixCreds) ClientHandshake(_ context.Context, _ string, conn net.Conn) (net.Conn, credentials.AuthInfo, error) { + return conn, UnixAuthInfo{}, nil +} + +// ServerHandshake extracts peer credentials from the Unix connection using LOCAL_PEERCRED. +// Returns an error if credentials cannot be extracted (fail-closed). +func (c *unixCreds) ServerHandshake(conn net.Conn) (net.Conn, credentials.AuthInfo, error) { + uc, ok := conn.(*net.UnixConn) + if !ok { + return nil, nil, fmt.Errorf("expected *net.UnixConn, got %T", conn) + } + + raw, err := uc.SyscallConn() + if err != nil { + return nil, nil, fmt.Errorf("get raw conn for peer credentials: %w", err) + } + + var xucred *unix.Xucred + var credErr error + if err := raw.Control(func(fd uintptr) { + xucred, credErr = unix.GetsockoptXucred(int(fd), unix.SOL_LOCAL, unix.LOCAL_PEERCRED) + }); err != nil { + return nil, nil, fmt.Errorf("control raw conn for peer credentials: %w", err) + } + if credErr != nil { + return nil, nil, fmt.Errorf("get peer credentials: %w", credErr) + } + + return conn, UnixAuthInfo{ + CommonAuthInfo: credentials.CommonAuthInfo{SecurityLevel: credentials.NoSecurity}, + UID: UID(xucred.Uid), + }, nil +} + +func (c *unixCreds) Info() credentials.ProtocolInfo { + return credentials.ProtocolInfo{SecurityProtocol: "unix_peercred"} +} + +func (c *unixCreds) Clone() credentials.TransportCredentials { + return &unixCreds{} +} + +func (c *unixCreds) OverrideServerName(_ string) error { + return nil +} diff --git a/client/internal/owner/transport_generic.go b/client/internal/owner/transport_generic.go new file mode 100644 index 000000000..cb2e424c6 --- /dev/null +++ b/client/internal/owner/transport_generic.go @@ -0,0 +1,11 @@ +//go:build !linux && !darwin && !freebsd + +package owner + +import "google.golang.org/grpc/credentials" + +// NewUnixTransportCredentials returns nil on platforms without Unix socket peer credentials. +// The daemon should use insecure credentials and skip owner enforcement. +func NewUnixTransportCredentials() credentials.TransportCredentials { + return nil +} diff --git a/client/internal/owner/transport_linux.go b/client/internal/owner/transport_linux.go new file mode 100644 index 000000000..79d6bb25e --- /dev/null +++ b/client/internal/owner/transport_linux.go @@ -0,0 +1,66 @@ +package owner + +import ( + "context" + "fmt" + "net" + + "golang.org/x/sys/unix" + "google.golang.org/grpc/credentials" +) + +// NewUnixTransportCredentials returns gRPC TransportCredentials that extract +// peer UID/GID/PID from Unix socket connections via SO_PEERCRED. +func NewUnixTransportCredentials() credentials.TransportCredentials { + return &unixCreds{} +} + +type unixCreds struct{} + +func (c *unixCreds) ClientHandshake(_ context.Context, _ string, conn net.Conn) (net.Conn, credentials.AuthInfo, error) { + return conn, UnixAuthInfo{}, nil +} + +// ServerHandshake extracts peer credentials from the Unix connection. +// Returns an error if credentials cannot be extracted (fail-closed). +func (c *unixCreds) ServerHandshake(conn net.Conn) (net.Conn, credentials.AuthInfo, error) { + uc, ok := conn.(*net.UnixConn) + if !ok { + return nil, nil, fmt.Errorf("expected *net.UnixConn, got %T", conn) + } + + raw, err := uc.SyscallConn() + if err != nil { + return nil, nil, fmt.Errorf("get raw conn for peer credentials: %w", err) + } + + var ucred *unix.Ucred + var credErr error + if err := raw.Control(func(fd uintptr) { + ucred, credErr = unix.GetsockoptUcred(int(fd), unix.SOL_SOCKET, unix.SO_PEERCRED) + }); err != nil { + return nil, nil, fmt.Errorf("control raw conn for peer credentials: %w", err) + } + if credErr != nil { + return nil, nil, fmt.Errorf("get peer credentials: %w", credErr) + } + + return conn, UnixAuthInfo{ + CommonAuthInfo: credentials.CommonAuthInfo{SecurityLevel: credentials.NoSecurity}, + UID: UID(ucred.Uid), + GID: ucred.Gid, + PID: ucred.Pid, + }, nil +} + +func (c *unixCreds) Info() credentials.ProtocolInfo { + return credentials.ProtocolInfo{SecurityProtocol: "unix_peercred"} +} + +func (c *unixCreds) Clone() credentials.TransportCredentials { + return &unixCreds{} +} + +func (c *unixCreds) OverrideServerName(_ string) error { + return nil +} diff --git a/client/internal/owner/transport_test.go b/client/internal/owner/transport_test.go new file mode 100644 index 000000000..196132420 --- /dev/null +++ b/client/internal/owner/transport_test.go @@ -0,0 +1,107 @@ +package owner + +import ( + "net" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/credentials" +) + +func TestUnixTransportCredentials_ServerHandshake(t *testing.T) { + creds := NewUnixTransportCredentials() + if creds == nil { + t.Skip("unix transport credentials not supported on this platform") + } + + sockPath := filepath.Join(t.TempDir(), "test.sock") + + ln, err := net.Listen("unix", sockPath) + require.NoError(t, err) + t.Cleanup(func() { ln.Close() }) + + done := make(chan struct{}) + var serverConn net.Conn + var serverAuth credentials.AuthInfo + var serverErr error + + go func() { + defer close(done) + raw, err := ln.Accept() + if err != nil { + serverErr = err + return + } + serverConn, serverAuth, serverErr = creds.ServerHandshake(raw) + }() + + client, err := net.Dial("unix", sockPath) + require.NoError(t, err) + t.Cleanup(func() { client.Close() }) + + <-done + require.NoError(t, serverErr) + require.NotNil(t, serverConn) + t.Cleanup(func() { serverConn.Close() }) + + authInfo, ok := serverAuth.(UnixAuthInfo) + require.True(t, ok, "expected UnixAuthInfo, got %T", serverAuth) + assert.Equal(t, UID(os.Getuid()), authInfo.UID, "UID should match current user") +} + +func TestUnixTransportCredentials_ServerHandshake_NonUnixConn(t *testing.T) { + creds := NewUnixTransportCredentials() + if creds == nil { + t.Skip("unix transport credentials not supported on this platform") + } + + // Use a TCP connection, which is not *net.UnixConn. + ln, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + t.Cleanup(func() { ln.Close() }) + + done := make(chan struct{}) + var handshakeErr error + + go func() { + defer close(done) + raw, err := ln.Accept() + if err != nil { + handshakeErr = err + return + } + defer raw.Close() + _, _, handshakeErr = creds.ServerHandshake(raw) + }() + + client, err := net.Dial("tcp", ln.Addr().String()) + require.NoError(t, err) + t.Cleanup(func() { client.Close() }) + + <-done + require.Error(t, handshakeErr, "ServerHandshake must fail for non-Unix connections") +} + +func TestUnixTransportCredentials_Info(t *testing.T) { + creds := NewUnixTransportCredentials() + if creds == nil { + t.Skip("unix transport credentials not supported on this platform") + } + + info := creds.Info() + assert.Equal(t, "unix_peercred", info.SecurityProtocol) +} + +func TestUnixTransportCredentials_Clone(t *testing.T) { + creds := NewUnixTransportCredentials() + if creds == nil { + t.Skip("unix transport credentials not supported on this platform") + } + + cloned := creds.Clone() + require.NotNil(t, cloned) + assert.Equal(t, creds.Info(), cloned.Info()) +} diff --git a/client/internal/owner/uid.go b/client/internal/owner/uid.go new file mode 100644 index 000000000..f7e00154d --- /dev/null +++ b/client/internal/owner/uid.go @@ -0,0 +1,5 @@ +package owner + +// UID is a Unix user ID. Defined as a distinct type so it can't be silently +// swapped with GID, PID, or other uint32 values at call sites. +type UID uint32 diff --git a/client/internal/profilemanager/config.go b/client/internal/profilemanager/config.go index cd5bc0680..5311f4767 100644 --- a/client/internal/profilemanager/config.go +++ b/client/internal/profilemanager/config.go @@ -21,6 +21,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/client/iface" + "github.com/netbirdio/netbird/client/internal/owner" "github.com/netbirdio/netbird/client/internal/routemanager/dynamic" "github.com/netbirdio/netbird/client/ssh" mgm "github.com/netbirdio/netbird/shared/management/client" @@ -99,6 +100,10 @@ type ConfigInput struct { LazyConnectionEnabled *bool MTU *uint16 + + // OwnerUIDs sets the UIDs of users allowed to control the daemon. + // When non-nil, replaces the config's OwnerUIDs. + OwnerUIDs []owner.UID } // Config Configuration type @@ -174,6 +179,12 @@ type Config struct { LazyConnectionEnabled bool MTU uint16 + + // OwnerUIDs controls who can perform privileged daemon operations via the gRPC socket. + // nil (absent from JSON): TOFU mode, first privileged caller claims ownership (backward compat for existing installs). + // [] (empty slice): root-only, no non-root owners until explicitly set via "netbird up --owner". + // [uid1, uid2, ...]: these UIDs plus root can perform privileged operations. + OwnerUIDs []owner.UID `json:"OwnerUIDs"` } var ConfigDirOverride string @@ -234,10 +245,18 @@ func fileExists(path string) (bool, error) { // createNewConfig creates a new config generating a new Wireguard key and saving to file func createNewConfig(input ConfigInput) (*Config, error) { + // Seed owner UIDs from environment if set (for MDM deployments), + // otherwise default to root-only (empty slice). + ownerUIDs := owner.OwnerUIDsFromEnv() + if ownerUIDs == nil { + ownerUIDs = []owner.UID{} + } + config := &Config{ // defaults to false only for new (post 0.26) configurations ServerSSHAllowed: util.False(), WgPort: iface.DefaultWgPort, + OwnerUIDs: ownerUIDs, } if _, err := config.apply(input); err != nil { @@ -612,6 +631,14 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) { updated = true } + if input.OwnerUIDs != nil { + if !slices.Equal(config.OwnerUIDs, input.OwnerUIDs) { + log.Infof("updating owner UIDs to %v", input.OwnerUIDs) + config.OwnerUIDs = input.OwnerUIDs + updated = true + } + } + return updated, nil } diff --git a/client/internal/profilemanager/service.go b/client/internal/profilemanager/service.go index ef3eb1114..fdc391576 100644 --- a/client/internal/profilemanager/service.go +++ b/client/internal/profilemanager/service.go @@ -13,6 +13,7 @@ import ( log "github.com/sirupsen/logrus" + "github.com/netbirdio/netbird/client/internal/owner" "github.com/netbirdio/netbird/util" ) @@ -243,7 +244,10 @@ func (s *ServiceManager) DefaultProfilePath() string { return DefaultConfigPath } -func (s *ServiceManager) AddProfile(profileName, username string) error { +// AddProfile creates a new profile with the given name. inheritOwnerUIDs is +// applied to the new profile's OwnerUIDs (pass the active profile's owners so +// the caller stays authorized; pass nil to leave the default empty/env-seeded). +func (s *ServiceManager) AddProfile(profileName, username string, inheritOwnerUIDs []owner.UID) error { configDir, err := s.getConfigDir(username) if err != nil { return fmt.Errorf("failed to get config directory: %w", err) @@ -264,7 +268,7 @@ func (s *ServiceManager) AddProfile(profileName, username string) error { return ErrProfileAlreadyExists } - cfg, err := createNewConfig(ConfigInput{ConfigPath: profPath}) + cfg, err := createNewConfig(ConfigInput{ConfigPath: profPath, OwnerUIDs: inheritOwnerUIDs}) if err != nil { return fmt.Errorf("failed to create new config: %w", err) } diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index 2c054c99a..c75d805eb 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -823,9 +823,13 @@ func (x *WaitSSOLoginResponse) GetEmail() string { } type UpRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ProfileName *string `protobuf:"bytes,1,opt,name=profileName,proto3,oneof" json:"profileName,omitempty"` - Username *string `protobuf:"bytes,2,opt,name=username,proto3,oneof" json:"username,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + ProfileName *string `protobuf:"bytes,1,opt,name=profileName,proto3,oneof" json:"profileName,omitempty"` + Username *string `protobuf:"bytes,2,opt,name=username,proto3,oneof" json:"username,omitempty"` + // When true, the caller claims owner privileges for this profile. + // Requires root or current owner; for new installs (root-only mode), + // the calling UID becomes an owner. + ClaimOwner bool `protobuf:"varint,4,opt,name=claimOwner,proto3" json:"claimOwner,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -874,6 +878,13 @@ func (x *UpRequest) GetUsername() string { return "" } +func (x *UpRequest) GetClaimOwner() bool { + if x != nil { + return x.ClaimOwner + } + return false +} + type UpResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -4445,6 +4456,158 @@ func (*AddProfileResponse) Descriptor() ([]byte, []int) { return file_daemon_proto_rawDescGZIP(), []int{59} } +type AddOwnerRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Uid uint32 `protobuf:"varint,1,opt,name=uid,proto3" json:"uid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddOwnerRequest) Reset() { + *x = AddOwnerRequest{} + mi := &file_daemon_proto_msgTypes[60] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddOwnerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddOwnerRequest) ProtoMessage() {} + +func (x *AddOwnerRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[60] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddOwnerRequest.ProtoReflect.Descriptor instead. +func (*AddOwnerRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{60} +} + +func (x *AddOwnerRequest) GetUid() uint32 { + if x != nil { + return x.Uid + } + return 0 +} + +type AddOwnerResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddOwnerResponse) Reset() { + *x = AddOwnerResponse{} + mi := &file_daemon_proto_msgTypes[61] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddOwnerResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddOwnerResponse) ProtoMessage() {} + +func (x *AddOwnerResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[61] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddOwnerResponse.ProtoReflect.Descriptor instead. +func (*AddOwnerResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{61} +} + +type ResetOwnerRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResetOwnerRequest) Reset() { + *x = ResetOwnerRequest{} + mi := &file_daemon_proto_msgTypes[62] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResetOwnerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResetOwnerRequest) ProtoMessage() {} + +func (x *ResetOwnerRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[62] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResetOwnerRequest.ProtoReflect.Descriptor instead. +func (*ResetOwnerRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{62} +} + +type ResetOwnerResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResetOwnerResponse) Reset() { + *x = ResetOwnerResponse{} + mi := &file_daemon_proto_msgTypes[63] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResetOwnerResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResetOwnerResponse) ProtoMessage() {} + +func (x *ResetOwnerResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[63] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResetOwnerResponse.ProtoReflect.Descriptor instead. +func (*ResetOwnerResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{63} +} + type RemoveProfileRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` @@ -4455,7 +4618,7 @@ type RemoveProfileRequest struct { func (x *RemoveProfileRequest) Reset() { *x = RemoveProfileRequest{} - mi := &file_daemon_proto_msgTypes[60] + mi := &file_daemon_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4467,7 +4630,7 @@ func (x *RemoveProfileRequest) String() string { func (*RemoveProfileRequest) ProtoMessage() {} func (x *RemoveProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[60] + mi := &file_daemon_proto_msgTypes[64] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4480,7 +4643,7 @@ func (x *RemoveProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveProfileRequest.ProtoReflect.Descriptor instead. func (*RemoveProfileRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{60} + return file_daemon_proto_rawDescGZIP(), []int{64} } func (x *RemoveProfileRequest) GetUsername() string { @@ -4505,7 +4668,7 @@ type RemoveProfileResponse struct { func (x *RemoveProfileResponse) Reset() { *x = RemoveProfileResponse{} - mi := &file_daemon_proto_msgTypes[61] + mi := &file_daemon_proto_msgTypes[65] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4517,7 +4680,7 @@ func (x *RemoveProfileResponse) String() string { func (*RemoveProfileResponse) ProtoMessage() {} func (x *RemoveProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[61] + mi := &file_daemon_proto_msgTypes[65] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4530,7 +4693,7 @@ func (x *RemoveProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveProfileResponse.ProtoReflect.Descriptor instead. func (*RemoveProfileResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{61} + return file_daemon_proto_rawDescGZIP(), []int{65} } type ListProfilesRequest struct { @@ -4542,7 +4705,7 @@ type ListProfilesRequest struct { func (x *ListProfilesRequest) Reset() { *x = ListProfilesRequest{} - mi := &file_daemon_proto_msgTypes[62] + mi := &file_daemon_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4554,7 +4717,7 @@ func (x *ListProfilesRequest) String() string { func (*ListProfilesRequest) ProtoMessage() {} func (x *ListProfilesRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[62] + mi := &file_daemon_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4567,7 +4730,7 @@ func (x *ListProfilesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListProfilesRequest.ProtoReflect.Descriptor instead. func (*ListProfilesRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{62} + return file_daemon_proto_rawDescGZIP(), []int{66} } func (x *ListProfilesRequest) GetUsername() string { @@ -4586,7 +4749,7 @@ type ListProfilesResponse struct { func (x *ListProfilesResponse) Reset() { *x = ListProfilesResponse{} - mi := &file_daemon_proto_msgTypes[63] + mi := &file_daemon_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4598,7 +4761,7 @@ func (x *ListProfilesResponse) String() string { func (*ListProfilesResponse) ProtoMessage() {} func (x *ListProfilesResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[63] + mi := &file_daemon_proto_msgTypes[67] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4611,7 +4774,7 @@ func (x *ListProfilesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListProfilesResponse.ProtoReflect.Descriptor instead. func (*ListProfilesResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{63} + return file_daemon_proto_rawDescGZIP(), []int{67} } func (x *ListProfilesResponse) GetProfiles() []*Profile { @@ -4631,7 +4794,7 @@ type Profile struct { func (x *Profile) Reset() { *x = Profile{} - mi := &file_daemon_proto_msgTypes[64] + mi := &file_daemon_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4643,7 +4806,7 @@ func (x *Profile) String() string { func (*Profile) ProtoMessage() {} func (x *Profile) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[64] + mi := &file_daemon_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4656,7 +4819,7 @@ func (x *Profile) ProtoReflect() protoreflect.Message { // Deprecated: Use Profile.ProtoReflect.Descriptor instead. func (*Profile) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{64} + return file_daemon_proto_rawDescGZIP(), []int{68} } func (x *Profile) GetName() string { @@ -4681,7 +4844,7 @@ type GetActiveProfileRequest struct { func (x *GetActiveProfileRequest) Reset() { *x = GetActiveProfileRequest{} - mi := &file_daemon_proto_msgTypes[65] + mi := &file_daemon_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4693,7 +4856,7 @@ func (x *GetActiveProfileRequest) String() string { func (*GetActiveProfileRequest) ProtoMessage() {} func (x *GetActiveProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[65] + mi := &file_daemon_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4706,7 +4869,7 @@ func (x *GetActiveProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetActiveProfileRequest.ProtoReflect.Descriptor instead. func (*GetActiveProfileRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{65} + return file_daemon_proto_rawDescGZIP(), []int{69} } type GetActiveProfileResponse struct { @@ -4719,7 +4882,7 @@ type GetActiveProfileResponse struct { func (x *GetActiveProfileResponse) Reset() { *x = GetActiveProfileResponse{} - mi := &file_daemon_proto_msgTypes[66] + mi := &file_daemon_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4731,7 +4894,7 @@ func (x *GetActiveProfileResponse) String() string { func (*GetActiveProfileResponse) ProtoMessage() {} func (x *GetActiveProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[66] + mi := &file_daemon_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4744,7 +4907,7 @@ func (x *GetActiveProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetActiveProfileResponse.ProtoReflect.Descriptor instead. func (*GetActiveProfileResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{66} + return file_daemon_proto_rawDescGZIP(), []int{70} } func (x *GetActiveProfileResponse) GetProfileName() string { @@ -4771,7 +4934,7 @@ type LogoutRequest struct { func (x *LogoutRequest) Reset() { *x = LogoutRequest{} - mi := &file_daemon_proto_msgTypes[67] + mi := &file_daemon_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4783,7 +4946,7 @@ func (x *LogoutRequest) String() string { func (*LogoutRequest) ProtoMessage() {} func (x *LogoutRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[67] + mi := &file_daemon_proto_msgTypes[71] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4796,7 +4959,7 @@ func (x *LogoutRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use LogoutRequest.ProtoReflect.Descriptor instead. func (*LogoutRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{67} + return file_daemon_proto_rawDescGZIP(), []int{71} } func (x *LogoutRequest) GetProfileName() string { @@ -4821,7 +4984,7 @@ type LogoutResponse struct { func (x *LogoutResponse) Reset() { *x = LogoutResponse{} - mi := &file_daemon_proto_msgTypes[68] + mi := &file_daemon_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4833,7 +4996,7 @@ func (x *LogoutResponse) String() string { func (*LogoutResponse) ProtoMessage() {} func (x *LogoutResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[68] + mi := &file_daemon_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4846,7 +5009,7 @@ func (x *LogoutResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use LogoutResponse.ProtoReflect.Descriptor instead. func (*LogoutResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{68} + return file_daemon_proto_rawDescGZIP(), []int{72} } type GetFeaturesRequest struct { @@ -4857,7 +5020,7 @@ type GetFeaturesRequest struct { func (x *GetFeaturesRequest) Reset() { *x = GetFeaturesRequest{} - mi := &file_daemon_proto_msgTypes[69] + mi := &file_daemon_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4869,7 +5032,7 @@ func (x *GetFeaturesRequest) String() string { func (*GetFeaturesRequest) ProtoMessage() {} func (x *GetFeaturesRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[69] + mi := &file_daemon_proto_msgTypes[73] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4882,7 +5045,7 @@ func (x *GetFeaturesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetFeaturesRequest.ProtoReflect.Descriptor instead. func (*GetFeaturesRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{69} + return file_daemon_proto_rawDescGZIP(), []int{73} } type GetFeaturesResponse struct { @@ -4896,7 +5059,7 @@ type GetFeaturesResponse struct { func (x *GetFeaturesResponse) Reset() { *x = GetFeaturesResponse{} - mi := &file_daemon_proto_msgTypes[70] + mi := &file_daemon_proto_msgTypes[74] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4908,7 +5071,7 @@ func (x *GetFeaturesResponse) String() string { func (*GetFeaturesResponse) ProtoMessage() {} func (x *GetFeaturesResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[70] + mi := &file_daemon_proto_msgTypes[74] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4921,7 +5084,7 @@ func (x *GetFeaturesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetFeaturesResponse.ProtoReflect.Descriptor instead. func (*GetFeaturesResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{70} + return file_daemon_proto_rawDescGZIP(), []int{74} } func (x *GetFeaturesResponse) GetDisableProfiles() bool { @@ -4953,7 +5116,7 @@ type TriggerUpdateRequest struct { func (x *TriggerUpdateRequest) Reset() { *x = TriggerUpdateRequest{} - mi := &file_daemon_proto_msgTypes[71] + mi := &file_daemon_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4965,7 +5128,7 @@ func (x *TriggerUpdateRequest) String() string { func (*TriggerUpdateRequest) ProtoMessage() {} func (x *TriggerUpdateRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[71] + mi := &file_daemon_proto_msgTypes[75] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4978,7 +5141,7 @@ func (x *TriggerUpdateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TriggerUpdateRequest.ProtoReflect.Descriptor instead. func (*TriggerUpdateRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{71} + return file_daemon_proto_rawDescGZIP(), []int{75} } type TriggerUpdateResponse struct { @@ -4991,7 +5154,7 @@ type TriggerUpdateResponse struct { func (x *TriggerUpdateResponse) Reset() { *x = TriggerUpdateResponse{} - mi := &file_daemon_proto_msgTypes[72] + mi := &file_daemon_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5003,7 +5166,7 @@ func (x *TriggerUpdateResponse) String() string { func (*TriggerUpdateResponse) ProtoMessage() {} func (x *TriggerUpdateResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[72] + mi := &file_daemon_proto_msgTypes[76] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5016,7 +5179,7 @@ func (x *TriggerUpdateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use TriggerUpdateResponse.ProtoReflect.Descriptor instead. func (*TriggerUpdateResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{72} + return file_daemon_proto_rawDescGZIP(), []int{76} } func (x *TriggerUpdateResponse) GetSuccess() bool { @@ -5044,7 +5207,7 @@ type GetPeerSSHHostKeyRequest struct { func (x *GetPeerSSHHostKeyRequest) Reset() { *x = GetPeerSSHHostKeyRequest{} - mi := &file_daemon_proto_msgTypes[73] + mi := &file_daemon_proto_msgTypes[77] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5056,7 +5219,7 @@ func (x *GetPeerSSHHostKeyRequest) String() string { func (*GetPeerSSHHostKeyRequest) ProtoMessage() {} func (x *GetPeerSSHHostKeyRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[73] + mi := &file_daemon_proto_msgTypes[77] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5069,7 +5232,7 @@ func (x *GetPeerSSHHostKeyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetPeerSSHHostKeyRequest.ProtoReflect.Descriptor instead. func (*GetPeerSSHHostKeyRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{73} + return file_daemon_proto_rawDescGZIP(), []int{77} } func (x *GetPeerSSHHostKeyRequest) GetPeerAddress() string { @@ -5096,7 +5259,7 @@ type GetPeerSSHHostKeyResponse struct { func (x *GetPeerSSHHostKeyResponse) Reset() { *x = GetPeerSSHHostKeyResponse{} - mi := &file_daemon_proto_msgTypes[74] + mi := &file_daemon_proto_msgTypes[78] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5108,7 +5271,7 @@ func (x *GetPeerSSHHostKeyResponse) String() string { func (*GetPeerSSHHostKeyResponse) ProtoMessage() {} func (x *GetPeerSSHHostKeyResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[74] + mi := &file_daemon_proto_msgTypes[78] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5121,7 +5284,7 @@ func (x *GetPeerSSHHostKeyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetPeerSSHHostKeyResponse.ProtoReflect.Descriptor instead. func (*GetPeerSSHHostKeyResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{74} + return file_daemon_proto_rawDescGZIP(), []int{78} } func (x *GetPeerSSHHostKeyResponse) GetSshHostKey() []byte { @@ -5163,7 +5326,7 @@ type RequestJWTAuthRequest struct { func (x *RequestJWTAuthRequest) Reset() { *x = RequestJWTAuthRequest{} - mi := &file_daemon_proto_msgTypes[75] + mi := &file_daemon_proto_msgTypes[79] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5175,7 +5338,7 @@ func (x *RequestJWTAuthRequest) String() string { func (*RequestJWTAuthRequest) ProtoMessage() {} func (x *RequestJWTAuthRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[75] + mi := &file_daemon_proto_msgTypes[79] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5188,7 +5351,7 @@ func (x *RequestJWTAuthRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RequestJWTAuthRequest.ProtoReflect.Descriptor instead. func (*RequestJWTAuthRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{75} + return file_daemon_proto_rawDescGZIP(), []int{79} } func (x *RequestJWTAuthRequest) GetHint() string { @@ -5221,7 +5384,7 @@ type RequestJWTAuthResponse struct { func (x *RequestJWTAuthResponse) Reset() { *x = RequestJWTAuthResponse{} - mi := &file_daemon_proto_msgTypes[76] + mi := &file_daemon_proto_msgTypes[80] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5233,7 +5396,7 @@ func (x *RequestJWTAuthResponse) String() string { func (*RequestJWTAuthResponse) ProtoMessage() {} func (x *RequestJWTAuthResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[76] + mi := &file_daemon_proto_msgTypes[80] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5246,7 +5409,7 @@ func (x *RequestJWTAuthResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RequestJWTAuthResponse.ProtoReflect.Descriptor instead. func (*RequestJWTAuthResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{76} + return file_daemon_proto_rawDescGZIP(), []int{80} } func (x *RequestJWTAuthResponse) GetVerificationURI() string { @@ -5311,7 +5474,7 @@ type WaitJWTTokenRequest struct { func (x *WaitJWTTokenRequest) Reset() { *x = WaitJWTTokenRequest{} - mi := &file_daemon_proto_msgTypes[77] + mi := &file_daemon_proto_msgTypes[81] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5323,7 +5486,7 @@ func (x *WaitJWTTokenRequest) String() string { func (*WaitJWTTokenRequest) ProtoMessage() {} func (x *WaitJWTTokenRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[77] + mi := &file_daemon_proto_msgTypes[81] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5336,7 +5499,7 @@ func (x *WaitJWTTokenRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use WaitJWTTokenRequest.ProtoReflect.Descriptor instead. func (*WaitJWTTokenRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{77} + return file_daemon_proto_rawDescGZIP(), []int{81} } func (x *WaitJWTTokenRequest) GetDeviceCode() string { @@ -5368,7 +5531,7 @@ type WaitJWTTokenResponse struct { func (x *WaitJWTTokenResponse) Reset() { *x = WaitJWTTokenResponse{} - mi := &file_daemon_proto_msgTypes[78] + mi := &file_daemon_proto_msgTypes[82] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5380,7 +5543,7 @@ func (x *WaitJWTTokenResponse) String() string { func (*WaitJWTTokenResponse) ProtoMessage() {} func (x *WaitJWTTokenResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[78] + mi := &file_daemon_proto_msgTypes[82] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5393,7 +5556,7 @@ func (x *WaitJWTTokenResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use WaitJWTTokenResponse.ProtoReflect.Descriptor instead. func (*WaitJWTTokenResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{78} + return file_daemon_proto_rawDescGZIP(), []int{82} } func (x *WaitJWTTokenResponse) GetToken() string { @@ -5426,7 +5589,7 @@ type StartCPUProfileRequest struct { func (x *StartCPUProfileRequest) Reset() { *x = StartCPUProfileRequest{} - mi := &file_daemon_proto_msgTypes[79] + mi := &file_daemon_proto_msgTypes[83] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5438,7 +5601,7 @@ func (x *StartCPUProfileRequest) String() string { func (*StartCPUProfileRequest) ProtoMessage() {} func (x *StartCPUProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[79] + mi := &file_daemon_proto_msgTypes[83] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5451,7 +5614,7 @@ func (x *StartCPUProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StartCPUProfileRequest.ProtoReflect.Descriptor instead. func (*StartCPUProfileRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{79} + return file_daemon_proto_rawDescGZIP(), []int{83} } // StartCPUProfileResponse confirms CPU profiling has started @@ -5463,7 +5626,7 @@ type StartCPUProfileResponse struct { func (x *StartCPUProfileResponse) Reset() { *x = StartCPUProfileResponse{} - mi := &file_daemon_proto_msgTypes[80] + mi := &file_daemon_proto_msgTypes[84] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5475,7 +5638,7 @@ func (x *StartCPUProfileResponse) String() string { func (*StartCPUProfileResponse) ProtoMessage() {} func (x *StartCPUProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[80] + mi := &file_daemon_proto_msgTypes[84] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5488,7 +5651,7 @@ func (x *StartCPUProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StartCPUProfileResponse.ProtoReflect.Descriptor instead. func (*StartCPUProfileResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{80} + return file_daemon_proto_rawDescGZIP(), []int{84} } // StopCPUProfileRequest for stopping CPU profiling @@ -5500,7 +5663,7 @@ type StopCPUProfileRequest struct { func (x *StopCPUProfileRequest) Reset() { *x = StopCPUProfileRequest{} - mi := &file_daemon_proto_msgTypes[81] + mi := &file_daemon_proto_msgTypes[85] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5512,7 +5675,7 @@ func (x *StopCPUProfileRequest) String() string { func (*StopCPUProfileRequest) ProtoMessage() {} func (x *StopCPUProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[81] + mi := &file_daemon_proto_msgTypes[85] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5525,7 +5688,7 @@ func (x *StopCPUProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StopCPUProfileRequest.ProtoReflect.Descriptor instead. func (*StopCPUProfileRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{81} + return file_daemon_proto_rawDescGZIP(), []int{85} } // StopCPUProfileResponse confirms CPU profiling has stopped @@ -5537,7 +5700,7 @@ type StopCPUProfileResponse struct { func (x *StopCPUProfileResponse) Reset() { *x = StopCPUProfileResponse{} - mi := &file_daemon_proto_msgTypes[82] + mi := &file_daemon_proto_msgTypes[86] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5549,7 +5712,7 @@ func (x *StopCPUProfileResponse) String() string { func (*StopCPUProfileResponse) ProtoMessage() {} func (x *StopCPUProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[82] + mi := &file_daemon_proto_msgTypes[86] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5562,7 +5725,7 @@ func (x *StopCPUProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StopCPUProfileResponse.ProtoReflect.Descriptor instead. func (*StopCPUProfileResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{82} + return file_daemon_proto_rawDescGZIP(), []int{86} } type InstallerResultRequest struct { @@ -5573,7 +5736,7 @@ type InstallerResultRequest struct { func (x *InstallerResultRequest) Reset() { *x = InstallerResultRequest{} - mi := &file_daemon_proto_msgTypes[83] + mi := &file_daemon_proto_msgTypes[87] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5585,7 +5748,7 @@ func (x *InstallerResultRequest) String() string { func (*InstallerResultRequest) ProtoMessage() {} func (x *InstallerResultRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[83] + mi := &file_daemon_proto_msgTypes[87] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5598,7 +5761,7 @@ func (x *InstallerResultRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use InstallerResultRequest.ProtoReflect.Descriptor instead. func (*InstallerResultRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{83} + return file_daemon_proto_rawDescGZIP(), []int{87} } type InstallerResultResponse struct { @@ -5611,7 +5774,7 @@ type InstallerResultResponse struct { func (x *InstallerResultResponse) Reset() { *x = InstallerResultResponse{} - mi := &file_daemon_proto_msgTypes[84] + mi := &file_daemon_proto_msgTypes[88] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5623,7 +5786,7 @@ func (x *InstallerResultResponse) String() string { func (*InstallerResultResponse) ProtoMessage() {} func (x *InstallerResultResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[84] + mi := &file_daemon_proto_msgTypes[88] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5636,7 +5799,7 @@ func (x *InstallerResultResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use InstallerResultResponse.ProtoReflect.Descriptor instead. func (*InstallerResultResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{84} + return file_daemon_proto_rawDescGZIP(), []int{88} } func (x *InstallerResultResponse) GetSuccess() bool { @@ -5669,7 +5832,7 @@ type ExposeServiceRequest struct { func (x *ExposeServiceRequest) Reset() { *x = ExposeServiceRequest{} - mi := &file_daemon_proto_msgTypes[85] + mi := &file_daemon_proto_msgTypes[89] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5681,7 +5844,7 @@ func (x *ExposeServiceRequest) String() string { func (*ExposeServiceRequest) ProtoMessage() {} func (x *ExposeServiceRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[85] + mi := &file_daemon_proto_msgTypes[89] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5694,7 +5857,7 @@ func (x *ExposeServiceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ExposeServiceRequest.ProtoReflect.Descriptor instead. func (*ExposeServiceRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{85} + return file_daemon_proto_rawDescGZIP(), []int{89} } func (x *ExposeServiceRequest) GetPort() uint32 { @@ -5765,7 +5928,7 @@ type ExposeServiceEvent struct { func (x *ExposeServiceEvent) Reset() { *x = ExposeServiceEvent{} - mi := &file_daemon_proto_msgTypes[86] + mi := &file_daemon_proto_msgTypes[90] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5777,7 +5940,7 @@ func (x *ExposeServiceEvent) String() string { func (*ExposeServiceEvent) ProtoMessage() {} func (x *ExposeServiceEvent) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[86] + mi := &file_daemon_proto_msgTypes[90] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5790,7 +5953,7 @@ func (x *ExposeServiceEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use ExposeServiceEvent.ProtoReflect.Descriptor instead. func (*ExposeServiceEvent) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{86} + return file_daemon_proto_rawDescGZIP(), []int{90} } func (x *ExposeServiceEvent) GetEvent() isExposeServiceEvent_Event { @@ -5831,7 +5994,7 @@ type ExposeServiceReady struct { func (x *ExposeServiceReady) Reset() { *x = ExposeServiceReady{} - mi := &file_daemon_proto_msgTypes[87] + mi := &file_daemon_proto_msgTypes[91] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5843,7 +6006,7 @@ func (x *ExposeServiceReady) String() string { func (*ExposeServiceReady) ProtoMessage() {} func (x *ExposeServiceReady) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[87] + mi := &file_daemon_proto_msgTypes[91] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5856,7 +6019,7 @@ func (x *ExposeServiceReady) ProtoReflect() protoreflect.Message { // Deprecated: Use ExposeServiceReady.ProtoReflect.Descriptor instead. func (*ExposeServiceReady) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{87} + return file_daemon_proto_rawDescGZIP(), []int{91} } func (x *ExposeServiceReady) GetServiceName() string { @@ -5901,7 +6064,7 @@ type StartCaptureRequest struct { func (x *StartCaptureRequest) Reset() { *x = StartCaptureRequest{} - mi := &file_daemon_proto_msgTypes[88] + mi := &file_daemon_proto_msgTypes[92] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5913,7 +6076,7 @@ func (x *StartCaptureRequest) String() string { func (*StartCaptureRequest) ProtoMessage() {} func (x *StartCaptureRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[88] + mi := &file_daemon_proto_msgTypes[92] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5926,7 +6089,7 @@ func (x *StartCaptureRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StartCaptureRequest.ProtoReflect.Descriptor instead. func (*StartCaptureRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{88} + return file_daemon_proto_rawDescGZIP(), []int{92} } func (x *StartCaptureRequest) GetTextOutput() bool { @@ -5980,7 +6143,7 @@ type CapturePacket struct { func (x *CapturePacket) Reset() { *x = CapturePacket{} - mi := &file_daemon_proto_msgTypes[89] + mi := &file_daemon_proto_msgTypes[93] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5992,7 +6155,7 @@ func (x *CapturePacket) String() string { func (*CapturePacket) ProtoMessage() {} func (x *CapturePacket) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[89] + mi := &file_daemon_proto_msgTypes[93] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6005,7 +6168,7 @@ func (x *CapturePacket) ProtoReflect() protoreflect.Message { // Deprecated: Use CapturePacket.ProtoReflect.Descriptor instead. func (*CapturePacket) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{89} + return file_daemon_proto_rawDescGZIP(), []int{93} } func (x *CapturePacket) GetData() []byte { @@ -6026,7 +6189,7 @@ type StartBundleCaptureRequest struct { func (x *StartBundleCaptureRequest) Reset() { *x = StartBundleCaptureRequest{} - mi := &file_daemon_proto_msgTypes[90] + mi := &file_daemon_proto_msgTypes[94] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6038,7 +6201,7 @@ func (x *StartBundleCaptureRequest) String() string { func (*StartBundleCaptureRequest) ProtoMessage() {} func (x *StartBundleCaptureRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[90] + mi := &file_daemon_proto_msgTypes[94] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6051,7 +6214,7 @@ func (x *StartBundleCaptureRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StartBundleCaptureRequest.ProtoReflect.Descriptor instead. func (*StartBundleCaptureRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{90} + return file_daemon_proto_rawDescGZIP(), []int{94} } func (x *StartBundleCaptureRequest) GetTimeout() *durationpb.Duration { @@ -6069,7 +6232,7 @@ type StartBundleCaptureResponse struct { func (x *StartBundleCaptureResponse) Reset() { *x = StartBundleCaptureResponse{} - mi := &file_daemon_proto_msgTypes[91] + mi := &file_daemon_proto_msgTypes[95] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6081,7 +6244,7 @@ func (x *StartBundleCaptureResponse) String() string { func (*StartBundleCaptureResponse) ProtoMessage() {} func (x *StartBundleCaptureResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[91] + mi := &file_daemon_proto_msgTypes[95] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6094,7 +6257,7 @@ func (x *StartBundleCaptureResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StartBundleCaptureResponse.ProtoReflect.Descriptor instead. func (*StartBundleCaptureResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{91} + return file_daemon_proto_rawDescGZIP(), []int{95} } type StopBundleCaptureRequest struct { @@ -6105,7 +6268,7 @@ type StopBundleCaptureRequest struct { func (x *StopBundleCaptureRequest) Reset() { *x = StopBundleCaptureRequest{} - mi := &file_daemon_proto_msgTypes[92] + mi := &file_daemon_proto_msgTypes[96] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6117,7 +6280,7 @@ func (x *StopBundleCaptureRequest) String() string { func (*StopBundleCaptureRequest) ProtoMessage() {} func (x *StopBundleCaptureRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[92] + mi := &file_daemon_proto_msgTypes[96] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6130,7 +6293,7 @@ func (x *StopBundleCaptureRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StopBundleCaptureRequest.ProtoReflect.Descriptor instead. func (*StopBundleCaptureRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{92} + return file_daemon_proto_rawDescGZIP(), []int{96} } type StopBundleCaptureResponse struct { @@ -6141,7 +6304,7 @@ type StopBundleCaptureResponse struct { func (x *StopBundleCaptureResponse) Reset() { *x = StopBundleCaptureResponse{} - mi := &file_daemon_proto_msgTypes[93] + mi := &file_daemon_proto_msgTypes[97] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6153,7 +6316,7 @@ func (x *StopBundleCaptureResponse) String() string { func (*StopBundleCaptureResponse) ProtoMessage() {} func (x *StopBundleCaptureResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[93] + mi := &file_daemon_proto_msgTypes[97] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6166,7 +6329,7 @@ func (x *StopBundleCaptureResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StopBundleCaptureResponse.ProtoReflect.Descriptor instead. func (*StopBundleCaptureResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{93} + return file_daemon_proto_rawDescGZIP(), []int{97} } type PortInfo_Range struct { @@ -6179,7 +6342,7 @@ type PortInfo_Range struct { func (x *PortInfo_Range) Reset() { *x = PortInfo_Range{} - mi := &file_daemon_proto_msgTypes[95] + mi := &file_daemon_proto_msgTypes[99] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6191,7 +6354,7 @@ func (x *PortInfo_Range) String() string { func (*PortInfo_Range) ProtoMessage() {} func (x *PortInfo_Range) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[95] + mi := &file_daemon_proto_msgTypes[99] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6309,10 +6472,13 @@ const file_daemon_proto_rawDesc = "" + "\buserCode\x18\x01 \x01(\tR\buserCode\x12\x1a\n" + "\bhostname\x18\x02 \x01(\tR\bhostname\",\n" + "\x14WaitSSOLoginResponse\x12\x14\n" + - "\x05email\x18\x01 \x01(\tR\x05email\"v\n" + + "\x05email\x18\x01 \x01(\tR\x05email\"\x96\x01\n" + "\tUpRequest\x12%\n" + "\vprofileName\x18\x01 \x01(\tH\x00R\vprofileName\x88\x01\x01\x12\x1f\n" + - "\busername\x18\x02 \x01(\tH\x01R\busername\x88\x01\x01B\x0e\n" + + "\busername\x18\x02 \x01(\tH\x01R\busername\x88\x01\x01\x12\x1e\n" + + "\n" + + "claimOwner\x18\x04 \x01(\bR\n" + + "claimOwnerB\x0e\n" + "\f_profileNameB\v\n" + "\t_usernameJ\x04\b\x03\x10\x04\"\f\n" + "\n" + @@ -6649,7 +6815,12 @@ const file_daemon_proto_rawDesc = "" + "\x11AddProfileRequest\x12\x1a\n" + "\busername\x18\x01 \x01(\tR\busername\x12 \n" + "\vprofileName\x18\x02 \x01(\tR\vprofileName\"\x14\n" + - "\x12AddProfileResponse\"T\n" + + "\x12AddProfileResponse\"#\n" + + "\x0fAddOwnerRequest\x12\x10\n" + + "\x03uid\x18\x01 \x01(\rR\x03uid\"\x12\n" + + "\x10AddOwnerResponse\"\x13\n" + + "\x11ResetOwnerRequest\"\x14\n" + + "\x12ResetOwnerResponse\"T\n" + "\x14RemoveProfileRequest\x12\x1a\n" + "\busername\x18\x01 \x01(\tR\busername\x12 \n" + "\vprofileName\x18\x02 \x01(\tR\vprofileName\"\x17\n" + @@ -6773,7 +6944,7 @@ const file_daemon_proto_rawDesc = "" + "\n" + "EXPOSE_UDP\x10\x03\x12\x0e\n" + "\n" + - "EXPOSE_TLS\x10\x042\xaf\x17\n" + + "EXPOSE_TLS\x10\x042\xb7\x18\n" + "\rDaemonService\x126\n" + "\x05Login\x12\x14.daemon.LoginRequest\x1a\x15.daemon.LoginResponse\"\x00\x12K\n" + "\fWaitSSOLogin\x12\x1b.daemon.WaitSSOLoginRequest\x1a\x1c.daemon.WaitSSOLoginResponse\"\x00\x12-\n" + @@ -6806,7 +6977,10 @@ const file_daemon_proto_rawDesc = "" + "AddProfile\x12\x19.daemon.AddProfileRequest\x1a\x1a.daemon.AddProfileResponse\"\x00\x12N\n" + "\rRemoveProfile\x12\x1c.daemon.RemoveProfileRequest\x1a\x1d.daemon.RemoveProfileResponse\"\x00\x12K\n" + "\fListProfiles\x12\x1b.daemon.ListProfilesRequest\x1a\x1c.daemon.ListProfilesResponse\"\x00\x12W\n" + - "\x10GetActiveProfile\x12\x1f.daemon.GetActiveProfileRequest\x1a .daemon.GetActiveProfileResponse\"\x00\x129\n" + + "\x10GetActiveProfile\x12\x1f.daemon.GetActiveProfileRequest\x1a .daemon.GetActiveProfileResponse\"\x00\x12?\n" + + "\bAddOwner\x12\x17.daemon.AddOwnerRequest\x1a\x18.daemon.AddOwnerResponse\"\x00\x12E\n" + + "\n" + + "ResetOwner\x12\x19.daemon.ResetOwnerRequest\x1a\x1a.daemon.ResetOwnerResponse\"\x00\x129\n" + "\x06Logout\x12\x15.daemon.LogoutRequest\x1a\x16.daemon.LogoutResponse\"\x00\x12H\n" + "\vGetFeatures\x12\x1a.daemon.GetFeaturesRequest\x1a\x1b.daemon.GetFeaturesResponse\"\x00\x12N\n" + "\rTriggerUpdate\x12\x1c.daemon.TriggerUpdateRequest\x1a\x1d.daemon.TriggerUpdateResponse\"\x00\x12Z\n" + @@ -6831,7 +7005,7 @@ func file_daemon_proto_rawDescGZIP() []byte { } var file_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 4) -var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 97) +var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 101) var file_daemon_proto_goTypes = []any{ (LogLevel)(0), // 0: daemon.LogLevel (ExposeProtocol)(0), // 1: daemon.ExposeProtocol @@ -6897,52 +7071,56 @@ var file_daemon_proto_goTypes = []any{ (*SetConfigResponse)(nil), // 61: daemon.SetConfigResponse (*AddProfileRequest)(nil), // 62: daemon.AddProfileRequest (*AddProfileResponse)(nil), // 63: daemon.AddProfileResponse - (*RemoveProfileRequest)(nil), // 64: daemon.RemoveProfileRequest - (*RemoveProfileResponse)(nil), // 65: daemon.RemoveProfileResponse - (*ListProfilesRequest)(nil), // 66: daemon.ListProfilesRequest - (*ListProfilesResponse)(nil), // 67: daemon.ListProfilesResponse - (*Profile)(nil), // 68: daemon.Profile - (*GetActiveProfileRequest)(nil), // 69: daemon.GetActiveProfileRequest - (*GetActiveProfileResponse)(nil), // 70: daemon.GetActiveProfileResponse - (*LogoutRequest)(nil), // 71: daemon.LogoutRequest - (*LogoutResponse)(nil), // 72: daemon.LogoutResponse - (*GetFeaturesRequest)(nil), // 73: daemon.GetFeaturesRequest - (*GetFeaturesResponse)(nil), // 74: daemon.GetFeaturesResponse - (*TriggerUpdateRequest)(nil), // 75: daemon.TriggerUpdateRequest - (*TriggerUpdateResponse)(nil), // 76: daemon.TriggerUpdateResponse - (*GetPeerSSHHostKeyRequest)(nil), // 77: daemon.GetPeerSSHHostKeyRequest - (*GetPeerSSHHostKeyResponse)(nil), // 78: daemon.GetPeerSSHHostKeyResponse - (*RequestJWTAuthRequest)(nil), // 79: daemon.RequestJWTAuthRequest - (*RequestJWTAuthResponse)(nil), // 80: daemon.RequestJWTAuthResponse - (*WaitJWTTokenRequest)(nil), // 81: daemon.WaitJWTTokenRequest - (*WaitJWTTokenResponse)(nil), // 82: daemon.WaitJWTTokenResponse - (*StartCPUProfileRequest)(nil), // 83: daemon.StartCPUProfileRequest - (*StartCPUProfileResponse)(nil), // 84: daemon.StartCPUProfileResponse - (*StopCPUProfileRequest)(nil), // 85: daemon.StopCPUProfileRequest - (*StopCPUProfileResponse)(nil), // 86: daemon.StopCPUProfileResponse - (*InstallerResultRequest)(nil), // 87: daemon.InstallerResultRequest - (*InstallerResultResponse)(nil), // 88: daemon.InstallerResultResponse - (*ExposeServiceRequest)(nil), // 89: daemon.ExposeServiceRequest - (*ExposeServiceEvent)(nil), // 90: daemon.ExposeServiceEvent - (*ExposeServiceReady)(nil), // 91: daemon.ExposeServiceReady - (*StartCaptureRequest)(nil), // 92: daemon.StartCaptureRequest - (*CapturePacket)(nil), // 93: daemon.CapturePacket - (*StartBundleCaptureRequest)(nil), // 94: daemon.StartBundleCaptureRequest - (*StartBundleCaptureResponse)(nil), // 95: daemon.StartBundleCaptureResponse - (*StopBundleCaptureRequest)(nil), // 96: daemon.StopBundleCaptureRequest - (*StopBundleCaptureResponse)(nil), // 97: daemon.StopBundleCaptureResponse - nil, // 98: daemon.Network.ResolvedIPsEntry - (*PortInfo_Range)(nil), // 99: daemon.PortInfo.Range - nil, // 100: daemon.SystemEvent.MetadataEntry - (*durationpb.Duration)(nil), // 101: google.protobuf.Duration - (*timestamppb.Timestamp)(nil), // 102: google.protobuf.Timestamp + (*AddOwnerRequest)(nil), // 64: daemon.AddOwnerRequest + (*AddOwnerResponse)(nil), // 65: daemon.AddOwnerResponse + (*ResetOwnerRequest)(nil), // 66: daemon.ResetOwnerRequest + (*ResetOwnerResponse)(nil), // 67: daemon.ResetOwnerResponse + (*RemoveProfileRequest)(nil), // 68: daemon.RemoveProfileRequest + (*RemoveProfileResponse)(nil), // 69: daemon.RemoveProfileResponse + (*ListProfilesRequest)(nil), // 70: daemon.ListProfilesRequest + (*ListProfilesResponse)(nil), // 71: daemon.ListProfilesResponse + (*Profile)(nil), // 72: daemon.Profile + (*GetActiveProfileRequest)(nil), // 73: daemon.GetActiveProfileRequest + (*GetActiveProfileResponse)(nil), // 74: daemon.GetActiveProfileResponse + (*LogoutRequest)(nil), // 75: daemon.LogoutRequest + (*LogoutResponse)(nil), // 76: daemon.LogoutResponse + (*GetFeaturesRequest)(nil), // 77: daemon.GetFeaturesRequest + (*GetFeaturesResponse)(nil), // 78: daemon.GetFeaturesResponse + (*TriggerUpdateRequest)(nil), // 79: daemon.TriggerUpdateRequest + (*TriggerUpdateResponse)(nil), // 80: daemon.TriggerUpdateResponse + (*GetPeerSSHHostKeyRequest)(nil), // 81: daemon.GetPeerSSHHostKeyRequest + (*GetPeerSSHHostKeyResponse)(nil), // 82: daemon.GetPeerSSHHostKeyResponse + (*RequestJWTAuthRequest)(nil), // 83: daemon.RequestJWTAuthRequest + (*RequestJWTAuthResponse)(nil), // 84: daemon.RequestJWTAuthResponse + (*WaitJWTTokenRequest)(nil), // 85: daemon.WaitJWTTokenRequest + (*WaitJWTTokenResponse)(nil), // 86: daemon.WaitJWTTokenResponse + (*StartCPUProfileRequest)(nil), // 87: daemon.StartCPUProfileRequest + (*StartCPUProfileResponse)(nil), // 88: daemon.StartCPUProfileResponse + (*StopCPUProfileRequest)(nil), // 89: daemon.StopCPUProfileRequest + (*StopCPUProfileResponse)(nil), // 90: daemon.StopCPUProfileResponse + (*InstallerResultRequest)(nil), // 91: daemon.InstallerResultRequest + (*InstallerResultResponse)(nil), // 92: daemon.InstallerResultResponse + (*ExposeServiceRequest)(nil), // 93: daemon.ExposeServiceRequest + (*ExposeServiceEvent)(nil), // 94: daemon.ExposeServiceEvent + (*ExposeServiceReady)(nil), // 95: daemon.ExposeServiceReady + (*StartCaptureRequest)(nil), // 96: daemon.StartCaptureRequest + (*CapturePacket)(nil), // 97: daemon.CapturePacket + (*StartBundleCaptureRequest)(nil), // 98: daemon.StartBundleCaptureRequest + (*StartBundleCaptureResponse)(nil), // 99: daemon.StartBundleCaptureResponse + (*StopBundleCaptureRequest)(nil), // 100: daemon.StopBundleCaptureRequest + (*StopBundleCaptureResponse)(nil), // 101: daemon.StopBundleCaptureResponse + nil, // 102: daemon.Network.ResolvedIPsEntry + (*PortInfo_Range)(nil), // 103: daemon.PortInfo.Range + nil, // 104: daemon.SystemEvent.MetadataEntry + (*durationpb.Duration)(nil), // 105: google.protobuf.Duration + (*timestamppb.Timestamp)(nil), // 106: google.protobuf.Timestamp } var file_daemon_proto_depIdxs = []int32{ - 101, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration + 105, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration 25, // 1: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus - 102, // 2: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp - 102, // 3: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp - 101, // 4: daemon.PeerState.latency:type_name -> google.protobuf.Duration + 106, // 2: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp + 106, // 3: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp + 105, // 4: daemon.PeerState.latency:type_name -> google.protobuf.Duration 23, // 5: daemon.SSHServerState.sessions:type_name -> daemon.SSHSessionInfo 20, // 6: daemon.FullStatus.managementState:type_name -> daemon.ManagementState 19, // 7: daemon.FullStatus.signalState:type_name -> daemon.SignalState @@ -6953,8 +7131,8 @@ var file_daemon_proto_depIdxs = []int32{ 55, // 12: daemon.FullStatus.events:type_name -> daemon.SystemEvent 24, // 13: daemon.FullStatus.sshServerState:type_name -> daemon.SSHServerState 31, // 14: daemon.ListNetworksResponse.routes:type_name -> daemon.Network - 98, // 15: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry - 99, // 16: daemon.PortInfo.range:type_name -> daemon.PortInfo.Range + 102, // 15: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry + 103, // 16: daemon.PortInfo.range:type_name -> daemon.PortInfo.Range 32, // 17: daemon.ForwardingRule.destinationPort:type_name -> daemon.PortInfo 32, // 18: daemon.ForwardingRule.translatedPort:type_name -> daemon.PortInfo 33, // 19: daemon.ForwardingRulesResponse.rules:type_name -> daemon.ForwardingRule @@ -6965,15 +7143,15 @@ var file_daemon_proto_depIdxs = []int32{ 52, // 24: daemon.TracePacketResponse.stages:type_name -> daemon.TraceStage 2, // 25: daemon.SystemEvent.severity:type_name -> daemon.SystemEvent.Severity 3, // 26: daemon.SystemEvent.category:type_name -> daemon.SystemEvent.Category - 102, // 27: daemon.SystemEvent.timestamp:type_name -> google.protobuf.Timestamp - 100, // 28: daemon.SystemEvent.metadata:type_name -> daemon.SystemEvent.MetadataEntry + 106, // 27: daemon.SystemEvent.timestamp:type_name -> google.protobuf.Timestamp + 104, // 28: daemon.SystemEvent.metadata:type_name -> daemon.SystemEvent.MetadataEntry 55, // 29: daemon.GetEventsResponse.events:type_name -> daemon.SystemEvent - 101, // 30: daemon.SetConfigRequest.dnsRouteInterval:type_name -> google.protobuf.Duration - 68, // 31: daemon.ListProfilesResponse.profiles:type_name -> daemon.Profile + 105, // 30: daemon.SetConfigRequest.dnsRouteInterval:type_name -> google.protobuf.Duration + 72, // 31: daemon.ListProfilesResponse.profiles:type_name -> daemon.Profile 1, // 32: daemon.ExposeServiceRequest.protocol:type_name -> daemon.ExposeProtocol - 91, // 33: daemon.ExposeServiceEvent.ready:type_name -> daemon.ExposeServiceReady - 101, // 34: daemon.StartCaptureRequest.duration:type_name -> google.protobuf.Duration - 101, // 35: daemon.StartBundleCaptureRequest.timeout:type_name -> google.protobuf.Duration + 95, // 33: daemon.ExposeServiceEvent.ready:type_name -> daemon.ExposeServiceReady + 105, // 34: daemon.StartCaptureRequest.duration:type_name -> google.protobuf.Duration + 105, // 35: daemon.StartBundleCaptureRequest.timeout:type_name -> google.protobuf.Duration 30, // 36: daemon.Network.ResolvedIPsEntry.value:type_name -> daemon.IPList 5, // 37: daemon.DaemonService.Login:input_type -> daemon.LoginRequest 7, // 38: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest @@ -6993,68 +7171,72 @@ var file_daemon_proto_depIdxs = []int32{ 46, // 52: daemon.DaemonService.DeleteState:input_type -> daemon.DeleteStateRequest 48, // 53: daemon.DaemonService.SetSyncResponsePersistence:input_type -> daemon.SetSyncResponsePersistenceRequest 51, // 54: daemon.DaemonService.TracePacket:input_type -> daemon.TracePacketRequest - 92, // 55: daemon.DaemonService.StartCapture:input_type -> daemon.StartCaptureRequest - 94, // 56: daemon.DaemonService.StartBundleCapture:input_type -> daemon.StartBundleCaptureRequest - 96, // 57: daemon.DaemonService.StopBundleCapture:input_type -> daemon.StopBundleCaptureRequest + 96, // 55: daemon.DaemonService.StartCapture:input_type -> daemon.StartCaptureRequest + 98, // 56: daemon.DaemonService.StartBundleCapture:input_type -> daemon.StartBundleCaptureRequest + 100, // 57: daemon.DaemonService.StopBundleCapture:input_type -> daemon.StopBundleCaptureRequest 54, // 58: daemon.DaemonService.SubscribeEvents:input_type -> daemon.SubscribeRequest 56, // 59: daemon.DaemonService.GetEvents:input_type -> daemon.GetEventsRequest 58, // 60: daemon.DaemonService.SwitchProfile:input_type -> daemon.SwitchProfileRequest 60, // 61: daemon.DaemonService.SetConfig:input_type -> daemon.SetConfigRequest 62, // 62: daemon.DaemonService.AddProfile:input_type -> daemon.AddProfileRequest - 64, // 63: daemon.DaemonService.RemoveProfile:input_type -> daemon.RemoveProfileRequest - 66, // 64: daemon.DaemonService.ListProfiles:input_type -> daemon.ListProfilesRequest - 69, // 65: daemon.DaemonService.GetActiveProfile:input_type -> daemon.GetActiveProfileRequest - 71, // 66: daemon.DaemonService.Logout:input_type -> daemon.LogoutRequest - 73, // 67: daemon.DaemonService.GetFeatures:input_type -> daemon.GetFeaturesRequest - 75, // 68: daemon.DaemonService.TriggerUpdate:input_type -> daemon.TriggerUpdateRequest - 77, // 69: daemon.DaemonService.GetPeerSSHHostKey:input_type -> daemon.GetPeerSSHHostKeyRequest - 79, // 70: daemon.DaemonService.RequestJWTAuth:input_type -> daemon.RequestJWTAuthRequest - 81, // 71: daemon.DaemonService.WaitJWTToken:input_type -> daemon.WaitJWTTokenRequest - 83, // 72: daemon.DaemonService.StartCPUProfile:input_type -> daemon.StartCPUProfileRequest - 85, // 73: daemon.DaemonService.StopCPUProfile:input_type -> daemon.StopCPUProfileRequest - 87, // 74: daemon.DaemonService.GetInstallerResult:input_type -> daemon.InstallerResultRequest - 89, // 75: daemon.DaemonService.ExposeService:input_type -> daemon.ExposeServiceRequest - 6, // 76: daemon.DaemonService.Login:output_type -> daemon.LoginResponse - 8, // 77: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse - 10, // 78: daemon.DaemonService.Up:output_type -> daemon.UpResponse - 12, // 79: daemon.DaemonService.Status:output_type -> daemon.StatusResponse - 14, // 80: daemon.DaemonService.Down:output_type -> daemon.DownResponse - 16, // 81: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse - 27, // 82: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse - 29, // 83: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse - 29, // 84: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse - 34, // 85: daemon.DaemonService.ForwardingRules:output_type -> daemon.ForwardingRulesResponse - 36, // 86: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse - 38, // 87: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse - 40, // 88: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse - 43, // 89: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse - 45, // 90: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse - 47, // 91: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse - 49, // 92: daemon.DaemonService.SetSyncResponsePersistence:output_type -> daemon.SetSyncResponsePersistenceResponse - 53, // 93: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse - 93, // 94: daemon.DaemonService.StartCapture:output_type -> daemon.CapturePacket - 95, // 95: daemon.DaemonService.StartBundleCapture:output_type -> daemon.StartBundleCaptureResponse - 97, // 96: daemon.DaemonService.StopBundleCapture:output_type -> daemon.StopBundleCaptureResponse - 55, // 97: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent - 57, // 98: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse - 59, // 99: daemon.DaemonService.SwitchProfile:output_type -> daemon.SwitchProfileResponse - 61, // 100: daemon.DaemonService.SetConfig:output_type -> daemon.SetConfigResponse - 63, // 101: daemon.DaemonService.AddProfile:output_type -> daemon.AddProfileResponse - 65, // 102: daemon.DaemonService.RemoveProfile:output_type -> daemon.RemoveProfileResponse - 67, // 103: daemon.DaemonService.ListProfiles:output_type -> daemon.ListProfilesResponse - 70, // 104: daemon.DaemonService.GetActiveProfile:output_type -> daemon.GetActiveProfileResponse - 72, // 105: daemon.DaemonService.Logout:output_type -> daemon.LogoutResponse - 74, // 106: daemon.DaemonService.GetFeatures:output_type -> daemon.GetFeaturesResponse - 76, // 107: daemon.DaemonService.TriggerUpdate:output_type -> daemon.TriggerUpdateResponse - 78, // 108: daemon.DaemonService.GetPeerSSHHostKey:output_type -> daemon.GetPeerSSHHostKeyResponse - 80, // 109: daemon.DaemonService.RequestJWTAuth:output_type -> daemon.RequestJWTAuthResponse - 82, // 110: daemon.DaemonService.WaitJWTToken:output_type -> daemon.WaitJWTTokenResponse - 84, // 111: daemon.DaemonService.StartCPUProfile:output_type -> daemon.StartCPUProfileResponse - 86, // 112: daemon.DaemonService.StopCPUProfile:output_type -> daemon.StopCPUProfileResponse - 88, // 113: daemon.DaemonService.GetInstallerResult:output_type -> daemon.InstallerResultResponse - 90, // 114: daemon.DaemonService.ExposeService:output_type -> daemon.ExposeServiceEvent - 76, // [76:115] is the sub-list for method output_type - 37, // [37:76] is the sub-list for method input_type + 68, // 63: daemon.DaemonService.RemoveProfile:input_type -> daemon.RemoveProfileRequest + 70, // 64: daemon.DaemonService.ListProfiles:input_type -> daemon.ListProfilesRequest + 73, // 65: daemon.DaemonService.GetActiveProfile:input_type -> daemon.GetActiveProfileRequest + 64, // 66: daemon.DaemonService.AddOwner:input_type -> daemon.AddOwnerRequest + 66, // 67: daemon.DaemonService.ResetOwner:input_type -> daemon.ResetOwnerRequest + 75, // 68: daemon.DaemonService.Logout:input_type -> daemon.LogoutRequest + 77, // 69: daemon.DaemonService.GetFeatures:input_type -> daemon.GetFeaturesRequest + 79, // 70: daemon.DaemonService.TriggerUpdate:input_type -> daemon.TriggerUpdateRequest + 81, // 71: daemon.DaemonService.GetPeerSSHHostKey:input_type -> daemon.GetPeerSSHHostKeyRequest + 83, // 72: daemon.DaemonService.RequestJWTAuth:input_type -> daemon.RequestJWTAuthRequest + 85, // 73: daemon.DaemonService.WaitJWTToken:input_type -> daemon.WaitJWTTokenRequest + 87, // 74: daemon.DaemonService.StartCPUProfile:input_type -> daemon.StartCPUProfileRequest + 89, // 75: daemon.DaemonService.StopCPUProfile:input_type -> daemon.StopCPUProfileRequest + 91, // 76: daemon.DaemonService.GetInstallerResult:input_type -> daemon.InstallerResultRequest + 93, // 77: daemon.DaemonService.ExposeService:input_type -> daemon.ExposeServiceRequest + 6, // 78: daemon.DaemonService.Login:output_type -> daemon.LoginResponse + 8, // 79: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse + 10, // 80: daemon.DaemonService.Up:output_type -> daemon.UpResponse + 12, // 81: daemon.DaemonService.Status:output_type -> daemon.StatusResponse + 14, // 82: daemon.DaemonService.Down:output_type -> daemon.DownResponse + 16, // 83: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse + 27, // 84: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse + 29, // 85: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse + 29, // 86: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse + 34, // 87: daemon.DaemonService.ForwardingRules:output_type -> daemon.ForwardingRulesResponse + 36, // 88: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse + 38, // 89: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse + 40, // 90: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse + 43, // 91: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse + 45, // 92: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse + 47, // 93: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse + 49, // 94: daemon.DaemonService.SetSyncResponsePersistence:output_type -> daemon.SetSyncResponsePersistenceResponse + 53, // 95: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse + 97, // 96: daemon.DaemonService.StartCapture:output_type -> daemon.CapturePacket + 99, // 97: daemon.DaemonService.StartBundleCapture:output_type -> daemon.StartBundleCaptureResponse + 101, // 98: daemon.DaemonService.StopBundleCapture:output_type -> daemon.StopBundleCaptureResponse + 55, // 99: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent + 57, // 100: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse + 59, // 101: daemon.DaemonService.SwitchProfile:output_type -> daemon.SwitchProfileResponse + 61, // 102: daemon.DaemonService.SetConfig:output_type -> daemon.SetConfigResponse + 63, // 103: daemon.DaemonService.AddProfile:output_type -> daemon.AddProfileResponse + 69, // 104: daemon.DaemonService.RemoveProfile:output_type -> daemon.RemoveProfileResponse + 71, // 105: daemon.DaemonService.ListProfiles:output_type -> daemon.ListProfilesResponse + 74, // 106: daemon.DaemonService.GetActiveProfile:output_type -> daemon.GetActiveProfileResponse + 65, // 107: daemon.DaemonService.AddOwner:output_type -> daemon.AddOwnerResponse + 67, // 108: daemon.DaemonService.ResetOwner:output_type -> daemon.ResetOwnerResponse + 76, // 109: daemon.DaemonService.Logout:output_type -> daemon.LogoutResponse + 78, // 110: daemon.DaemonService.GetFeatures:output_type -> daemon.GetFeaturesResponse + 80, // 111: daemon.DaemonService.TriggerUpdate:output_type -> daemon.TriggerUpdateResponse + 82, // 112: daemon.DaemonService.GetPeerSSHHostKey:output_type -> daemon.GetPeerSSHHostKeyResponse + 84, // 113: daemon.DaemonService.RequestJWTAuth:output_type -> daemon.RequestJWTAuthResponse + 86, // 114: daemon.DaemonService.WaitJWTToken:output_type -> daemon.WaitJWTTokenResponse + 88, // 115: daemon.DaemonService.StartCPUProfile:output_type -> daemon.StartCPUProfileResponse + 90, // 116: daemon.DaemonService.StopCPUProfile:output_type -> daemon.StopCPUProfileResponse + 92, // 117: daemon.DaemonService.GetInstallerResult:output_type -> daemon.InstallerResultResponse + 94, // 118: daemon.DaemonService.ExposeService:output_type -> daemon.ExposeServiceEvent + 78, // [78:119] is the sub-list for method output_type + 37, // [37:78] is the sub-list for method input_type 37, // [37:37] is the sub-list for extension type_name 37, // [37:37] is the sub-list for extension extendee 0, // [0:37] is the sub-list for field type_name @@ -7076,9 +7258,9 @@ func file_daemon_proto_init() { file_daemon_proto_msgTypes[48].OneofWrappers = []any{} file_daemon_proto_msgTypes[54].OneofWrappers = []any{} file_daemon_proto_msgTypes[56].OneofWrappers = []any{} - file_daemon_proto_msgTypes[67].OneofWrappers = []any{} - file_daemon_proto_msgTypes[75].OneofWrappers = []any{} - file_daemon_proto_msgTypes[86].OneofWrappers = []any{ + file_daemon_proto_msgTypes[71].OneofWrappers = []any{} + file_daemon_proto_msgTypes[79].OneofWrappers = []any{} + file_daemon_proto_msgTypes[90].OneofWrappers = []any{ (*ExposeServiceEvent_Ready)(nil), } type x struct{} @@ -7087,7 +7269,7 @@ func file_daemon_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_proto_rawDesc), len(file_daemon_proto_rawDesc)), NumEnums: 4, - NumMessages: 97, + NumMessages: 101, NumExtensions: 0, NumServices: 1, }, diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index dedff43e2..8ef8fad0c 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -91,6 +91,15 @@ service DaemonService { rpc GetActiveProfile(GetActiveProfileRequest) returns (GetActiveProfileResponse) {} + // AddOwner adds a UID to the active profile's owner list. Requires + // root or an existing owner. + rpc AddOwner(AddOwnerRequest) returns (AddOwnerResponse) {} + + // ResetOwner clears the active profile's owner list, returning it to + // the unconfigured state. The next call from the active console-session + // user will then re-claim ownership. Requires root. + rpc ResetOwner(ResetOwnerRequest) returns (ResetOwnerResponse) {} + // Logout disconnects from the network and deletes the peer from the management server rpc Logout(LogoutRequest) returns (LogoutResponse) {} @@ -227,6 +236,10 @@ message UpRequest { optional string profileName = 1; optional string username = 2; reserved 3; + // When true, the caller claims owner privileges for this profile. + // Requires root or current owner; for new installs (root-only mode), + // the calling UID becomes an owner. + bool claimOwner = 4; } message UpResponse {} @@ -689,6 +702,16 @@ message AddProfileRequest { message AddProfileResponse {} +message AddOwnerRequest { + uint32 uid = 1; +} + +message AddOwnerResponse {} + +message ResetOwnerRequest {} + +message ResetOwnerResponse {} + message RemoveProfileRequest { string username = 1; string profileName = 2; diff --git a/client/proto/daemon_grpc.pb.go b/client/proto/daemon_grpc.pb.go index 66a8efcc3..0c39a3305 100644 --- a/client/proto/daemon_grpc.pb.go +++ b/client/proto/daemon_grpc.pb.go @@ -48,6 +48,8 @@ const ( DaemonService_RemoveProfile_FullMethodName = "/daemon.DaemonService/RemoveProfile" DaemonService_ListProfiles_FullMethodName = "/daemon.DaemonService/ListProfiles" DaemonService_GetActiveProfile_FullMethodName = "/daemon.DaemonService/GetActiveProfile" + DaemonService_AddOwner_FullMethodName = "/daemon.DaemonService/AddOwner" + DaemonService_ResetOwner_FullMethodName = "/daemon.DaemonService/ResetOwner" DaemonService_Logout_FullMethodName = "/daemon.DaemonService/Logout" DaemonService_GetFeatures_FullMethodName = "/daemon.DaemonService/GetFeatures" DaemonService_TriggerUpdate_FullMethodName = "/daemon.DaemonService/TriggerUpdate" @@ -115,6 +117,13 @@ type DaemonServiceClient interface { RemoveProfile(ctx context.Context, in *RemoveProfileRequest, opts ...grpc.CallOption) (*RemoveProfileResponse, error) ListProfiles(ctx context.Context, in *ListProfilesRequest, opts ...grpc.CallOption) (*ListProfilesResponse, error) GetActiveProfile(ctx context.Context, in *GetActiveProfileRequest, opts ...grpc.CallOption) (*GetActiveProfileResponse, error) + // AddOwner adds a UID to the active profile's owner list. Requires + // root or an existing owner. + AddOwner(ctx context.Context, in *AddOwnerRequest, opts ...grpc.CallOption) (*AddOwnerResponse, error) + // ResetOwner clears the active profile's owner list, returning it to + // the unconfigured state. The next call from the active console-session + // user will then re-claim ownership. Requires root. + ResetOwner(ctx context.Context, in *ResetOwnerRequest, opts ...grpc.CallOption) (*ResetOwnerResponse, error) // Logout disconnects from the network and deletes the peer from the management server Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*LogoutResponse, error) GetFeatures(ctx context.Context, in *GetFeaturesRequest, opts ...grpc.CallOption) (*GetFeaturesResponse, error) @@ -452,6 +461,26 @@ func (c *daemonServiceClient) GetActiveProfile(ctx context.Context, in *GetActiv return out, nil } +func (c *daemonServiceClient) AddOwner(ctx context.Context, in *AddOwnerRequest, opts ...grpc.CallOption) (*AddOwnerResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AddOwnerResponse) + err := c.cc.Invoke(ctx, DaemonService_AddOwner_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *daemonServiceClient) ResetOwner(ctx context.Context, in *ResetOwnerRequest, opts ...grpc.CallOption) (*ResetOwnerResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResetOwnerResponse) + err := c.cc.Invoke(ctx, DaemonService_ResetOwner_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *daemonServiceClient) Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*LogoutResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(LogoutResponse) @@ -616,6 +645,13 @@ type DaemonServiceServer interface { RemoveProfile(context.Context, *RemoveProfileRequest) (*RemoveProfileResponse, error) ListProfiles(context.Context, *ListProfilesRequest) (*ListProfilesResponse, error) GetActiveProfile(context.Context, *GetActiveProfileRequest) (*GetActiveProfileResponse, error) + // AddOwner adds a UID to the active profile's owner list. Requires + // root or an existing owner. + AddOwner(context.Context, *AddOwnerRequest) (*AddOwnerResponse, error) + // ResetOwner clears the active profile's owner list, returning it to + // the unconfigured state. The next call from the active console-session + // user will then re-claim ownership. Requires root. + ResetOwner(context.Context, *ResetOwnerRequest) (*ResetOwnerResponse, error) // Logout disconnects from the network and deletes the peer from the management server Logout(context.Context, *LogoutRequest) (*LogoutResponse, error) GetFeatures(context.Context, *GetFeaturesRequest) (*GetFeaturesResponse, error) @@ -732,6 +768,12 @@ func (UnimplementedDaemonServiceServer) ListProfiles(context.Context, *ListProfi func (UnimplementedDaemonServiceServer) GetActiveProfile(context.Context, *GetActiveProfileRequest) (*GetActiveProfileResponse, error) { return nil, status.Error(codes.Unimplemented, "method GetActiveProfile not implemented") } +func (UnimplementedDaemonServiceServer) AddOwner(context.Context, *AddOwnerRequest) (*AddOwnerResponse, error) { + return nil, status.Error(codes.Unimplemented, "method AddOwner not implemented") +} +func (UnimplementedDaemonServiceServer) ResetOwner(context.Context, *ResetOwnerRequest) (*ResetOwnerResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ResetOwner not implemented") +} func (UnimplementedDaemonServiceServer) Logout(context.Context, *LogoutRequest) (*LogoutResponse, error) { return nil, status.Error(codes.Unimplemented, "method Logout not implemented") } @@ -1291,6 +1333,42 @@ func _DaemonService_GetActiveProfile_Handler(srv interface{}, ctx context.Contex return interceptor(ctx, in, info, handler) } +func _DaemonService_AddOwner_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AddOwnerRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).AddOwner(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DaemonService_AddOwner_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).AddOwner(ctx, req.(*AddOwnerRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DaemonService_ResetOwner_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ResetOwnerRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).ResetOwner(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DaemonService_ResetOwner_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).ResetOwner(ctx, req.(*ResetOwnerRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _DaemonService_Logout_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(LogoutRequest) if err := dec(in); err != nil { @@ -1579,6 +1657,14 @@ var DaemonService_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetActiveProfile", Handler: _DaemonService_GetActiveProfile_Handler, }, + { + MethodName: "AddOwner", + Handler: _DaemonService_AddOwner_Handler, + }, + { + MethodName: "ResetOwner", + Handler: _DaemonService_ResetOwner_Handler, + }, { MethodName: "Logout", Handler: _DaemonService_Logout_Handler, diff --git a/client/server/owner.go b/client/server/owner.go new file mode 100644 index 000000000..c6ee66ceb --- /dev/null +++ b/client/server/owner.go @@ -0,0 +1,172 @@ +package server + +import ( + "context" + "fmt" + "slices" + + log "github.com/sirupsen/logrus" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/netbirdio/netbird/client/internal/owner" + "github.com/netbirdio/netbird/client/internal/profilemanager" + "github.com/netbirdio/netbird/client/proto" + "github.com/netbirdio/netbird/util" +) + +// authorizeTargetProfile enforces the "match or root" rule for operations +// that target a specific profile (Remove/Switch). The caller must be root +// or appear in the target profile config's OwnerUIDs. A target profile in +// legacy TOFU state (nil OwnerUIDs) is treated as unowned and therefore +// accessible to any peer-creds caller, which matches pre-enforcement +// behavior on upgraded installs. +func (s *Server) authorizeTargetProfile(ctx context.Context, profileName, username string) error { + uid, ok := owner.UIDFromContext(ctx) + if !ok { + return status.Error(codes.PermissionDenied, "peer credentials unavailable") + } + if uid == 0 { + return nil + } + + cfg, err := s.readProfileConfig(profileName, username) + if err != nil { + return fmt.Errorf("read target profile config: %w", err) + } + + // Legacy / never-claimed target: allow, mirroring the migration TOFU + // semantics in the interceptor. + if cfg.OwnerUIDs == nil { + return nil + } + + if slices.Contains(cfg.OwnerUIDs, uid) { + return nil + } + + return status.Errorf(codes.PermissionDenied, + "profile %q is owned by another user (uid %d is not in its owner list)", profileName, uid) +} + +// readProfileConfig loads a profile's config from disk without making it +// active. Used by authorizeTargetProfile. +func (s *Server) readProfileConfig(profileName, username string) (*profilemanager.Config, error) { + state := &profilemanager.ActiveProfileState{Name: profileName, Username: username} + path, err := state.FilePath() + if err != nil { + return nil, fmt.Errorf("resolve profile path: %w", err) + } + cfg, err := profilemanager.GetConfig(path) + if err != nil { + return nil, fmt.Errorf("load %s: %w", path, err) + } + return cfg, nil +} + +// GetOwnerUIDs returns the current owner UIDs from the active config. +// nil means TOFU mode, empty means root-only, populated means those UIDs are owners. +func (s *Server) GetOwnerUIDs() []owner.UID { + s.mutex.Lock() + defer s.mutex.Unlock() + + if s.config == nil { + return nil + } + + return s.config.OwnerUIDs +} + +// AddOwnerUID adds the given UID to the owner list in the active profile config. +func (s *Server) AddOwnerUID(uid owner.UID) error { + s.mutex.Lock() + defer s.mutex.Unlock() + + return s.addOwnerUIDLocked(uid) +} + +// addOwnerUIDLocked adds uid to the active profile's owner list and persists it. +// The caller must hold s.mutex. +func (s *Server) addOwnerUIDLocked(uid owner.UID) error { + if s.config == nil { + return fmt.Errorf("config not loaded") + } + + if slices.Contains(s.config.OwnerUIDs, uid) { + return nil + } + + s.config.OwnerUIDs = append(s.config.OwnerUIDs, uid) + + activeProf, err := s.profileManager.GetActiveProfileState() + if err != nil { + return fmt.Errorf("get active profile: %w", err) + } + + cfgPath, err := activeProf.FilePath() + if err != nil { + return fmt.Errorf("get profile file path: %w", err) + } + + if err := util.WriteJson(context.Background(), cfgPath, s.config); err != nil { + return fmt.Errorf("write config: %w", err) + } + + log.Infof("owner UID %d added in %s (owners: %v)", uid, cfgPath, s.config.OwnerUIDs) + return nil +} + +// AddOwner handles the AddOwner RPC. The interceptor has already gated this +// call (caller must be root or an existing owner); the handler just persists +// the new UID into the active profile config. +func (s *Server) AddOwner(_ context.Context, msg *proto.AddOwnerRequest) (*proto.AddOwnerResponse, error) { + if msg == nil || msg.Uid == 0 { + return nil, status.Error(codes.InvalidArgument, "uid must be non-zero") + } + if err := s.AddOwnerUID(owner.UID(msg.Uid)); err != nil { + return nil, fmt.Errorf("add owner: %w", err) + } + return &proto.AddOwnerResponse{}, nil +} + +// ResetOwner clears the active profile's owner list. Only callable by root +// (the interceptor enforces this: a non-owner non-root caller is denied +// before reaching the handler, and only owners or root can reach Add/Reset +// at all; we additionally require root here so existing owners can't reset +// each other out). +func (s *Server) ResetOwner(ctx context.Context, _ *proto.ResetOwnerRequest) (*proto.ResetOwnerResponse, error) { + uid, ok := owner.UIDFromContext(ctx) + if !ok { + return nil, status.Error(codes.PermissionDenied, "peer credentials unavailable") + } + if uid != 0 { + return nil, status.Error(codes.PermissionDenied, "reset-owner requires root") + } + + s.mutex.Lock() + defer s.mutex.Unlock() + + if s.config == nil { + return nil, fmt.Errorf("config not loaded") + } + + // Reset to the fresh-install state (empty, not nil): only root and the + // active console-session user can reclaim. nil would be legacy migration + // TOFU, where any non-root caller (including SSH) could reclaim. + s.config.OwnerUIDs = []owner.UID{} + + activeProf, err := s.profileManager.GetActiveProfileState() + if err != nil { + return nil, fmt.Errorf("get active profile: %w", err) + } + cfgPath, err := activeProf.FilePath() + if err != nil { + return nil, fmt.Errorf("get profile file path: %w", err) + } + if err := util.WriteJson(context.Background(), cfgPath, s.config); err != nil { + return nil, fmt.Errorf("write config: %w", err) + } + + log.Infof("owner list reset; next call from the active console user will re-claim ownership") + return &proto.ResetOwnerResponse{}, nil +} diff --git a/client/server/server.go b/client/server/server.go index 397fb37e4..8d3f82885 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -22,6 +22,7 @@ import ( "github.com/netbirdio/netbird/client/internal/auth" "github.com/netbirdio/netbird/client/internal/expose" + "github.com/netbirdio/netbird/client/internal/owner" "github.com/netbirdio/netbird/client/internal/profilemanager" sleephandler "github.com/netbirdio/netbird/client/internal/sleep/handler" "github.com/netbirdio/netbird/client/system" @@ -735,6 +736,18 @@ func (s *Server) Up(callerCtx context.Context, msg *proto.UpRequest) (*proto.UpR } s.config = config + // An explicit --owner claim locks the active profile to the calling user + // (plus root). Root has no specific UID to claim, so only non-root callers + // take effect here; the interceptor has already authorized the call. + if msg != nil && msg.ClaimOwner { + if uid, ok := owner.UIDFromContext(callerCtx); ok && uid != 0 { + if err := s.addOwnerUIDLocked(uid); err != nil { + s.mutex.Unlock() + return nil, fmt.Errorf("claim owner: %w", err) + } + } + } + s.statusRecorder.UpdateManagementAddress(s.config.ManagementURL.String()) s.statusRecorder.UpdateRosenpass(s.config.RosenpassEnabled, s.config.RosenpassPermissive) @@ -800,6 +813,18 @@ func (s *Server) switchProfileIfNeeded(profileName string, userName *string, act // SwitchProfile switches the active profile in the daemon. func (s *Server) SwitchProfile(callerCtx context.Context, msg *proto.SwitchProfileRequest) (*proto.SwitchProfileResponse, error) { + // Switching downs the current session and starts another, so the caller + // must own the target profile (or be root). + if msg != nil && msg.ProfileName != nil { + username := "" + if msg.Username != nil { + username = *msg.Username + } + if err := s.authorizeTargetProfile(callerCtx, *msg.ProfileName, username); err != nil { + return nil, err + } + } + s.mutex.Lock() defer s.mutex.Unlock() @@ -1564,7 +1589,17 @@ func (s *Server) AddProfile(ctx context.Context, msg *proto.AddProfileRequest) ( return nil, gstatus.Errorf(codes.InvalidArgument, "profile name and username must be provided") } - if err := s.profileManager.AddProfile(msg.ProfileName, msg.Username); err != nil { + // New profiles auto-claim the caller as their sole owner so the user who + // just created the profile retains control (and other local users can't + // touch it via SwitchProfile/RemoveProfile). When called by root, leave + // OwnerUIDs at the default (empty/env-seeded); root explicitly didn't + // claim ownership for any specific user. + var initialOwners []owner.UID + if uid, ok := owner.UIDFromContext(ctx); ok && uid != 0 { + initialOwners = []owner.UID{uid} + } + + if err := s.profileManager.AddProfile(msg.ProfileName, msg.Username, initialOwners); err != nil { log.Errorf("failed to create profile: %v", err) return nil, fmt.Errorf("failed to create profile: %w", err) } @@ -1574,6 +1609,10 @@ func (s *Server) AddProfile(ctx context.Context, msg *proto.AddProfileRequest) ( // RemoveProfile removes a profile from the daemon. func (s *Server) RemoveProfile(ctx context.Context, msg *proto.RemoveProfileRequest) (*proto.RemoveProfileResponse, error) { + if err := s.authorizeTargetProfile(ctx, msg.ProfileName, msg.Username); err != nil { + return nil, err + } + s.mutex.Lock() defer s.mutex.Unlock()