From 27afbd79528a9a79cb3de300402c697fd2a2ac5a Mon Sep 17 00:00:00 2001 From: "Theodor S. Midtlien" Date: Thu, 23 Jul 2026 14:13:58 +0200 Subject: [PATCH] WIP: acl interceptor and named pipe ui --- client/android/profile_manager.go | 2 +- client/cmd/owner.go | 115 +++ client/cmd/root.go | 34 +- client/cmd/service_controller.go | 42 +- client/cmd/service_json_gateway.go | 32 +- client/cmd/service_pipe_other.go | 20 + client/cmd/service_pipe_windows.go | 32 + client/cmd/service_socket.go | 21 +- client/cmd/up.go | 3 + client/cmd/up_daemon_test.go | 2 +- client/internal/ipcauth/authorize.go | 90 ++ client/internal/ipcauth/creds_stub.go | 12 + client/internal/ipcauth/creds_unix.go | 48 + client/internal/ipcauth/creds_windows.go | 143 +++ client/internal/ipcauth/forward.go | 72 ++ client/internal/ipcauth/identity.go | 106 +++ client/internal/ipcauth/identity_test.go | 50 ++ client/internal/ipcauth/interceptor.go | 124 +++ client/internal/ipcauth/interceptor_test.go | 139 +++ client/internal/ipcauth/peercred_bsd.go | 43 + client/internal/ipcauth/peercred_linux.go | 43 + client/internal/ipcauth/peercred_stub.go | 16 + client/internal/ipcauth/peercred_unix_test.go | 114 +++ client/internal/ipcauth/policy.go | 92 ++ client/internal/ipcauth/principal.go | 55 ++ client/internal/ipcauth/resolver_unix.go | 72 ++ client/internal/ipcauth/resolver_windows.go | 10 + client/internal/profilemanager/config.go | 28 + client/internal/profilemanager/service.go | 5 +- .../internal/profilemanager/service_test.go | 18 +- client/internal/shell/getent_cgo_unix.go | 29 + .../shell}/getent_nocgo_unix.go | 38 +- .../server => internal/shell}/getent_test.go | 54 +- .../server => internal/shell}/getent_unix.go | 52 +- .../shell}/getent_unix_test.go | 22 +- client/internal/shell/getent_windows.go | 31 + .../{ssh/server => internal/shell}/shell.go | 51 +- client/proto/daemon.pb.go | 837 ++++++++++++------ client/proto/daemon.pb.gw.go | 189 ++++ client/proto/daemon.proto | 34 + client/proto/daemon_grpc.pb.go | 126 +++ client/server/ownership.go | 204 +++++ client/server/profile_authz.go | 53 ++ client/server/profile_authz_unix.go | 20 + client/server/profile_authz_windows.go | 18 + client/server/server.go | 59 +- client/server/ssh_gate.go | 44 + client/server/ssh_gate_test.go | 52 ++ client/ssh/server/command_execution_unix.go | 6 +- .../ssh/server/command_execution_windows.go | 11 +- client/ssh/server/getent_cgo_unix.go | 24 - client/ssh/server/getent_windows.go | 26 - client/ssh/server/ssh_env.go | 35 + client/ssh/server/user_utils.go | 6 +- client/ssh/server/userswitching_unix.go | 12 +- client/ssh/server/userswitching_windows.go | 4 +- client/ui/grpc.go | 16 +- go.mod | 2 +- 58 files changed, 3163 insertions(+), 475 deletions(-) create mode 100644 client/cmd/owner.go create mode 100644 client/cmd/service_pipe_other.go create mode 100644 client/cmd/service_pipe_windows.go create mode 100644 client/internal/ipcauth/authorize.go create mode 100644 client/internal/ipcauth/creds_stub.go create mode 100644 client/internal/ipcauth/creds_unix.go create mode 100644 client/internal/ipcauth/creds_windows.go create mode 100644 client/internal/ipcauth/forward.go create mode 100644 client/internal/ipcauth/identity.go create mode 100644 client/internal/ipcauth/identity_test.go create mode 100644 client/internal/ipcauth/interceptor.go create mode 100644 client/internal/ipcauth/interceptor_test.go create mode 100644 client/internal/ipcauth/peercred_bsd.go create mode 100644 client/internal/ipcauth/peercred_linux.go create mode 100644 client/internal/ipcauth/peercred_stub.go create mode 100644 client/internal/ipcauth/peercred_unix_test.go create mode 100644 client/internal/ipcauth/policy.go create mode 100644 client/internal/ipcauth/principal.go create mode 100644 client/internal/ipcauth/resolver_unix.go create mode 100644 client/internal/ipcauth/resolver_windows.go create mode 100644 client/internal/shell/getent_cgo_unix.go rename client/{ssh/server => internal/shell}/getent_nocgo_unix.go (56%) rename client/{ssh/server => internal/shell}/getent_test.go (75%) rename client/{ssh/server => internal/shell}/getent_unix.go (64%) rename client/{ssh/server => internal/shell}/getent_unix_test.go (95%) create mode 100644 client/internal/shell/getent_windows.go rename client/{ssh/server => internal/shell}/shell.go (69%) create mode 100644 client/server/ownership.go create mode 100644 client/server/profile_authz.go create mode 100644 client/server/profile_authz_unix.go create mode 100644 client/server/profile_authz_windows.go create mode 100644 client/server/ssh_gate.go create mode 100644 client/server/ssh_gate_test.go delete mode 100644 client/ssh/server/getent_cgo_unix.go delete mode 100644 client/ssh/server/getent_windows.go create mode 100644 client/ssh/server/ssh_env.go diff --git a/client/android/profile_manager.go b/client/android/profile_manager.go index 87c001396..691d8cf12 100644 --- a/client/android/profile_manager.go +++ b/client/android/profile_manager.go @@ -145,7 +145,7 @@ func (pm *ProfileManager) SwitchProfile(id string) error { // AddProfile creates a new profile func (pm *ProfileManager) AddProfile(profileName string) error { // Use ServiceManager (creates profile in profiles/ directory) - profile, err := pm.serviceMgr.AddProfile(profileName, androidUsername) + profile, err := pm.serviceMgr.AddProfile(profileName, androidUsername, nil) if 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..1ac146cd9 --- /dev/null +++ b/client/cmd/owner.go @@ -0,0 +1,115 @@ +package cmd + +import ( + "context" + "time" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "github.com/netbirdio/netbird/client/proto" + "github.com/netbirdio/netbird/util" +) + +var ownerCmd = &cobra.Command{ + Use: "owner", + Short: "Manage who may control the active NetBird profile", + Long: `Manage the owners of the active profile's daemon control channel. + +Ownership is enforced per profile: an isolated profile can only be controlled by +its owner principals (plus root/administrator). A new profile is automatically +owned by its creator; an unowned profile is claimed by the first caller.`, +} + +var ownerAddCmd = &cobra.Command{ + Use: "add ", + Short: "Add an owner principal to the active profile", + Long: `Add an owner principal to the active profile. Principals are typed: + uid:1000 a Unix user ID + gid:1000 a Unix group ID + group:netbird-admins a Unix group name (resolved via NSS/getent) + sid:S-1-5-21-... a Windows user or group SID + +Requires root/administrator or an existing owner.`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return withDaemon(cmd, func(ctx context.Context, c proto.DaemonServiceClient) error { + if _, err := c.AddOwner(ctx, &proto.AddOwnerRequest{Principal: args[0]}); err != nil { + return err + } + cmd.Printf("Added owner %q to the active profile\n", args[0]) + return nil + }) + }, +} + +var ownerResetCmd = &cobra.Command{ + Use: "reset", + Short: "Clear the active profile's owner list (root/administrator only)", + Long: `Clear the active profile's owner list, returning it to the unowned +state. The next caller then claims ownership (trust-on-first-use). Requires +root/administrator.`, + RunE: func(cmd *cobra.Command, args []string) error { + return withDaemon(cmd, func(ctx context.Context, c proto.DaemonServiceClient) error { + if _, err := c.ResetOwner(ctx, &proto.ResetOwnerRequest{}); err != nil { + return err + } + cmd.Println("Owner list cleared; the next caller will claim ownership") + return nil + }) + }, +} + +var ownerShareCmd = &cobra.Command{ + Use: "share", + Short: "Mark the active profile shared (any local user may control it)", + RunE: func(cmd *cobra.Command, args []string) error { + return withDaemon(cmd, func(ctx context.Context, c proto.DaemonServiceClient) error { + if _, err := c.ShareProfile(ctx, &proto.ShareProfileRequest{Shared: true}); err != nil { + return err + } + cmd.Println("Active profile is now shared with all local users") + return nil + }) + }, +} + +var ownerUnshareCmd = &cobra.Command{ + Use: "unshare", + Short: "Stop sharing the active profile (restrict to its owners)", + RunE: func(cmd *cobra.Command, args []string) error { + return withDaemon(cmd, func(ctx context.Context, c proto.DaemonServiceClient) error { + if _, err := c.ShareProfile(ctx, &proto.ShareProfileRequest{Shared: false}); err != nil { + return err + } + cmd.Println("Active profile is no longer shared") + return nil + }) + }, +} + +// withDaemon runs fn with a connected daemon client, handling setup and teardown. +func withDaemon(cmd *cobra.Command, fn func(context.Context, proto.DaemonServiceClient) error) error { + SetFlagsFromEnvVars(rootCmd) + cmd.SetOut(cmd.OutOrStdout()) + if err := util.InitLog(logLevel, util.LogConsole); err != nil { + log.Errorf("failed initializing log %v", err) + return err + } + + ctx, cancel := context.WithTimeout(cmd.Context(), 20*time.Second) + defer cancel() + + conn, err := DialClientGRPCServer(ctx, daemonAddr) + if err != nil { + log.Errorf("failed to connect to service CLI interface %v", err) + return err + } + defer func() { + if cerr := conn.Close(); cerr != nil { + log.Debugf("close daemon connection: %v", cerr) + } + }() + + return fn(ctx, proto.NewDaemonServiceClient(conn)) +} diff --git a/client/cmd/root.go b/client/cmd/root.go index f1ef32717..24ee8a6de 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "io/fs" + "net" "os" "os/signal" "path" @@ -143,10 +144,12 @@ func init() { defaultDaemonAddr := "unix:///var/run/netbird.sock" if runtime.GOOS == "windows" { - defaultDaemonAddr = "tcp://127.0.0.1:41731" + // Named pipe (not loopback TCP): the pipe client token carries the + // caller's SID so the daemon can authorize each RPC by caller identity. + defaultDaemonAddr = "npipe://netbird" } - rootCmd.PersistentFlags().StringVar(&daemonAddr, "daemon-addr", defaultDaemonAddr, "Daemon service address to serve CLI requests [unix|tcp]://[path|host:port]") + rootCmd.PersistentFlags().StringVar(&daemonAddr, "daemon-addr", defaultDaemonAddr, "Daemon service address to serve CLI requests [unix|tcp|npipe]://[path|host:port|name]") rootCmd.PersistentFlags().StringVarP(&managementURL, "management-url", "m", "", fmt.Sprintf("Management Service URL [http|https]://[host]:[port] (default \"%s\")", profilemanager.DefaultManagementURL)) rootCmd.PersistentFlags().StringVar(&adminURL, "admin-url", "", fmt.Sprintf("Admin Panel URL [http|https]://[host]:[port] (default \"%s\")", profilemanager.DefaultAdminURL)) rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", "info", "sets NetBird log level") @@ -172,6 +175,9 @@ func init() { rootCmd.AddCommand(profileCmd) rootCmd.AddCommand(exposeCmd) + rootCmd.AddCommand(ownerCmd) + ownerCmd.AddCommand(ownerAddCmd, ownerResetCmd, ownerShareCmd, ownerUnshareCmd) + networksCMD.AddCommand(routesListCmd) networksCMD.AddCommand(routesSelectCmd, routesDeselectCmd) @@ -265,16 +271,28 @@ func FlagNameToEnvVar(cmdFlag string, prefix string) string { } // DialClientGRPCServer returns client connection to the daemon server. -func DialClientGRPCServer(ctx context.Context, addr string) (*grpc.ClientConn, error) { +func DialClientGRPCServer(ctx context.Context, addr string, opts ...grpc.DialOption) (*grpc.ClientConn, error) { ctx, cancel := context.WithTimeout(ctx, time.Second*10) defer cancel() - return grpc.DialContext( - ctx, - strings.TrimPrefix(addr, "tcp://"), + opts = append([]grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithBlock(), - ) + grpc.WithBlock()}, opts...) + + // The daemon reads the caller's kernel identity from the transport + // (SO_PEERCRED on a Unix socket, the client token on a Windows named pipe), + // so the client stays insecure. For npipe we install a context dialer since + // gRPC's resolver does not understand Windows named pipes. + target := strings.TrimPrefix(addr, "tcp://") + if strings.HasPrefix(addr, "npipe://") { + path := pipePath(strings.TrimPrefix(addr, "npipe://")) + opts = append(opts, grpc.WithContextDialer(func(dialCtx context.Context, _ string) (net.Conn, error) { + return dialNamedPipe(dialCtx, path) + })) + target = "passthrough:///netbird-daemon-pipe" + } + + return grpc.DialContext(ctx, target, opts...) } // WithBackOff execute function in backoff cycle. diff --git a/client/cmd/service_controller.go b/client/cmd/service_controller.go index 5ef13a0a6..d56aa9457 100644 --- a/client/cmd/service_controller.go +++ b/client/cmd/service_controller.go @@ -5,6 +5,7 @@ package cmd import ( "context" "fmt" + "runtime" "time" "github.com/kardianos/service" @@ -13,12 +14,36 @@ import ( "github.com/spf13/cobra" "google.golang.org/grpc" + "github.com/netbirdio/netbird/client/internal/ipcauth" "github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/server" "github.com/netbirdio/netbird/client/system" "github.com/netbirdio/netbird/util" ) +// daemonServerOptions installs peer-identity transport credentials and the +// authorization interceptor on the daemon control channel. Identity is only +// available over a Unix socket (SO_PEERCRED) or a Windows named pipe (client +// token); over TCP, or on platforms without a peer-credential primitive, the +// daemon runs without per-caller authorization and warns (no interceptor, so it +// does not deny everyone). +func daemonServerOptions(network string, interceptor *ipcauth.Interceptor) []grpc.ServerOption { + creds := ipcauth.NewTransportCredentials() + if creds == nil { + log.Warnf("daemon control channel has no peer-identity primitive on %s; per-caller authorization is disabled", runtime.GOOS) + return nil + } + if network == "tcp" { + log.Warnf("daemon is listening on TCP (%s); peer identity cannot be authenticated over TCP, per-caller authorization is disabled", daemonAddr) + return nil + } + return []grpc.ServerOption{ + grpc.Creds(creds), + grpc.ChainUnaryInterceptor(interceptor.UnaryServerInterceptor()), + grpc.ChainStreamInterceptor(interceptor.StreamServerInterceptor()), + } +} + func validateJSONSocketFlags() error { if serviceCmd.PersistentFlags().Changed("json-socket") && !enableJSONSocket { return fmt.Errorf("--json-socket requires --enable-json-socket to configure the daemon JSON gateway") @@ -37,8 +62,19 @@ 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() + network, _, err := parseListenAddress(daemonAddr) + if err != nil { + return fmt.Errorf("parse daemon address: %w", err) + } + + // Owner-authorization interceptor. The ConfigAdapter is a lazy bridge: the + // gRPC server is built before the daemon server instance exists, so we set + // the real policy backend below once serverInstance is created. + ownerAdapter := &ipcauth.ConfigAdapter{} + authInterceptor := ipcauth.NewInterceptor(ownerAdapter, ipcauth.NewDefaultGroupResolver()) + + // in any case, even if configuration does not exist we run daemon to serve the CLI gRPC API. + p.serv = grpc.NewServer(daemonServerOptions(network, authInterceptor)...) daemonListener, err := listenOnAddress(daemonAddr) if err != nil { @@ -77,6 +113,7 @@ func (p *program) Start(svc service.Service) error { if err := serverInstance.Start(); err != nil { log.Fatalf("failed to start daemon: %v", err) } + ownerAdapter.SetBackend(serverInstance) proto.RegisterDaemonServiceServer(p.serv, serverInstance) p.serverInstanceMu.Lock() @@ -84,6 +121,7 @@ func (p *program) Start(svc service.Service) error { p.serverInstanceMu.Unlock() if jsonListener != nil { + log.Warnf("JSON gateway (--enable-json-socket) re-dials the daemon locally; the HTTP client's identity is forwarded so per-caller authorization still applies, but restrict access to %s appropriately", jsonSocket) if err := p.startJSONGateway(jsonListener, daemonAddr); err != nil { log.Fatalf("failed to start daemon JSON server: %v", err) } diff --git a/client/cmd/service_json_gateway.go b/client/cmd/service_json_gateway.go index 29c1a6456..8c39c5f45 100644 --- a/client/cmd/service_json_gateway.go +++ b/client/cmd/service_json_gateway.go @@ -14,16 +14,45 @@ import ( log "github.com/sirupsen/logrus" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" + "github.com/netbirdio/netbird/client/internal/ipcauth" "github.com/netbirdio/netbird/client/proto" ) +// jsonPeerCtxKey keys the HTTP client's kernel identity in the request context. +type jsonPeerCtxKey struct{} + +// jsonConnContext reads the connecting HTTP client's identity from the JSON +// socket (peercred) and stashes it so it can be forwarded to the daemon. The +// gateway re-dials the daemon as the daemon's own identity, so without this the +// daemon would see every JSON request as privileged. +func jsonConnContext(ctx context.Context, c net.Conn) context.Context { + id, err := ipcauth.PeerIdentity(c) + if err != nil { + log.Debugf("json gateway: cannot read HTTP client identity, requests won't carry it: %v", err) + return ctx + } + return context.WithValue(ctx, jsonPeerCtxKey{}, id) +} + +// jsonForwardIdentity injects the stashed HTTP client identity as gRPC metadata +// on the gateway's re-dial to the daemon. The daemon trusts it only because the +// dial arrives as the daemon's own (self/privileged) identity. +func jsonForwardIdentity(ctx context.Context, _ *http.Request) metadata.MD { + id, ok := ctx.Value(jsonPeerCtxKey{}).(ipcauth.Identity) + if !ok { + return nil + } + return ipcauth.ForwardIdentityMetadata(id) +} + func grpcGatewayEndpoint(addr string) string { return strings.TrimPrefix(addr, "tcp://") } func (p *program) startJSONGateway(jsonListener *socketListener, daemonEndpoint string) error { - mux := runtime.NewServeMux() + mux := runtime.NewServeMux(runtime.WithMetadata(jsonForwardIdentity)) opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())} if err := proto.RegisterDaemonServiceHandlerFromEndpoint(p.ctx, mux, grpcGatewayEndpoint(daemonEndpoint), opts); err != nil { return err @@ -35,6 +64,7 @@ func (p *program) startJSONGateway(jsonListener *socketListener, daemonEndpoint BaseContext: func(net.Listener) context.Context { return p.ctx }, + ConnContext: jsonConnContext, } p.jsonServMu.Lock() diff --git a/client/cmd/service_pipe_other.go b/client/cmd/service_pipe_other.go new file mode 100644 index 000000000..dd78cc0c4 --- /dev/null +++ b/client/cmd/service_pipe_other.go @@ -0,0 +1,20 @@ +//go:build !windows + +package cmd + +import ( + "context" + "fmt" + "net" + "runtime" +) + +// listenNamedPipe is unsupported off Windows; named pipes are a Windows-only transport. +func listenNamedPipe(string) (net.Listener, error) { + return nil, fmt.Errorf("named pipe daemon socket is only supported on Windows, not %s", runtime.GOOS) +} + +// dialNamedPipe is unsupported off Windows. +func dialNamedPipe(context.Context, string) (net.Conn, error) { + return nil, fmt.Errorf("named pipe daemon socket is only supported on Windows, not %s", runtime.GOOS) +} diff --git a/client/cmd/service_pipe_windows.go b/client/cmd/service_pipe_windows.go new file mode 100644 index 000000000..51eff1cae --- /dev/null +++ b/client/cmd/service_pipe_windows.go @@ -0,0 +1,32 @@ +//go:build windows + +package cmd + +import ( + "context" + "net" + "time" + + "github.com/Microsoft/go-winio" + + "github.com/netbirdio/netbird/client/internal/ipcauth" +) + +// listenNamedPipe creates the daemon control named pipe with a tight SDDL +// (SYSTEM + Administrators + interactive users). ListenPipe fails if the pipe +// already exists (first-instance semantics), which prevents a squatting process +// from pre-creating it — we surface that error loudly rather than falling back. +func listenNamedPipe(path string) (net.Listener, error) { + return winio.ListenPipe(path, &winio.PipeConfig{ + SecurityDescriptor: ipcauth.DefaultPipeSDDL(), + }) +} + +// dialNamedPipe connects to the daemon control named pipe. +func dialNamedPipe(ctx context.Context, path string) (net.Conn, error) { + if deadline, ok := ctx.Deadline(); ok { + timeout := time.Until(deadline) + return winio.DialPipe(path, &timeout) + } + return winio.DialPipeContext(ctx, path) +} diff --git a/client/cmd/service_socket.go b/client/cmd/service_socket.go index f825a4062..67031203b 100644 --- a/client/cmd/service_socket.go +++ b/client/cmd/service_socket.go @@ -26,6 +26,15 @@ func listenOnAddress(addr string) (*socketListener, error) { return nil, err } + if network == "npipe" { + path := pipePath(address) + listener, err := listenNamedPipe(path) + if err != nil { + return nil, err + } + return &socketListener{Listener: listener, network: network, address: path}, nil + } + if network == "unix" { removeStaleUnixSocket(address) } @@ -45,13 +54,23 @@ func parseListenAddress(addr string) (string, string, error) { } switch network { - case "unix", "tcp": + case "unix", "tcp", "npipe": return network, address, nil default: return "", "", fmt.Errorf("unsupported daemon address protocol: %v", network) } } +// pipePath maps a daemon-addr npipe name (e.g. "netbird" from "npipe://netbird") +// to a Windows named-pipe path (\\.\pipe\netbird). A full \\.\pipe\ path is +// returned unchanged. +func pipePath(name string) string { + if strings.HasPrefix(name, `\\`) { + return name + } + return `\\.\pipe\` + name +} + func removeStaleUnixSocket(path string) { stat, err := os.Lstat(path) if err != nil { diff --git a/client/cmd/up.go b/client/cmd/up.go index 2d9731f26..4d8ff9c27 100644 --- a/client/cmd/up.go +++ b/client/cmd/up.go @@ -56,6 +56,7 @@ var ( showQR bool profileName string configPath string + claimOwner bool upCmd = &cobra.Command{ Use: "up", @@ -67,6 +68,7 @@ var ( func init() { upCmd.PersistentFlags().BoolVarP(&foregroundMode, "foreground-mode", "F", false, "start service in foreground") + upCmd.PersistentFlags().BoolVar(&claimOwner, "owner", false, "claim ownership of this profile for the current user, restricting daemon control of it to you and root/administrator") upCmd.PersistentFlags().StringVar(&interfaceName, interfaceNameFlag, iface.WgInterfaceDefault, "WireGuard interface name") upCmd.PersistentFlags().Uint16Var(&wireguardPort, wireguardPortFlag, iface.DefaultWgPort, "WireGuard interface listening port") upCmd.PersistentFlags().Uint16Var(&mtu, mtuFlag, iface.DefaultMTU, "Set MTU (Maximum Transmission Unit) for the WireGuard interface") @@ -391,6 +393,7 @@ func doDaemonUp(ctx context.Context, cmd *cobra.Command, client proto.DaemonServ if _, err := client.Up(ctx, &proto.UpRequest{ ProfileName: &profileID, 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 ea4cdf162..f61f11000 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{} - created, err := sm.AddProfile("test1", currUser.Username) + created, err := sm.AddProfile("test1", currUser.Username, nil) if err != nil { t.Fatalf("failed to add profile: %v", err) return diff --git a/client/internal/ipcauth/authorize.go b/client/internal/ipcauth/authorize.go new file mode 100644 index 000000000..3673b157d --- /dev/null +++ b/client/internal/ipcauth/authorize.go @@ -0,0 +1,90 @@ +package ipcauth + +import ( + "slices" + "strconv" +) + +// Ownership is a profile's access policy: the typed owner principals plus the +// opt-in shared flag. +type Ownership struct { + Owners []string + Shared bool +} + +// GroupResolver resolves a Unix caller's effective group IDs (primary + +// supplementary, NSS-aware) and owner group names to GIDs. It is only consulted +// for Unix `gid:`/`group:` owners; Windows uses the SIDs carried in the Identity. +// A nil resolver disables group matching. +type GroupResolver interface { + // CallerGIDs returns the set of group IDs the caller belongs to. + CallerGIDs(id Identity) map[uint32]struct{} + // GroupNameGID resolves a group name to its GID. + GroupNameGID(name string) (uint32, bool) +} + +// Authorize reports whether the identity may control a profile with the given +// ownership. Privileged callers (root / elevated-admin / LocalSystem) and shared +// profiles are always allowed; otherwise the identity must match one of the +// owner principals. +func Authorize(o Ownership, id Identity, r GroupResolver) bool { + if id.IsPrivileged() { + return true + } + if o.Shared { + return true + } + for _, raw := range o.Owners { + p, ok := ParsePrincipal(raw) + if !ok { + continue + } + if principalMatches(p, id, r) { + return true + } + } + return false +} + +func principalMatches(p Principal, id Identity, r GroupResolver) bool { + switch p.Kind { + case KindUID: + if id.IsWindows() { + return false + } + uid, err := strconv.ParseUint(p.Value, 10, 32) + return err == nil && uint32(uid) == id.UID + case KindGID: + if id.IsWindows() { + return false + } + gid, err := strconv.ParseUint(p.Value, 10, 32) + return err == nil && callerHasGID(uint32(gid), id, r) + case KindGroup: + if id.IsWindows() || r == nil { + return false + } + gid, ok := r.GroupNameGID(p.Value) + return ok && callerHasGID(gid, id, r) + case KindSID: + if !id.IsWindows() { + return false + } + return id.SID == p.Value || slices.Contains(id.Groups, p.Value) + default: + return false + } +} + +// callerHasGID reports whether gid is the caller's primary GID (from peercred, +// no lookup) or one of their supplementary groups (NSS-resolved via r). +func callerHasGID(gid uint32, id Identity, r GroupResolver) bool { + if id.GID == gid { + return true + } + if r == nil { + return false + } + _, ok := r.CallerGIDs(id)[gid] + return ok +} diff --git a/client/internal/ipcauth/creds_stub.go b/client/internal/ipcauth/creds_stub.go new file mode 100644 index 000000000..9af2cd695 --- /dev/null +++ b/client/internal/ipcauth/creds_stub.go @@ -0,0 +1,12 @@ +//go:build !linux && !darwin && !freebsd && !windows + +package ipcauth + +import "google.golang.org/grpc/credentials" + +// NewTransportCredentials returns nil on platforms without a peer-identity +// primitive. The daemon falls back to insecure credentials and skips per-RPC +// authorization (logging a warning), preserving pre-hardening behavior. +func NewTransportCredentials() credentials.TransportCredentials { + return nil +} diff --git a/client/internal/ipcauth/creds_unix.go b/client/internal/ipcauth/creds_unix.go new file mode 100644 index 000000000..b63e2ed60 --- /dev/null +++ b/client/internal/ipcauth/creds_unix.go @@ -0,0 +1,48 @@ +//go:build linux || darwin || freebsd + +package ipcauth + +import ( + "context" + "net" + + "google.golang.org/grpc/credentials" +) + +// NewTransportCredentials returns gRPC transport credentials that extract the +// caller's kernel-authenticated identity from a Unix-socket connection and +// expose it via IdentityFromContext. Non-nil on platforms with a +// peer-credential primitive. +func NewTransportCredentials() credentials.TransportCredentials { + return unixCreds{} +} + +// unixCreds implements credentials.TransportCredentials over a Unix socket. +// The server side reads SO_PEERCRED/LOCAL_PEERCRED during the handshake; the +// client side is a no-op (the kernel supplies the peer identity to the server +// without any client cooperation), so an ordinary insecure client still works. +type unixCreds struct{} + +func (unixCreds) ClientHandshake(_ context.Context, _ string, conn net.Conn) (net.Conn, credentials.AuthInfo, error) { + return conn, AuthInfo{}, nil +} + +// ServerHandshake extracts the peer identity and fails closed if it cannot be read. +func (unixCreds) ServerHandshake(conn net.Conn) (net.Conn, credentials.AuthInfo, error) { + id, err := PeerIdentity(conn) + if err != nil { + return nil, nil, err + } + return conn, AuthInfo{ + CommonAuthInfo: credentials.CommonAuthInfo{SecurityLevel: credentials.NoSecurity}, + Identity: id, + }, nil +} + +func (unixCreds) Info() credentials.ProtocolInfo { + return credentials.ProtocolInfo{SecurityProtocol: "netbird-ipc-peercred"} +} + +func (unixCreds) Clone() credentials.TransportCredentials { return unixCreds{} } + +func (unixCreds) OverrideServerName(string) error { return nil } diff --git a/client/internal/ipcauth/creds_windows.go b/client/internal/ipcauth/creds_windows.go new file mode 100644 index 000000000..f09cb27f9 --- /dev/null +++ b/client/internal/ipcauth/creds_windows.go @@ -0,0 +1,143 @@ +//go:build windows + +package ipcauth + +import ( + "context" + "fmt" + "net" + "runtime" + + "golang.org/x/sys/windows" + "google.golang.org/grpc/credentials" +) + +var ( + modadvapi32 = windows.NewLazySystemDLL("advapi32.dll") + procImpersonateNamedPipeClient = modadvapi32.NewProc("ImpersonateNamedPipeClient") + procRevertToSelf = modadvapi32.NewProc("RevertToSelf") +) + +// Windows group-SID attribute flags (winnt.h): a group only counts toward +// membership when it is enabled and not marked use-for-deny-only. +const ( + seGroupEnabled = 0x00000004 + seGroupUseForDenyOnly = 0x00000010 +) + +// DefaultPipeSDDL restricts the daemon control pipe to LocalSystem (SY), the +// Administrators group (BA), and interactive logon users (IU). It deliberately +// excludes Authenticated Users / Everyone so remote or arbitrary service +// principals cannot connect. This is the connection gate; the interceptor still +// does per-RPC authorization by caller identity. +func DefaultPipeSDDL() string { + return "D:P(A;;GA;;;SY)(A;;GA;;;BA)(A;;GA;;;IU)" +} + +// NewTransportCredentials returns gRPC transport credentials that derive the +// caller's identity from the named-pipe client token, following Microsoft's +// "Verifying Client Access with ACLs" pattern: ImpersonateNamedPipeClient -> +// OpenThreadToken -> RevertToSelf. Per threat-model M-NOIMP, impersonation is +// used only to read the client token for identity, never to perform privileged work. +func NewTransportCredentials() credentials.TransportCredentials { + return winpipeCreds{} +} + +type winpipeCreds struct{} + +func (winpipeCreds) ClientHandshake(_ context.Context, _ string, conn net.Conn) (net.Conn, credentials.AuthInfo, error) { + return conn, AuthInfo{}, nil +} + +// ServerHandshake extracts the connecting client's identity from the pipe token. +// Fails closed if the handle or token cannot be read. +func (winpipeCreds) ServerHandshake(conn net.Conn) (net.Conn, credentials.AuthInfo, error) { + // go-winio's pipe connection embeds *win32File, which exposes Fd(). + fdConn, ok := conn.(interface{ Fd() uintptr }) + if !ok { + return nil, nil, fmt.Errorf("connection %T does not expose a pipe handle", conn) + } + handle := windows.Handle(fdConn.Fd()) + + id, err := pipeClientIdentity(handle) + if err != nil { + return nil, nil, err + } + return conn, AuthInfo{ + CommonAuthInfo: credentials.CommonAuthInfo{SecurityLevel: credentials.NoSecurity}, + Identity: id, + }, nil +} + +func (winpipeCreds) Info() credentials.ProtocolInfo { + return credentials.ProtocolInfo{SecurityProtocol: "netbird-ipc-peercred"} +} + +func (winpipeCreds) Clone() credentials.TransportCredentials { return winpipeCreds{} } + +func (winpipeCreds) OverrideServerName(string) error { return nil } + +// pipeClientIdentity reads the connecting client's user SID, enabled group SIDs, +// and elevation from the named-pipe handle. The impersonation window is kept as +// small as possible and pinned to the OS thread (impersonation is thread-local). +func pipeClientIdentity(handle windows.Handle) (Identity, error) { + var pid uint32 + hasPID := windows.GetNamedPipeClientProcessId(handle, &pid) == nil + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + if err := impersonateNamedPipeClient(handle); err != nil { + return Identity{}, fmt.Errorf("impersonate named pipe client: %w", err) + } + defer func() { _ = revertToSelf() }() + + // openAsSelf=true: the token is opened using the daemon's process context + // (LocalSystem), not the impersonated client's, so the open always succeeds. + var token windows.Token + if err := windows.OpenThreadToken(windows.CurrentThread(), windows.TOKEN_QUERY, true, &token); err != nil { + return Identity{}, fmt.Errorf("open thread token: %w", err) + } + defer token.Close() + + tu, err := token.GetTokenUser() + if err != nil { + return Identity{}, fmt.Errorf("get token user: %w", err) + } + + tg, err := token.GetTokenGroups() + if err != nil { + return Identity{}, fmt.Errorf("get token groups: %w", err) + } + var groups []string + for _, g := range tg.AllGroups() { + if g.Attributes&seGroupEnabled == 0 || g.Attributes&seGroupUseForDenyOnly != 0 { + continue + } + groups = append(groups, g.Sid.String()) + } + + return Identity{ + SID: tu.User.Sid.String(), + Groups: groups, + Elevated: token.IsElevated(), + PID: int32(pid), + HasPID: hasPID, + }, nil +} + +func impersonateNamedPipeClient(h windows.Handle) error { + r, _, e := procImpersonateNamedPipeClient.Call(uintptr(h)) + if r == 0 { + return e + } + return nil +} + +func revertToSelf() error { + r, _, e := procRevertToSelf.Call() + if r == 0 { + return e + } + return nil +} diff --git a/client/internal/ipcauth/forward.go b/client/internal/ipcauth/forward.go new file mode 100644 index 000000000..b03de150d --- /dev/null +++ b/client/internal/ipcauth/forward.go @@ -0,0 +1,72 @@ +package ipcauth + +import ( + "context" + "strconv" + + "google.golang.org/grpc/metadata" +) + +// Metadata keys used by the local JSON gateway to forward the HTTP client's +// identity to the daemon. Trusted by the interceptor ONLY when the gRPC peer is +// itself the daemon (self/privileged) — i.e. the loopback gateway — so a direct +// gRPC caller cannot forge them. +const ( + mdFwdUID = "x-netbird-fwd-uid" + mdFwdGID = "x-netbird-fwd-gid" + mdFwdPID = "x-netbird-fwd-pid" +) + +// ForwardIdentityMetadata encodes a Unix identity for the gateway to forward to +// the daemon. Windows identities are not forwarded (the gateway cannot read a +// pipe token for an HTTP client); nil is returned in that case. +func ForwardIdentityMetadata(id Identity) metadata.MD { + if id.IsWindows() { + return nil + } + md := metadata.Pairs( + mdFwdUID, strconv.FormatUint(uint64(id.UID), 10), + mdFwdGID, strconv.FormatUint(uint64(id.GID), 10), + ) + if id.HasPID { + md.Set(mdFwdPID, strconv.FormatInt(int64(id.PID), 10)) + } + return md +} + +// forwardedIdentity extracts a forwarded Unix identity from incoming gRPC +// metadata, if present and well-formed. +func forwardedIdentity(ctx context.Context) (Identity, bool) { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return Identity{}, false + } + uidStr := mdFirst(md, mdFwdUID) + if uidStr == "" { + return Identity{}, false + } + uid, err := strconv.ParseUint(uidStr, 10, 32) + if err != nil { + return Identity{}, false + } + id := Identity{UID: uint32(uid)} + if g := mdFirst(md, mdFwdGID); g != "" { + if v, err := strconv.ParseUint(g, 10, 32); err == nil { + id.GID = uint32(v) + } + } + if p := mdFirst(md, mdFwdPID); p != "" { + if v, err := strconv.ParseInt(p, 10, 32); err == nil { + id.PID = int32(v) + id.HasPID = true + } + } + return id, true +} + +func mdFirst(md metadata.MD, key string) string { + if v := md.Get(key); len(v) > 0 { + return v[0] + } + return "" +} diff --git a/client/internal/ipcauth/identity.go b/client/internal/ipcauth/identity.go new file mode 100644 index 000000000..0260362c5 --- /dev/null +++ b/client/internal/ipcauth/identity.go @@ -0,0 +1,106 @@ +// Package ipcauth provides the kernel-authenticated identity of a local IPC +// (gRPC) caller and the transport credentials that surface it into the gRPC +// context, so the daemon can authorize each RPC by caller identity. +// +// On Unix the identity is read from the kernel via SO_PEERCRED (Linux) or +// LOCAL_PEERCRED (Darwin/FreeBSD). On Windows it is derived from the named-pipe +// client token. Platforms without a peer-identity primitive get no credentials +// and therefore no enforcement (the daemon logs a warning and stays open, +// preserving today's behavior until the transport gains an identity primitive). +package ipcauth + +import ( + "context" + "fmt" + + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/peer" +) + +// sidLocalSystem is the well-known Windows SID for the LocalSystem account. +const sidLocalSystem = "S-1-5-18" + +// Identity is the kernel-authenticated identity of a local IPC caller. The zero +// value is not a valid identity; obtain one via IdentityFromContext (which +// reports presence) or PeerIdentity. +type Identity struct { + // UID and GID are the caller's Unix user ID and primary group ID. + // Zero on Windows, where SID is authoritative instead. + UID uint32 + GID uint32 + + // PID is the caller's process ID, for audit only. HasPID is false when the + // platform cannot supply it (e.g. Darwin/FreeBSD xucred carries no PID). + PID int32 + HasPID bool + + // SID is the caller's Windows security identifier (empty on Unix). + SID string + + // Groups holds the caller's Windows group SIDs, captured from the client + // token at handshake (empty on Unix, where supplementary group membership is + // resolved on demand via NSS/getent by the authorizer). + Groups []string + + // Elevated reports whether the Windows client token is elevated (run as + // administrator). Always false on Unix, where privilege is uid==0. + Elevated bool +} + +// IsWindows reports whether this identity is a Windows principal (SID-based) +// rather than a Unix uid/gid principal. +func (i Identity) IsWindows() bool { + return i.SID != "" +} + +// IsPrivileged reports whether the caller is the platform's administrative +// principal — Unix root (uid 0), or on Windows an elevated token or LocalSystem. +// It deliberately requires actual elevation on Windows (a non-elevated member of +// Administrators has a filtered token and is NOT privileged), mirroring "must +// really be root" on Unix. +func (i Identity) IsPrivileged() bool { + if i.IsWindows() { + return i.Elevated || i.SID == sidLocalSystem + } + return i.UID == 0 +} + +// String renders the identity for audit logs. +func (i Identity) String() string { + if i.IsWindows() { + if i.HasPID { + return fmt.Sprintf("sid=%s elevated=%t pid=%d", i.SID, i.Elevated, i.PID) + } + return fmt.Sprintf("sid=%s elevated=%t", i.SID, i.Elevated) + } + if i.HasPID { + return fmt.Sprintf("uid=%d gid=%d pid=%d", i.UID, i.GID, i.PID) + } + return fmt.Sprintf("uid=%d gid=%d", i.UID, i.GID) +} + +// AuthInfo carries the peer Identity as a gRPC credentials.AuthInfo so the +// interceptor can retrieve it from the request context via IdentityFromContext. +type AuthInfo struct { + credentials.CommonAuthInfo + Identity Identity +} + +// AuthType identifies the authentication scheme. +func (AuthInfo) AuthType() string { return "netbird-ipc-peercred" } + +// IdentityFromContext extracts the caller's kernel-authenticated identity from +// the gRPC peer context. The second return value is false when no IPC transport +// credentials were negotiated (e.g. an unsupported platform, or a caller that +// did not come through the daemon socket) — callers MUST fail closed in that case. +func IdentityFromContext(ctx context.Context) (Identity, bool) { + p, ok := peer.FromContext(ctx) + if !ok { + return Identity{}, false + } + info, ok := p.AuthInfo.(AuthInfo) + if !ok { + return Identity{}, false + } + return info.Identity, true +} diff --git a/client/internal/ipcauth/identity_test.go b/client/internal/ipcauth/identity_test.go new file mode 100644 index 000000000..61ac84cfe --- /dev/null +++ b/client/internal/ipcauth/identity_test.go @@ -0,0 +1,50 @@ +package ipcauth + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/peer" +) + +func TestIdentityFromContext_NoPeer(t *testing.T) { + _, ok := IdentityFromContext(context.Background()) + assert.False(t, ok, "bare context must report no identity (fail closed)") +} + +func TestIdentityFromContext_WrongAuthInfo(t *testing.T) { + ctx := peer.NewContext(context.Background(), &peer.Peer{}) + _, ok := IdentityFromContext(ctx) + assert.False(t, ok, "peer without our AuthInfo must report no identity") +} + +func TestIdentityFromContext_Present(t *testing.T) { + want := Identity{UID: 1000, GID: 1000, PID: 4242, HasPID: true} + ctx := peer.NewContext(context.Background(), &peer.Peer{ + AuthInfo: AuthInfo{ + CommonAuthInfo: credentials.CommonAuthInfo{SecurityLevel: credentials.NoSecurity}, + Identity: want, + }, + }) + + got, ok := IdentityFromContext(ctx) + assert.True(t, ok) + assert.Equal(t, want, got) +} + +func TestIdentity_IsPrivileged(t *testing.T) { + // Unix + assert.True(t, Identity{UID: 0}.IsPrivileged(), "root is privileged") + assert.False(t, Identity{UID: 1000}.IsPrivileged(), "non-root is not privileged") + // Windows + assert.True(t, Identity{SID: "S-1-5-21-1-2-3-1001", Elevated: true}.IsPrivileged(), "elevated admin is privileged") + assert.True(t, Identity{SID: "S-1-5-18"}.IsPrivileged(), "LocalSystem is privileged") + assert.False(t, Identity{SID: "S-1-5-21-1-2-3-1001"}.IsPrivileged(), "non-elevated admin is NOT privileged") +} + +func TestIdentity_IsWindows(t *testing.T) { + assert.True(t, Identity{SID: "S-1-5-18"}.IsWindows()) + assert.False(t, Identity{UID: 0}.IsWindows()) +} diff --git a/client/internal/ipcauth/interceptor.go b/client/internal/ipcauth/interceptor.go new file mode 100644 index 000000000..f475eadc5 --- /dev/null +++ b/client/internal/ipcauth/interceptor.go @@ -0,0 +1,124 @@ +package ipcauth + +import ( + "context" + "os" + + log "github.com/sirupsen/logrus" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// Interceptor enforces per-RPC authorization on the daemon control channel, +// keyed to the caller's kernel-authenticated identity. It is safe-by-default: +// any RPC without a matching bypass is gated by the active profile's ownership, +// and a caller without a readable identity is denied. +type Interceptor struct { + policy ProfilePolicy + resolver GroupResolver + // selfUID is the daemon's own effective UID. A caller whose UID matches it + // (rootless container / foreground daemon running as the invoking user) is + // allowed: it already has full control of the daemon process. -1 on Windows. + selfUID int +} + +// NewInterceptor builds an interceptor over the given policy and group resolver. +func NewInterceptor(policy ProfilePolicy, resolver GroupResolver) *Interceptor { + return &Interceptor{policy: policy, resolver: resolver, selfUID: os.Geteuid()} +} + +// UnaryServerInterceptor authorizes each unary RPC before the handler runs. +func (i *Interceptor) UnaryServerInterceptor() 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) + } +} + +// StreamServerInterceptor authorizes each streaming RPC before the handler runs. +func (i *Interceptor) StreamServerInterceptor() 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) + } +} + +func (i *Interceptor) authorize(ctx context.Context, fullMethod string) error { + id, ok := IdentityFromContext(ctx) + if !ok { + log.Warnf("ipc authz: DENY %s — caller identity unavailable", fullMethod) + return status.Error(codes.PermissionDenied, "caller identity could not be verified on the daemon control channel") + } + + if i.isSelfOrPrivileged(id) { + // The local JSON gateway connects as the daemon itself (self/privileged) + // and forwards the real HTTP client's identity. Trust it here — and only + // here, where the transport peer is already the daemon — then authorize + // as the forwarded client. A direct non-privileged caller never reaches + // this branch, so it cannot forge the forwarding metadata. + fwd, hasFwd := forwardedIdentity(ctx) + if !hasFwd { + i.auditAllow(id, fullMethod) + return nil + } + log.Infof("ipc authz: honoring gateway-forwarded identity %s", fwd) + id = fwd + if i.isSelfOrPrivileged(id) { + i.auditAllow(id, fullMethod) + return nil + } + } + + // Per-user / per-target-profile RPCs authorize themselves in the handler. + if handlerAuthorizedMethods[fullMethod] { + return nil + } + + o := i.policy.ActiveProfileOwnership() + + // Trust-on-first-use: an unowned, non-shared profile is claimed by the first + // caller. The claim is atomic; if we lose the race we re-read and authorize. + if len(o.Owners) == 0 && !o.Shared { + claimed, err := i.policy.ClaimActiveProfileOwnerIfUnowned(id) + if err != nil { + log.Errorf("ipc authz: claim active profile for %s: %v", id, err) + return status.Error(codes.Internal, "failed to claim profile ownership") + } + if claimed { + log.Infof("ipc authz: %s claimed ownership of the active profile (trust-on-first-use)", id) + i.auditAllow(id, fullMethod) + return nil + } + o = i.policy.ActiveProfileOwnership() + } + + if Authorize(o, id, i.resolver) { + i.auditAllow(id, fullMethod) + return nil + } + + log.Warnf("ipc authz: DENY %s for %s — active profile owned by another principal", fullMethod, id) + return status.Errorf(codes.PermissionDenied, + "not authorized to control the active profile (caller %s); ask an owner or run as root/administrator", id) +} + +// isSelfOrPrivileged reports whether the caller is the platform administrator +// (root / elevated-admin / LocalSystem) or the daemon's own user. +func (i *Interceptor) isSelfOrPrivileged(id Identity) bool { + if id.IsPrivileged() { + return true + } + // Daemon-self: only meaningful on Unix (Windows privilege is covered above). + return !id.IsWindows() && i.selfUID >= 0 && int(id.UID) == i.selfUID +} + +func (i *Interceptor) auditAllow(id Identity, fullMethod string) { + if auditMethods[fullMethod] { + log.Infof("ipc authz: allow %s for %s", fullMethod, id) + } +} diff --git a/client/internal/ipcauth/interceptor_test.go b/client/internal/ipcauth/interceptor_test.go new file mode 100644 index 000000000..666f613cc --- /dev/null +++ b/client/internal/ipcauth/interceptor_test.go @@ -0,0 +1,139 @@ +package ipcauth + +import ( + "context" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/peer" + "google.golang.org/grpc/status" +) + +type mockPolicy struct { + o Ownership + claimed bool +} + +func (m *mockPolicy) ActiveProfileOwnership() Ownership { return m.o } + +// ClaimActiveProfileOwnerIfUnowned records a claim and marks the profile owned. +func (m *mockPolicy) ClaimActiveProfileOwnerIfUnowned(id Identity) (bool, error) { + if len(m.o.Owners) == 0 && !m.o.Shared { + m.o.Owners = []string{OwnerPrincipalForIdentity(id)} + m.claimed = true + return true, nil + } + return false, nil +} + +type mockResolver struct { + gids map[uint32]struct{} + names map[string]uint32 +} + +func (m mockResolver) CallerGIDs(Identity) map[uint32]struct{} { return m.gids } +func (m mockResolver) GroupNameGID(n string) (uint32, bool) { g, ok := m.names[n]; return g, ok } + +func ctxWith(id Identity) context.Context { + return peer.NewContext(context.Background(), &peer.Peer{AuthInfo: AuthInfo{Identity: id}}) +} + +const ( + up = servicePath + "Up" + list = servicePath + "ListProfiles" + unkwn = servicePath + "SomeFutureMethod" +) + +func TestInterceptorAuthorize(t *testing.T) { + const selfUID = 4000 + + tests := []struct { + name string + own Ownership + resolver GroupResolver + ctx context.Context + method string + wantErr bool + }{ + {"no identity denies", Ownership{}, nil, context.Background(), up, true}, + {"root allowed", Ownership{}, nil, ctxWith(Identity{UID: 0}), up, false}, + {"daemon-self allowed", Ownership{}, nil, ctxWith(Identity{UID: selfUID}), up, false}, + {"shared allows any", Ownership{Shared: true}, nil, ctxWith(Identity{UID: 1234}), up, false}, + {"uid owner allowed", Ownership{Owners: []string{"uid:1000"}}, nil, ctxWith(Identity{UID: 1000}), up, false}, + {"non-owner denied", Ownership{Owners: []string{"uid:1000"}}, nil, ctxWith(Identity{UID: 2000}), up, true}, + {"handler-authorized bypass", Ownership{Owners: []string{"uid:1000"}}, nil, ctxWith(Identity{UID: 2000}), list, false}, + {"unknown method gated", Ownership{Owners: []string{"uid:1000"}}, nil, ctxWith(Identity{UID: 2000}), unkwn, true}, + {"primary gid owner", Ownership{Owners: []string{"gid:5000"}}, nil, ctxWith(Identity{UID: 2000, GID: 5000}), up, false}, + {"group-name owner via resolver", Ownership{Owners: []string{"group:admins"}}, + mockResolver{names: map[string]uint32{"admins": 5000}, gids: map[uint32]struct{}{5000: {}}}, + ctxWith(Identity{UID: 2000, GID: 42}), up, false}, + {"windows sid owner", Ownership{Owners: []string{"sid:S-1-5-21-9"}}, nil, + ctxWith(Identity{SID: "S-1-5-21-9"}), up, false}, + {"windows group-sid owner", Ownership{Owners: []string{"sid:S-1-5-32-544"}}, nil, + ctxWith(Identity{SID: "S-1-5-21-1", Groups: []string{"S-1-5-32-544"}}), up, false}, + {"windows elevated privileged", Ownership{}, nil, + ctxWith(Identity{SID: "S-1-5-21-1", Elevated: true}), up, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + i := &Interceptor{policy: &mockPolicy{o: tt.own}, resolver: tt.resolver, selfUID: selfUID} + err := i.authorize(tt.ctx, tt.method) + if tt.wantErr { + assert.Error(t, err) + assert.Equal(t, codes.PermissionDenied, status.Code(err)) + } else { + assert.NoError(t, err) + } + }) + } +} + +// TestInterceptorForwardedIdentity verifies the JSON-gateway trust model: a +// self/privileged transport peer (the loopback gateway) may forward a real +// client identity, but a non-privileged caller cannot forge it. +func TestInterceptorForwardedIdentity(t *testing.T) { + const selfUID = 4000 + owners := Ownership{Owners: []string{"uid:1000"}} + + withFwd := func(peerUID, fwdUID uint32) context.Context { + ctx := ctxWith(Identity{UID: peerUID}) + return metadata.NewIncomingContext(ctx, metadata.Pairs(mdFwdUID, itoa(fwdUID))) + } + + // Gateway (peer == daemon-self) forwards a non-owner client → denied as that client. + i := &Interceptor{policy: &mockPolicy{o: owners}, selfUID: selfUID} + assert.Error(t, i.authorize(withFwd(selfUID, 2000), up)) + + // Gateway forwards the owner → allowed. + assert.NoError(t, i.authorize(withFwd(selfUID, 1000), up)) + + // A non-privileged direct caller's forwarded metadata is IGNORED (can't forge): + // caller uid 2000 forwarding uid:1000 is still treated as 2000 → denied. + assert.Error(t, i.authorize(withFwd(2000, 1000), up)) +} + +func itoa(u uint32) string { + return strconv.FormatUint(uint64(u), 10) +} + +// TestInterceptorTOFU verifies an unowned, non-shared profile is claimed by the +// first non-privileged caller, and a different caller is then denied. +func TestInterceptorTOFU(t *testing.T) { + policy := &mockPolicy{o: Ownership{}} // unowned + i := &Interceptor{policy: policy, resolver: nil, selfUID: 4000} + + // First caller (uid 1000) claims via TOFU. + err := i.authorize(ctxWith(Identity{UID: 1000}), up) + assert.NoError(t, err) + assert.True(t, policy.claimed, "first caller should claim ownership") + assert.Equal(t, []string{"uid:1000"}, policy.o.Owners) + + // A different caller is now denied (profile owned by uid 1000). + err = i.authorize(ctxWith(Identity{UID: 2000}), up) + assert.Error(t, err) + assert.Equal(t, codes.PermissionDenied, status.Code(err)) +} diff --git a/client/internal/ipcauth/peercred_bsd.go b/client/internal/ipcauth/peercred_bsd.go new file mode 100644 index 000000000..178c7a17e --- /dev/null +++ b/client/internal/ipcauth/peercred_bsd.go @@ -0,0 +1,43 @@ +//go:build darwin || freebsd + +package ipcauth + +import ( + "fmt" + "net" + + "golang.org/x/sys/unix" +) + +// PeerIdentity reads the kernel-authenticated identity of the process on the +// other end of a Unix socket connection via LOCAL_PEERCRED (xucred). xucred +// carries the uid and group list but no pid, so audit on these platforms is +// uid/gid-based (HasPID is false); PID via LOCAL_PEERPID is a possible follow-up. +func PeerIdentity(c net.Conn) (Identity, error) { + uc, ok := c.(*net.UnixConn) + if !ok { + return Identity{}, fmt.Errorf("connection is not a unix socket: %T", c) + } + raw, err := uc.SyscallConn() + if err != nil { + return Identity{}, fmt.Errorf("raw conn: %w", err) + } + + var cred *unix.Xucred + var credErr error + if err := raw.Control(func(fd uintptr) { + cred, credErr = unix.GetsockoptXucred(int(fd), unix.SOL_LOCAL, unix.LOCAL_PEERCRED) + }); err != nil { + return Identity{}, fmt.Errorf("getsockopt control: %w", err) + } + if credErr != nil { + return Identity{}, fmt.Errorf("LOCAL_PEERCRED: %w", credErr) + } + + id := Identity{UID: cred.Uid} + // Groups[0] is the effective (primary) GID; guard against an empty list. + if cred.Ngroups > 0 { + id.GID = cred.Groups[0] + } + return id, nil +} diff --git a/client/internal/ipcauth/peercred_linux.go b/client/internal/ipcauth/peercred_linux.go new file mode 100644 index 000000000..8c9e01b8e --- /dev/null +++ b/client/internal/ipcauth/peercred_linux.go @@ -0,0 +1,43 @@ +//go:build linux + +package ipcauth + +import ( + "fmt" + "net" + + "golang.org/x/sys/unix" +) + +// PeerIdentity reads the kernel-authenticated identity of the process on the +// other end of a Unix socket connection via SO_PEERCRED. The credentials are +// captured by the kernel at connect() time and cannot be spoofed or changed for +// the life of the connection. +func PeerIdentity(c net.Conn) (Identity, error) { + uc, ok := c.(*net.UnixConn) + if !ok { + return Identity{}, fmt.Errorf("connection is not a unix socket: %T", c) + } + raw, err := uc.SyscallConn() + if err != nil { + return Identity{}, fmt.Errorf("raw conn: %w", err) + } + + var cred *unix.Ucred + var credErr error + if err := raw.Control(func(fd uintptr) { + cred, credErr = unix.GetsockoptUcred(int(fd), unix.SOL_SOCKET, unix.SO_PEERCRED) + }); err != nil { + return Identity{}, fmt.Errorf("getsockopt control: %w", err) + } + if credErr != nil { + return Identity{}, fmt.Errorf("SO_PEERCRED: %w", credErr) + } + + return Identity{ + UID: cred.Uid, + GID: cred.Gid, + PID: cred.Pid, + HasPID: true, + }, nil +} diff --git a/client/internal/ipcauth/peercred_stub.go b/client/internal/ipcauth/peercred_stub.go new file mode 100644 index 000000000..170af1555 --- /dev/null +++ b/client/internal/ipcauth/peercred_stub.go @@ -0,0 +1,16 @@ +//go:build !linux && !darwin && !freebsd + +package ipcauth + +import ( + "fmt" + "net" + "runtime" +) + +// PeerIdentity is unimplemented on platforms without a Unix-socket peer-credential +// primitive. Windows derives identity from the named-pipe client token instead +// (see the Windows transport credentials), so it never calls this. +func PeerIdentity(net.Conn) (Identity, error) { + return Identity{}, fmt.Errorf("peer credential check not supported on %s", runtime.GOOS) +} diff --git a/client/internal/ipcauth/peercred_unix_test.go b/client/internal/ipcauth/peercred_unix_test.go new file mode 100644 index 000000000..ee39f2a5b --- /dev/null +++ b/client/internal/ipcauth/peercred_unix_test.go @@ -0,0 +1,114 @@ +//go:build linux || darwin || freebsd + +package ipcauth + +import ( + "context" + "net" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/health" + healthpb "google.golang.org/grpc/health/grpc_health_v1" +) + +// TestPeerIdentity_MatchesCurrentProcess connects to a real Unix socket and +// verifies the extracted UID/GID match the running process (both ends are us). +func TestPeerIdentity_MatchesCurrentProcess(t *testing.T) { + sock := filepath.Join(t.TempDir(), "peer.sock") + ln, err := net.Listen("unix", sock) + require.NoError(t, err) + t.Cleanup(func() { _ = ln.Close() }) + + type result struct { + id Identity + err error + } + done := make(chan result, 1) + go func() { + c, aerr := ln.Accept() + if aerr != nil { + done <- result{err: aerr} + return + } + defer func() { _ = c.Close() }() + id, ierr := PeerIdentity(c) + done <- result{id: id, err: ierr} + }() + + client, err := net.Dial("unix", sock) + require.NoError(t, err) + t.Cleanup(func() { _ = client.Close() }) + + res := <-done + require.NoError(t, res.err) + assert.Equal(t, uint32(os.Getuid()), res.id.UID, "UID should match current process") + assert.Equal(t, uint32(os.Getgid()), res.id.GID, "primary GID should match current process") +} + +// TestPeerIdentity_NonUnixConn rejects non-Unix connections (fail closed). +func TestPeerIdentity_NonUnixConn(t *testing.T) { + ln, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + t.Cleanup(func() { _ = ln.Close() }) + + done := make(chan error, 1) + go func() { + c, aerr := ln.Accept() + if aerr != nil { + done <- aerr + return + } + defer func() { _ = c.Close() }() + _, ierr := PeerIdentity(c) + done <- ierr + }() + + client, err := net.Dial("tcp", ln.Addr().String()) + require.NoError(t, err) + t.Cleanup(func() { _ = client.Close() }) + + assert.Error(t, <-done, "PeerIdentity must reject a non-Unix connection") +} + +// TestGRPCRoundTrip_ServerCredsClientInsecure proves the transport contract end +// to end: a gRPC server using the peercred transport credentials still serves a +// plain insecure client (the CLI never changed), and the caller's kernel +// identity reaches the handler via IdentityFromContext. +func TestGRPCRoundTrip_ServerCredsClientInsecure(t *testing.T) { + sock := filepath.Join(t.TempDir(), "rt.sock") + ln, err := net.Listen("unix", sock) + require.NoError(t, err) + + var gotUID uint32 + var gotOK bool + srv := grpc.NewServer( + grpc.Creds(NewTransportCredentials()), + grpc.UnaryInterceptor(func(ctx context.Context, req any, _ *grpc.UnaryServerInfo, h grpc.UnaryHandler) (any, error) { + id, ok := IdentityFromContext(ctx) + gotUID, gotOK = id.UID, ok + return h(ctx, req) + }), + ) + healthpb.RegisterHealthServer(srv, health.NewServer()) + go func() { _ = srv.Serve(ln) }() + t.Cleanup(srv.Stop) + + conn, err := grpc.NewClient("unix://"+sock, grpc.WithTransportCredentials(insecure.NewCredentials())) + require.NoError(t, err) + t.Cleanup(func() { _ = conn.Close() }) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + _, err = healthpb.NewHealthClient(conn).Check(ctx, &healthpb.HealthCheckRequest{}) + require.NoError(t, err, "insecure client must reach the peercred server") + + assert.True(t, gotOK, "handler must see a peer identity") + assert.Equal(t, uint32(os.Getuid()), gotUID, "handler must see the caller's UID") +} diff --git a/client/internal/ipcauth/policy.go b/client/internal/ipcauth/policy.go new file mode 100644 index 000000000..ac5940fbf --- /dev/null +++ b/client/internal/ipcauth/policy.go @@ -0,0 +1,92 @@ +package ipcauth + +import "sync" + +const servicePath = "/daemon.DaemonService/" + +// ProfilePolicy exposes the active profile's ownership to the interceptor. The +// daemon server implements it; ConfigAdapter bridges the gap because the gRPC +// server (and its interceptor) is constructed before the server instance exists. +type ProfilePolicy interface { + // ActiveProfileOwnership returns the active profile's ownership policy. + ActiveProfileOwnership() Ownership + + // ClaimActiveProfileOwnerIfUnowned atomically claims the active profile for + // id when it has no owners and is not shared (trust-on-first-use), and + // reports whether id is now an owner. A false return means the profile was + // already owned/shared or another caller won the claim — the caller must + // re-read ownership and authorize normally. + ClaimActiveProfileOwnerIfUnowned(id Identity) (bool, error) +} + +// handlerAuthorizedMethods bypass the active-profile gate: they are per-user or +// per-target-profile operations whose handler does its own authorization (bound +// to the caller identity). Peer identity is still required to reach them. +var handlerAuthorizedMethods = map[string]bool{ + servicePath + "AddProfile": true, + servicePath + "ListProfiles": true, + servicePath + "GetActiveProfile": true, + servicePath + "RemoveProfile": true, + servicePath + "RenameProfile": true, +} + +// auditMethods are the Tier-C/H RPCs (threat model §3) whose successful +// authorization is worth an audit log line. Denials are always logged. +var auditMethods = map[string]bool{ + servicePath + "GetConfig": true, + servicePath + "SetConfig": true, + servicePath + "Login": true, + servicePath + "WaitSSOLogin": true, + servicePath + "RequestJWTAuth": true, + servicePath + "WaitJWTToken": true, + servicePath + "StartCapture": true, + servicePath + "StartBundleCapture": true, + servicePath + "DebugBundle": true, + servicePath + "ExposeService": true, + servicePath + "Up": true, + servicePath + "Down": true, + servicePath + "SelectNetworks": true, + servicePath + "DeselectNetworks": true, + servicePath + "SwitchProfile": true, + servicePath + "TriggerUpdate": true, + servicePath + "Logout": true, + servicePath + "CleanState": true, + servicePath + "DeleteState": true, +} + +// ConfigAdapter is a ProfilePolicy whose backend is set lazily, once the daemon +// server instance is created. Until then it reports an unowned profile +// (Ownership zero value), so non-privileged callers are denied — fail closed. +type ConfigAdapter struct { + mu sync.RWMutex + backend ProfilePolicy +} + +// SetBackend installs the real policy. Must be called before serving RPCs. +func (a *ConfigAdapter) SetBackend(backend ProfilePolicy) { + a.mu.Lock() + defer a.mu.Unlock() + a.backend = backend +} + +// ActiveProfileOwnership delegates to the backend, or reports an unowned profile +// when no backend is set yet. +func (a *ConfigAdapter) ActiveProfileOwnership() Ownership { + a.mu.RLock() + defer a.mu.RUnlock() + if a.backend == nil { + return Ownership{} + } + return a.backend.ActiveProfileOwnership() +} + +// ClaimActiveProfileOwnerIfUnowned delegates to the backend. Before the backend +// is set it cannot claim, so it reports not-owned (fail closed). +func (a *ConfigAdapter) ClaimActiveProfileOwnerIfUnowned(id Identity) (bool, error) { + a.mu.RLock() + defer a.mu.RUnlock() + if a.backend == nil { + return false, nil + } + return a.backend.ClaimActiveProfileOwnerIfUnowned(id) +} diff --git a/client/internal/ipcauth/principal.go b/client/internal/ipcauth/principal.go new file mode 100644 index 000000000..785168ba0 --- /dev/null +++ b/client/internal/ipcauth/principal.go @@ -0,0 +1,55 @@ +package ipcauth + +import ( + "strconv" + "strings" +) + +// PrincipalKind is the type of an owner principal. +type PrincipalKind string + +const ( + KindUID PrincipalKind = "uid" // Unix user ID + KindGID PrincipalKind = "gid" // Unix group ID + KindGroup PrincipalKind = "group" // Unix group name (NSS-resolved) + KindSID PrincipalKind = "sid" // Windows user or group SID +) + +// Principal is a parsed owner entry from a profile's Owners list. +type Principal struct { + Kind PrincipalKind + Value string +} + +// ParsePrincipal parses a "kind:value" owner string. Returns false for empty +// values or unknown kinds so malformed entries are ignored rather than trusted. +func ParsePrincipal(s string) (Principal, bool) { + kind, value, ok := strings.Cut(s, ":") + if !ok || value == "" { + return Principal{}, false + } + switch PrincipalKind(kind) { + case KindUID, KindGID, KindGroup, KindSID: + return Principal{Kind: PrincipalKind(kind), Value: value}, true + default: + return Principal{}, false + } +} + +// UIDPrincipal builds the owner string for a Unix user ID. +func UIDPrincipal(uid uint32) string { + return string(KindUID) + ":" + strconv.FormatUint(uint64(uid), 10) +} + +// SIDPrincipal builds the owner string for a Windows SID. +func SIDPrincipal(sid string) string { return string(KindSID) + ":" + sid } + +// OwnerPrincipalForIdentity returns the self-ownership principal for an identity: +// the user's UID on Unix, or the user's SID on Windows. Used to auto-isolate a +// new profile to its creator. +func OwnerPrincipalForIdentity(id Identity) string { + if id.IsWindows() { + return SIDPrincipal(id.SID) + } + return UIDPrincipal(id.UID) +} diff --git a/client/internal/ipcauth/resolver_unix.go b/client/internal/ipcauth/resolver_unix.go new file mode 100644 index 000000000..e480c55d5 --- /dev/null +++ b/client/internal/ipcauth/resolver_unix.go @@ -0,0 +1,72 @@ +//go:build !windows + +package ipcauth + +import ( + "strconv" + "sync" + "time" + + "github.com/netbirdio/netbird/client/internal/shell" +) + +const groupCacheTTL = 30 * time.Second + +// NewDefaultGroupResolver returns an NSS-aware group resolver backed by +// getent/`id -G` (via client/internal/shell), so `gid:`/`group:` owners resolve +// correctly for LDAP/AD users under CGO_ENABLED=0. Results are cached briefly. +func NewDefaultGroupResolver() GroupResolver { + return &nssResolver{byUID: make(map[uint32]gidCacheEntry)} +} + +type gidCacheEntry struct { + gids map[uint32]struct{} + at time.Time +} + +type nssResolver struct { + mu sync.Mutex + byUID map[uint32]gidCacheEntry +} + +func (r *nssResolver) CallerGIDs(id Identity) map[uint32]struct{} { + r.mu.Lock() + defer r.mu.Unlock() + + if e, ok := r.byUID[id.UID]; ok && time.Since(e.at) < groupCacheTTL { + return e.gids + } + gids := resolveGIDs(id.UID) + r.byUID[id.UID] = gidCacheEntry{gids: gids, at: time.Now()} + return gids +} + +func resolveGIDs(uid uint32) map[uint32]struct{} { + out := make(map[uint32]struct{}) + u, err := shell.GetUserFromGetent(strconv.FormatUint(uint64(uid), 10)) + if err != nil { + return out + } + ids, err := shell.GroupIdsWithFallback(u) + if err != nil { + return out + } + for _, s := range ids { + if g, err := strconv.ParseUint(s, 10, 32); err == nil { + out[uint32(g)] = struct{}{} + } + } + return out +} + +func (r *nssResolver) GroupNameGID(name string) (uint32, bool) { + g, err := shell.LookupGroupWithGetent(name) + if err != nil { + return 0, false + } + gid, err := strconv.ParseUint(g.Gid, 10, 32) + if err != nil { + return 0, false + } + return uint32(gid), true +} diff --git a/client/internal/ipcauth/resolver_windows.go b/client/internal/ipcauth/resolver_windows.go new file mode 100644 index 000000000..7517ad943 --- /dev/null +++ b/client/internal/ipcauth/resolver_windows.go @@ -0,0 +1,10 @@ +//go:build windows + +package ipcauth + +// NewDefaultGroupResolver returns nil on Windows: group authorization uses the +// group SIDs carried in the client token (see the Windows transport +// credentials), not NSS/getent. +func NewDefaultGroupResolver() GroupResolver { + return nil +} diff --git a/client/internal/profilemanager/config.go b/client/internal/profilemanager/config.go index ed2f21999..cec6edf44 100644 --- a/client/internal/profilemanager/config.go +++ b/client/internal/profilemanager/config.go @@ -102,6 +102,11 @@ type ConfigInput struct { DNSLabels domain.List MTU *uint16 + + // Owners replaces the profile's owner principal list when non-nil. + // Shared replaces the profile's shared flag when non-nil. + Owners []string + Shared *bool } // Config Configuration type @@ -184,6 +189,17 @@ type Config struct { MTU uint16 + // Owners lists the principals allowed to control this profile over the local + // IPC, as typed strings: "uid:1000", "gid:1000", "group:netbird-admins" + // (Unix, NSS-resolved) or "sid:S-1-5-..." (Windows user or group SID). Empty + // with Shared=false means the profile is owned by nobody yet (privileged + + // daemon-self only, until claimed). Interpreted by client/internal/ipcauth. + Owners []string `json:"Owners,omitempty"` + + // Shared, when true, lets any authenticated local caller control this profile + // (opt-in). Takes precedence over Owners. + Shared bool `json:"Shared,omitempty"` + // policy is the MDM policy that produced the currently-set values for // any MDM-enforced fields. Set by applyMDMPolicy at the tail of apply() // and reset on every apply() invocation. Never persisted to disk. @@ -642,6 +658,18 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) { updated = true } + if input.Owners != nil && !slices.Equal(config.Owners, input.Owners) { + log.Infof("updating profile owners to %v", input.Owners) + config.Owners = input.Owners + updated = true + } + + if input.Shared != nil && *input.Shared != config.Shared { + log.Infof("updating profile shared flag to %t", *input.Shared) + config.Shared = *input.Shared + updated = true + } + // MDM is the last override layer: any key present in the policy // supersedes defaults, on-disk config, env vars and CLI input. config.applyMDMPolicy(loadMDMPolicy()) diff --git a/client/internal/profilemanager/service.go b/client/internal/profilemanager/service.go index 696a60310..3a4ac5fc1 100644 --- a/client/internal/profilemanager/service.go +++ b/client/internal/profilemanager/service.go @@ -296,7 +296,7 @@ func (s *ServiceManager) DefaultProfilePath() string { // The returned Profile carries the freshly-generated ID so callers can // show it to the user (and so the gRPC AddProfileResponse can include // it). -func (s *ServiceManager) AddProfile(displayName, username string) (*Profile, error) { +func (s *ServiceManager) AddProfile(displayName, username string, owners []string) (*Profile, error) { configDir, err := s.getConfigDir(username) if err != nil { return nil, fmt.Errorf("failed to get config directory: %w", err) @@ -318,6 +318,9 @@ func (s *ServiceManager) AddProfile(displayName, username string) (*Profile, err return nil, fmt.Errorf("failed to create new config: %w", err) } cfg.Name = displayName + // owners auto-isolates the new profile to its creator (nil = unowned, so the + // eventual first caller claims it via trust-on-first-use). + cfg.Owners = owners if err := util.WriteJson(context.Background(), profPath, cfg); err != nil { return nil, fmt.Errorf("failed to write profile config: %w", err) diff --git a/client/internal/profilemanager/service_test.go b/client/internal/profilemanager/service_test.go index 5e051b15d..09251119a 100644 --- a/client/internal/profilemanager/service_test.go +++ b/client/internal/profilemanager/service_test.go @@ -32,7 +32,7 @@ func withTestSM(t *testing.T, fn func(sm *ServiceManager, username string)) { func TestServiceProfile_ExactID(t *testing.T) { withTestSM(t, func(sm *ServiceManager, username string) { - created, err := sm.AddProfile("work", username) + created, err := sm.AddProfile("work", username, nil) require.NoError(t, err) got, err := sm.ResolveProfile(created.ID.String(), username) @@ -44,7 +44,7 @@ func TestServiceProfile_ExactID(t *testing.T) { func TestServiceProfile_IDPrefix(t *testing.T) { withTestSM(t, func(sm *ServiceManager, username string) { - created, err := sm.AddProfile("work", username) + created, err := sm.AddProfile("work", username, nil) require.NoError(t, err) prefix := created.ID[:4] @@ -75,7 +75,7 @@ func TestServiceProfile_AmbiguousPrefix(t *testing.T) { func TestServiceProfile_ExactNameUnique(t *testing.T) { withTestSM(t, func(sm *ServiceManager, username string) { - _, err := sm.AddProfile("work", username) + _, err := sm.AddProfile("work", username, nil) require.NoError(t, err) got, err := sm.ResolveProfile("work", username) @@ -86,9 +86,9 @@ func TestServiceProfile_ExactNameUnique(t *testing.T) { func TestServiceProfile_AmbiguousName(t *testing.T) { withTestSM(t, func(sm *ServiceManager, username string) { - _, err := sm.AddProfile("work", username) + _, err := sm.AddProfile("work", username, nil) require.NoError(t, err) - _, err = sm.AddProfile("work", username) + _, err = sm.AddProfile("work", username, nil) require.NoError(t, err) _, err = sm.ResolveProfile("work", username) @@ -133,10 +133,10 @@ func TestServiceProfile_LegacyFilenameCoexists(t *testing.T) { func TestAddProfile_AllowsDuplicateWithFlag(t *testing.T) { withTestSM(t, func(sm *ServiceManager, username string) { - first, err := sm.AddProfile("work", username) + first, err := sm.AddProfile("work", username, nil) require.NoError(t, err) - second, err := sm.AddProfile("work", username) + second, err := sm.AddProfile("work", username, nil) require.NoError(t, err) assert.NotEqual(t, first.ID, second.ID) assert.Equal(t, "work", second.Name) @@ -151,7 +151,7 @@ func TestAddProfile_RejectsInvalidNames(t *testing.T) { strings.Repeat("a", maxProfileNameLen+1), // too long } for _, name := range cases { - _, err := sm.AddProfile(name, username) + _, err := sm.AddProfile(name, username, nil) assert.Error(t, err, "expected error for %q", name) } }) @@ -215,7 +215,7 @@ func TestIsValidProfileFilenameStem(t *testing.T) { func TestRemoveProfile_DeletesStateFile(t *testing.T) { withTestSM(t, func(sm *ServiceManager, username string) { - created, err := sm.AddProfile("work", username) + created, err := sm.AddProfile("work", username, nil) require.NoError(t, err) configDir, err := sm.getConfigDir(username) diff --git a/client/internal/shell/getent_cgo_unix.go b/client/internal/shell/getent_cgo_unix.go new file mode 100644 index 000000000..65d6ce4cc --- /dev/null +++ b/client/internal/shell/getent_cgo_unix.go @@ -0,0 +1,29 @@ +//go:build cgo && !osusergo && !windows + +package shell + +import "os/user" + +// LookupWithGetent with CGO delegates directly to os/user.Lookup. +// When CGO is enabled, os/user uses libc (getpwnam_r) which goes through +// the NSS stack natively. If it fails, the user truly doesn't exist and +// getent would also fail. +func LookupWithGetent(username string) (*user.User, error) { + return user.Lookup(username) +} + +// CurrentUserWithGetent with CGO delegates directly to os/user.Current. +func CurrentUserWithGetent() (*user.User, error) { + return user.Current() +} + +// LookupGroupWithGetent returns the resolved group from either a gid or groupname. +func LookupGroupWithGetent(name string) (*user.Group, error) { + return user.LookupGroup(name) +} + +// GroupIdsWithFallback with CGO delegates directly to user.GroupIds. +// libc's getgrouplist handles NSS groups natively. +func GroupIdsWithFallback(u *user.User) ([]string, error) { + return u.GroupIds() +} diff --git a/client/ssh/server/getent_nocgo_unix.go b/client/internal/shell/getent_nocgo_unix.go similarity index 56% rename from client/ssh/server/getent_nocgo_unix.go rename to client/internal/shell/getent_nocgo_unix.go index 314daae4c..21151fb69 100644 --- a/client/ssh/server/getent_nocgo_unix.go +++ b/client/internal/shell/getent_nocgo_unix.go @@ -1,6 +1,6 @@ //go:build (!cgo || osusergo) && !windows -package server +package shell import ( "os" @@ -10,10 +10,10 @@ import ( log "github.com/sirupsen/logrus" ) -// lookupWithGetent looks up a user by name, falling back to getent if os/user fails. +// LookupWithGetent looks up a user by name, falling back to getent if os/user fails. // Without CGO, os/user only reads /etc/passwd and misses NSS-provided users. // getent goes through the host's NSS stack. -func lookupWithGetent(username string) (*user.User, error) { +func LookupWithGetent(username string) (*user.User, error) { u, err := user.Lookup(username) if err == nil { return u, nil @@ -22,7 +22,7 @@ func lookupWithGetent(username string) (*user.User, error) { stdErr := err log.Debugf("os/user.Lookup(%q) failed, trying getent: %v", username, err) - u, _, getentErr := runGetent(username) + u, _, getentErr := runGetentPasswd(username) if getentErr != nil { log.Debugf("getent fallback for %q also failed: %v", username, getentErr) return nil, stdErr @@ -31,8 +31,26 @@ func lookupWithGetent(username string) (*user.User, error) { return u, nil } -// currentUserWithGetent gets the current user, falling back to getent if os/user fails. -func currentUserWithGetent() (*user.User, error) { +// LookupGroupWithGetent returns the resolved group from either a gid or groupname, +// falling back to getent if os/user fails (NSS groups under nocgo). +func LookupGroupWithGetent(name string) (*user.Group, error) { + g, err := user.LookupGroup(name) + if err == nil { + return g, nil + } + + stdErr := err + log.Debugf("os/user.LookupGroup(%q) failed, trying getent: %v", name, err) + g, getentErr := runGetentGroup(name) + if getentErr != nil { + log.Debugf("getent fallback for %q also failed: %v", name, getentErr) + return nil, stdErr + } + return g, nil +} + +// CurrentUserWithGetent gets the current user, falling back to getent if os/user fails. +func CurrentUserWithGetent() (*user.User, error) { u, err := user.Current() if err == nil { return u, nil @@ -42,7 +60,7 @@ func currentUserWithGetent() (*user.User, error) { uid := strconv.Itoa(os.Getuid()) log.Debugf("os/user.Current() failed, trying getent with UID %s: %v", uid, err) - u, _, getentErr := runGetent(uid) + u, _, getentErr := runGetentPasswd(uid) if getentErr != nil { return nil, stdErr } @@ -50,14 +68,14 @@ func currentUserWithGetent() (*user.User, error) { return u, nil } -// groupIdsWithFallback gets group IDs for a user via the id command first, +// GroupIdsWithFallback gets group IDs for a user via the id command first, // falling back to user.GroupIds(). -// NOTE: unlike lookupWithGetent/currentUserWithGetent which try stdlib first, +// NOTE: unlike LookupWithGetent/CurrentUserWithGetent which try stdlib first, // this intentionally tries `id -G` first because without CGO, user.GroupIds() // only reads /etc/group and silently returns incomplete results for NSS users // (no error, just missing groups). The id command goes through NSS and returns // the full set. -func groupIdsWithFallback(u *user.User) ([]string, error) { +func GroupIdsWithFallback(u *user.User) ([]string, error) { ids, err := runIdGroups(u.Username) if err == nil { return ids, nil diff --git a/client/ssh/server/getent_test.go b/client/internal/shell/getent_test.go similarity index 75% rename from client/ssh/server/getent_test.go rename to client/internal/shell/getent_test.go index 5eac2fdbe..8d6ccbd8d 100644 --- a/client/ssh/server/getent_test.go +++ b/client/internal/shell/getent_test.go @@ -1,4 +1,4 @@ -package server +package shell import ( "os/user" @@ -15,7 +15,7 @@ func TestLookupWithGetent_CurrentUser(t *testing.T) { current, err := user.Current() require.NoError(t, err) - u, err := lookupWithGetent(current.Username) + u, err := LookupWithGetent(current.Username) require.NoError(t, err) assert.Equal(t, current.Username, u.Username) assert.Equal(t, current.Uid, u.Uid) @@ -23,7 +23,7 @@ func TestLookupWithGetent_CurrentUser(t *testing.T) { } func TestLookupWithGetent_NonexistentUser(t *testing.T) { - _, err := lookupWithGetent("nonexistent_user_xyzzy_12345") + _, err := LookupWithGetent("nonexistent_user_xyzzy_12345") require.Error(t, err, "should fail for nonexistent user") } @@ -31,7 +31,7 @@ func TestCurrentUserWithGetent(t *testing.T) { stdUser, err := user.Current() require.NoError(t, err) - u, err := currentUserWithGetent() + u, err := CurrentUserWithGetent() require.NoError(t, err) assert.Equal(t, stdUser.Uid, u.Uid) assert.Equal(t, stdUser.Username, u.Username) @@ -41,7 +41,7 @@ func TestGroupIdsWithFallback_CurrentUser(t *testing.T) { current, err := user.Current() require.NoError(t, err) - groups, err := groupIdsWithFallback(current) + groups, err := GroupIdsWithFallback(current) require.NoError(t, err) require.NotEmpty(t, groups, "current user should have at least one group") @@ -56,7 +56,7 @@ func TestGroupIdsWithFallback_CurrentUser(t *testing.T) { func TestGetShellFromGetent_CurrentUser(t *testing.T) { if runtime.GOOS == "windows" { // Windows stub always returns empty, which is correct - shell := getShellFromGetent("1000") + shell := GetShellFromGetent("1000") assert.Empty(t, shell, "Windows stub should return empty") return } @@ -65,9 +65,9 @@ func TestGetShellFromGetent_CurrentUser(t *testing.T) { require.NoError(t, err) // getent may not be available on all systems (e.g., macOS without Homebrew getent) - shell := getShellFromGetent(current.Uid) + shell := GetShellFromGetent(current.Uid) if shell == "" { - t.Log("getShellFromGetent returned empty, getent may not be available") + t.Log("GetShellFromGetent returned empty, getent may not be available") return } assert.True(t, shell[0] == '/', "shell should be an absolute path, got %q", shell) @@ -78,7 +78,7 @@ func TestLookupWithGetent_RootUser(t *testing.T) { t.Skip("no root user on Windows") } - u, err := lookupWithGetent("root") + u, err := LookupWithGetent("root") if err != nil { t.Skip("root user not available on this system") } @@ -86,25 +86,25 @@ func TestLookupWithGetent_RootUser(t *testing.T) { } // TestIntegration_FullLookupChain exercises the complete user lookup chain -// against the real system, testing that all wrappers (lookupWithGetent, -// currentUserWithGetent, groupIdsWithFallback, getShellFromGetent) produce +// against the real system, testing that all wrappers (LookupWithGetent, +// CurrentUserWithGetent, GroupIdsWithFallback, GetShellFromGetent) produce // consistent and correct results when composed together. func TestIntegration_FullLookupChain(t *testing.T) { - // Step 1: currentUserWithGetent must resolve the running user. - current, err := currentUserWithGetent() - require.NoError(t, err, "currentUserWithGetent must resolve the running user") + // Step 1: CurrentUserWithGetent must resolve the running user. + current, err := CurrentUserWithGetent() + require.NoError(t, err, "CurrentUserWithGetent must resolve the running user") require.NotEmpty(t, current.Uid) require.NotEmpty(t, current.Username) - // Step 2: lookupWithGetent by the same username must return matching identity. - byName, err := lookupWithGetent(current.Username) + // Step 2: LookupWithGetent by the same username must return matching identity. + byName, err := LookupWithGetent(current.Username) require.NoError(t, err) assert.Equal(t, current.Uid, byName.Uid, "lookup by name should return same UID") assert.Equal(t, current.Gid, byName.Gid, "lookup by name should return same GID") assert.Equal(t, current.HomeDir, byName.HomeDir, "lookup by name should return same home") - // Step 3: groupIdsWithFallback must return at least the primary GID. - groups, err := groupIdsWithFallback(current) + // Step 3: GroupIdsWithFallback must return at least the primary GID. + groups, err := GroupIdsWithFallback(current) require.NoError(t, err) require.NotEmpty(t, groups, "user must have at least one group") @@ -120,10 +120,10 @@ func TestIntegration_FullLookupChain(t *testing.T) { } assert.True(t, foundPrimary, "primary GID %s should appear in supplementary groups", current.Gid) - // Step 4: getShellFromGetent should either return a valid shell path or empty + // Step 4: GetShellFromGetent should either return a valid shell path or empty // (empty is OK when getent is not available, e.g. macOS without Homebrew getent). if runtime.GOOS != "windows" { - shell := getShellFromGetent(current.Uid) + shell := GetShellFromGetent(current.Uid) if shell != "" { assert.True(t, shell[0] == '/', "shell should be an absolute path, got %q", shell) } @@ -131,17 +131,17 @@ func TestIntegration_FullLookupChain(t *testing.T) { } // TestIntegration_LookupAndGroupsConsistency verifies that a user resolved via -// lookupWithGetent can have their groups resolved via groupIdsWithFallback, +// LookupWithGetent can have their groups resolved via GroupIdsWithFallback, // testing the handoff between the two functions as used by the SSH server. func TestIntegration_LookupAndGroupsConsistency(t *testing.T) { current, err := user.Current() require.NoError(t, err) // Simulate the SSH server flow: lookup user, then get their groups. - resolved, err := lookupWithGetent(current.Username) + resolved, err := LookupWithGetent(current.Username) require.NoError(t, err) - groups, err := groupIdsWithFallback(resolved) + groups, err := GroupIdsWithFallback(resolved) require.NoError(t, err) require.NotEmpty(t, groups, "resolved user must have groups") @@ -156,7 +156,7 @@ func TestIntegration_LookupAndGroupsConsistency(t *testing.T) { } // TestIntegration_ShellLookupChain tests the full shell resolution chain -// (getShellFromPasswd -> getShellFromGetent -> $SHELL -> default) on Unix. +// (getShellFromPasswd -> GetShellFromGetent -> $SHELL -> default) on Unix. func TestIntegration_ShellLookupChain(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("Unix shell lookup not applicable on Windows") @@ -165,8 +165,8 @@ func TestIntegration_ShellLookupChain(t *testing.T) { current, err := user.Current() require.NoError(t, err) - // getUserShell is the top-level function used by the SSH server. - shell := getUserShell(current.Uid) - require.NotEmpty(t, shell, "getUserShell must always return a shell") + // GetUserShell is the top-level function used by the SSH server. + shell := GetUserShell(current.Uid) + require.NotEmpty(t, shell, "GetUserShell must always return a shell") assert.True(t, shell[0] == '/', "shell should be an absolute path, got %q", shell) } diff --git a/client/ssh/server/getent_unix.go b/client/internal/shell/getent_unix.go similarity index 64% rename from client/ssh/server/getent_unix.go rename to client/internal/shell/getent_unix.go index a3a9641f8..f0bbe4dfd 100644 --- a/client/ssh/server/getent_unix.go +++ b/client/internal/shell/getent_unix.go @@ -1,6 +1,6 @@ //go:build !windows -package server +package shell import ( "context" @@ -14,19 +14,26 @@ import ( const getentTimeout = 5 * time.Second -// getShellFromGetent gets a user's login shell via getent by UID. +// GetShellFromGetent gets a user's login shell via getent by UID. // This is needed even with CGO because getShellFromPasswd reads /etc/passwd // directly and won't find NSS-provided users there. -func getShellFromGetent(userID string) string { - _, shell, err := runGetent(userID) +func GetShellFromGetent(userID string) string { + _, shell, err := runGetentPasswd(userID) if err != nil { return "" } return shell } -// runGetent executes `getent passwd ` and returns the user and login shell. -func runGetent(query string) (*user.User, string, error) { +// GetUserFromGetent returns the resolved user from either a uid or username, +// going through the host's NSS stack. +func GetUserFromGetent(query string) (*user.User, error) { + u, _, err := runGetentPasswd(query) + return u, err +} + +// runGetentPasswd executes `getent passwd ` and returns the user and login shell. +func runGetentPasswd(query string) (*user.User, string, error) { if !validateGetentInput(query) { return nil, "", fmt.Errorf("invalid getent input: %q", query) } @@ -42,7 +49,24 @@ func runGetent(query string) (*user.User, string, error) { return parseGetentPasswd(string(out)) } -// parseGetentPasswd parses getent passwd output: "name:x:uid:gid:gecos:home:shell" +// runGetentGroup executes `getent group ` and returns the group. +func runGetentGroup(query string) (*user.Group, error) { + if !validateGetentInput(query) { + return nil, fmt.Errorf("invalid getent input: %q", query) + } + + ctx, cancel := context.WithTimeout(context.Background(), getentTimeout) + defer cancel() + + out, err := exec.CommandContext(ctx, "getent", "group", query).Output() + if err != nil { + return nil, fmt.Errorf("getent group %s: %w", query, err) + } + + return parseGetentGroup(string(out)) +} + +// parseGetentPasswd parses getent passwd output: "name:x:uid:gid:gecos:home:shell". func parseGetentPasswd(output string) (*user.User, string, error) { fields := strings.SplitN(strings.TrimSpace(output), ":", 8) if len(fields) < 6 { @@ -67,6 +91,20 @@ func parseGetentPasswd(output string) (*user.User, string, error) { }, shell, nil } +// parseGetentGroup parses getent group output: "group:x:gid:members". +func parseGetentGroup(output string) (*user.Group, error) { + fields := strings.SplitN(strings.TrimSpace(output), ":", 8) + if len(fields) < 4 { + return nil, fmt.Errorf("unexpected getent output (need 4+ fields): %q", output) + } + + if fields[0] == "" || fields[2] == "" { + return nil, fmt.Errorf("missing required fields in getent output: %q", output) + } + + return &user.Group{Gid: fields[2], Name: fields[0]}, nil +} + // validateGetentInput checks that the input is safe to pass to getent or id. // Allows POSIX usernames, numeric UIDs, and common NSS extensions // (@ for Kerberos, $ for Samba, + for NIS compat). A leading hyphen is diff --git a/client/ssh/server/getent_unix_test.go b/client/internal/shell/getent_unix_test.go similarity index 95% rename from client/ssh/server/getent_unix_test.go rename to client/internal/shell/getent_unix_test.go index a73214e17..3a8ecbf47 100644 --- a/client/ssh/server/getent_unix_test.go +++ b/client/internal/shell/getent_unix_test.go @@ -1,6 +1,6 @@ //go:build !windows -package server +package shell import ( "os/exec" @@ -198,7 +198,7 @@ func TestRunGetent_RootUser(t *testing.T) { t.Skip("getent not available on this system") } - u, shell, err := runGetent("root") + u, shell, err := runGetentPasswd("root") require.NoError(t, err) assert.Equal(t, "root", u.Username) assert.Equal(t, "0", u.Uid) @@ -211,7 +211,7 @@ func TestRunGetent_ByUID(t *testing.T) { t.Skip("getent not available on this system") } - u, _, err := runGetent("0") + u, _, err := runGetentPasswd("0") require.NoError(t, err) assert.Equal(t, "root", u.Username) assert.Equal(t, "0", u.Uid) @@ -222,15 +222,15 @@ func TestRunGetent_NonexistentUser(t *testing.T) { t.Skip("getent not available on this system") } - _, _, err := runGetent("nonexistent_user_xyzzy_12345") + _, _, err := runGetentPasswd("nonexistent_user_xyzzy_12345") assert.Error(t, err) } func TestRunGetent_InvalidInput(t *testing.T) { - _, _, err := runGetent("") + _, _, err := runGetentPasswd("") assert.Error(t, err) - _, _, err = runGetent("user\x00name") + _, _, err = runGetentPasswd("user\x00name") assert.Error(t, err) } @@ -239,7 +239,7 @@ func TestRunGetent_NotAvailable(t *testing.T) { t.Skip("getent is available, can't test missing case") } - _, _, err := runGetent("root") + _, _, err := runGetentPasswd("root") assert.Error(t, err, "should fail when getent is not installed") } @@ -286,7 +286,7 @@ func TestGetentResultsMatchStdlib(t *testing.T) { current, err := user.Current() require.NoError(t, err) - getentUser, _, err := runGetent(current.Username) + getentUser, _, err := runGetentPasswd(current.Username) require.NoError(t, err) assert.Equal(t, current.Username, getentUser.Username, "username should match") @@ -303,7 +303,7 @@ func TestGetentResultsMatchStdlib_ByUID(t *testing.T) { current, err := user.Current() require.NoError(t, err) - getentUser, _, err := runGetent(current.Uid) + getentUser, _, err := runGetentPasswd(current.Uid) require.NoError(t, err) assert.Equal(t, current.Username, getentUser.Username, "username should match when looked up by UID") @@ -359,7 +359,7 @@ func TestGetShellFromPasswd_CurrentUser(t *testing.T) { assert.True(t, shell[0] == '/', "shell should be an absolute path, got %q", shell) if _, err := exec.LookPath("getent"); err == nil { - _, getentShell, getentErr := runGetent(current.Uid) + _, getentShell, getentErr := runGetentPasswd(current.Uid) if getentErr == nil && getentShell != "" { assert.Equal(t, getentShell, shell, "shell from /etc/passwd should match getent") } @@ -403,7 +403,7 @@ func TestGetShellFromPasswd_MatchesGetentForKnownUsers(t *testing.T) { continue } - _, getentShell, err := runGetent(uid) + _, getentShell, err := runGetentPasswd(uid) if err != nil { continue } diff --git a/client/internal/shell/getent_windows.go b/client/internal/shell/getent_windows.go new file mode 100644 index 000000000..28300b941 --- /dev/null +++ b/client/internal/shell/getent_windows.go @@ -0,0 +1,31 @@ +//go:build windows + +package shell + +import "os/user" + +// LookupWithGetent on Windows just delegates to os/user.Lookup. +// Windows does not use NSS/getent; its user lookup works without CGO. +func LookupWithGetent(username string) (*user.User, error) { + return user.Lookup(username) +} + +// CurrentUserWithGetent on Windows just delegates to os/user.Current. +func CurrentUserWithGetent() (*user.User, error) { + return user.Current() +} + +// LookupGroupWithGetent on Windows just delegates to os/user.LookupGroup. +func LookupGroupWithGetent(name string) (*user.Group, error) { + return user.LookupGroup(name) +} + +// GetShellFromGetent is a no-op on Windows; shell resolution uses PowerShell detection. +func GetShellFromGetent(_ string) string { + return "" +} + +// GroupIdsWithFallback on Windows just delegates to u.GroupIds(). +func GroupIdsWithFallback(u *user.User) ([]string, error) { + return u.GroupIds() +} diff --git a/client/ssh/server/shell.go b/client/internal/shell/shell.go similarity index 69% rename from client/ssh/server/shell.go rename to client/internal/shell/shell.go index 1e8ff5e31..ff2a622b3 100644 --- a/client/ssh/server/shell.go +++ b/client/internal/shell/shell.go @@ -1,17 +1,14 @@ -package server +package shell import ( "bufio" "fmt" - "net" "os" "os/exec" "os/user" "runtime" - "strconv" "strings" - "github.com/gliderlabs/ssh" log "github.com/sirupsen/logrus" ) @@ -22,9 +19,9 @@ const ( powershellExe = "powershell.exe" ) -// getUserShell returns the appropriate shell for the given user ID -// Handles all platform-specific logic and fallbacks consistently -func getUserShell(userID string) string { +// GetUserShell returns the appropriate shell for the given user ID. +// Handles all platform-specific logic and fallbacks consistently. +func GetUserShell(userID string) string { switch runtime.GOOS { case "windows": return getWindowsUserShell() @@ -56,7 +53,7 @@ func getUnixUserShell(userID string) string { return shell } - if shell := getShellFromGetent(userID); shell != "" { + if shell := GetShellFromGetent(userID); shell != "" { return shell } @@ -67,7 +64,7 @@ func getUnixUserShell(userID string) string { return defaultUnixShell } -// getShellFromPasswd reads the shell from /etc/passwd for the given user ID +// getShellFromPasswd reads the shell from /etc/passwd for the given user ID. func getShellFromPasswd(userID string) string { file, err := os.Open("/etc/passwd") if err != nil { @@ -101,8 +98,8 @@ func getShellFromPasswd(userID string) string { return "" } -// prepareUserEnv prepares environment variables for user execution -func prepareUserEnv(user *user.User, shell string) []string { +// PrepareUserEnv prepares environment variables for user execution. +func PrepareUserEnv(user *user.User, shell string) []string { pathValue := "/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games" if runtime.GOOS == "windows" { pathValue = `C:\Windows\System32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0` @@ -117,9 +114,9 @@ func prepareUserEnv(user *user.User, shell string) []string { } } -// acceptEnv checks if environment variable from SSH client should be accepted -// This is a whitelist of variables that SSH clients can send to the server -func acceptEnv(envVar string) bool { +// AcceptEnv checks if an environment variable from an SSH client should be accepted. +// This is a whitelist of variables that SSH clients can send to the server. +func AcceptEnv(envVar string) bool { varName := envVar if idx := strings.Index(envVar, "="); idx != -1 { varName = envVar[:idx] @@ -156,29 +153,3 @@ func acceptEnv(envVar string) bool { return false } - -// prepareSSHEnv prepares SSH protocol-specific environment variables -// These variables provide information about the SSH connection itself -func prepareSSHEnv(session ssh.Session) []string { - remoteAddr := session.RemoteAddr() - localAddr := session.LocalAddr() - - remoteHost, remotePort, err := net.SplitHostPort(remoteAddr.String()) - if err != nil { - remoteHost = remoteAddr.String() - remotePort = "0" - } - - localHost, localPort, err := net.SplitHostPort(localAddr.String()) - if err != nil { - localHost = localAddr.String() - localPort = strconv.Itoa(InternalSSHPort) - } - - return []string{ - // SSH_CLIENT format: "client_ip client_port server_port" - fmt.Sprintf("SSH_CLIENT=%s %s %s", remoteHost, remotePort, localPort), - // SSH_CONNECTION format: "client_ip client_port server_ip server_port" - fmt.Sprintf("SSH_CONNECTION=%s %s %s %s", remoteHost, remotePort, localHost, localPort), - } -} diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index 6fbb09958..311bdaa15 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -831,7 +831,10 @@ type UpRequest struct { // are delivered via the SubscribeStatus stream. When false (the default) the // RPC blocks until the engine is running or gives up, which is the behaviour // needed by the CLI. - Async bool `protobuf:"varint,4,opt,name=async,proto3" json:"async,omitempty"` + Async bool `protobuf:"varint,4,opt,name=async,proto3" json:"async,omitempty"` + // claimOwner, when true, claims ownership of the active profile for the + // calling user (adds their kernel principal to the profile's owner list). + ClaimOwner bool `protobuf:"varint,5,opt,name=claimOwner,proto3" json:"claimOwner,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -887,6 +890,13 @@ func (x *UpRequest) GetAsync() bool { return false } +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 @@ -4625,6 +4635,240 @@ func (x *AddProfileResponse) GetId() string { return "" } +type AddOwnerRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // principal is a typed owner string: "uid:1000", "gid:1000", + // "group:netbird-admins" (Unix, NSS-resolved), or "sid:S-1-5-..." (Windows). + Principal string `protobuf:"bytes,1,opt,name=principal,proto3" json:"principal,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddOwnerRequest) Reset() { + *x = AddOwnerRequest{} + mi := &file_daemon_proto_msgTypes[62] + 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[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 AddOwnerRequest.ProtoReflect.Descriptor instead. +func (*AddOwnerRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{62} +} + +func (x *AddOwnerRequest) GetPrincipal() string { + if x != nil { + return x.Principal + } + return "" +} + +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[63] + 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[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 AddOwnerResponse.ProtoReflect.Descriptor instead. +func (*AddOwnerResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{63} +} + +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[64] + 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[64] + 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{64} +} + +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[65] + 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[65] + 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{65} +} + +type ShareProfileRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Shared bool `protobuf:"varint,1,opt,name=shared,proto3" json:"shared,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ShareProfileRequest) Reset() { + *x = ShareProfileRequest{} + mi := &file_daemon_proto_msgTypes[66] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ShareProfileRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ShareProfileRequest) ProtoMessage() {} + +func (x *ShareProfileRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[66] + 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 ShareProfileRequest.ProtoReflect.Descriptor instead. +func (*ShareProfileRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{66} +} + +func (x *ShareProfileRequest) GetShared() bool { + if x != nil { + return x.Shared + } + return false +} + +type ShareProfileResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ShareProfileResponse) Reset() { + *x = ShareProfileResponse{} + mi := &file_daemon_proto_msgTypes[67] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ShareProfileResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ShareProfileResponse) ProtoMessage() {} + +func (x *ShareProfileResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[67] + 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 ShareProfileResponse.ProtoReflect.Descriptor instead. +func (*ShareProfileResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{67} +} + type RenameProfileRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` @@ -4638,7 +4882,7 @@ type RenameProfileRequest struct { func (x *RenameProfileRequest) Reset() { *x = RenameProfileRequest{} - mi := &file_daemon_proto_msgTypes[62] + mi := &file_daemon_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4650,7 +4894,7 @@ func (x *RenameProfileRequest) String() string { func (*RenameProfileRequest) ProtoMessage() {} func (x *RenameProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[62] + mi := &file_daemon_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4663,7 +4907,7 @@ func (x *RenameProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RenameProfileRequest.ProtoReflect.Descriptor instead. func (*RenameProfileRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{62} + return file_daemon_proto_rawDescGZIP(), []int{68} } func (x *RenameProfileRequest) GetUsername() string { @@ -4697,7 +4941,7 @@ type RenameProfileResponse struct { func (x *RenameProfileResponse) Reset() { *x = RenameProfileResponse{} - mi := &file_daemon_proto_msgTypes[63] + mi := &file_daemon_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4709,7 +4953,7 @@ func (x *RenameProfileResponse) String() string { func (*RenameProfileResponse) ProtoMessage() {} func (x *RenameProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[63] + mi := &file_daemon_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4722,7 +4966,7 @@ func (x *RenameProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RenameProfileResponse.ProtoReflect.Descriptor instead. func (*RenameProfileResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{63} + return file_daemon_proto_rawDescGZIP(), []int{69} } func (x *RenameProfileResponse) GetOldProfileName() string { @@ -4744,7 +4988,7 @@ type RemoveProfileRequest struct { func (x *RemoveProfileRequest) Reset() { *x = RemoveProfileRequest{} - mi := &file_daemon_proto_msgTypes[64] + mi := &file_daemon_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4756,7 +5000,7 @@ func (x *RemoveProfileRequest) String() string { func (*RemoveProfileRequest) ProtoMessage() {} func (x *RemoveProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[64] + mi := &file_daemon_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4769,7 +5013,7 @@ func (x *RemoveProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveProfileRequest.ProtoReflect.Descriptor instead. func (*RemoveProfileRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{64} + return file_daemon_proto_rawDescGZIP(), []int{70} } func (x *RemoveProfileRequest) GetUsername() string { @@ -4797,7 +5041,7 @@ type RemoveProfileResponse struct { func (x *RemoveProfileResponse) Reset() { *x = RemoveProfileResponse{} - mi := &file_daemon_proto_msgTypes[65] + mi := &file_daemon_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4809,7 +5053,7 @@ func (x *RemoveProfileResponse) String() string { func (*RemoveProfileResponse) ProtoMessage() {} func (x *RemoveProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[65] + mi := &file_daemon_proto_msgTypes[71] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4822,7 +5066,7 @@ func (x *RemoveProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveProfileResponse.ProtoReflect.Descriptor instead. func (*RemoveProfileResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{65} + return file_daemon_proto_rawDescGZIP(), []int{71} } func (x *RemoveProfileResponse) GetId() string { @@ -4841,7 +5085,7 @@ type ListProfilesRequest struct { func (x *ListProfilesRequest) Reset() { *x = ListProfilesRequest{} - mi := &file_daemon_proto_msgTypes[66] + mi := &file_daemon_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4853,7 +5097,7 @@ func (x *ListProfilesRequest) String() string { func (*ListProfilesRequest) ProtoMessage() {} func (x *ListProfilesRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[66] + mi := &file_daemon_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4866,7 +5110,7 @@ func (x *ListProfilesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListProfilesRequest.ProtoReflect.Descriptor instead. func (*ListProfilesRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{66} + return file_daemon_proto_rawDescGZIP(), []int{72} } func (x *ListProfilesRequest) GetUsername() string { @@ -4885,7 +5129,7 @@ type ListProfilesResponse struct { func (x *ListProfilesResponse) Reset() { *x = ListProfilesResponse{} - mi := &file_daemon_proto_msgTypes[67] + mi := &file_daemon_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4897,7 +5141,7 @@ func (x *ListProfilesResponse) String() string { func (*ListProfilesResponse) ProtoMessage() {} func (x *ListProfilesResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[67] + mi := &file_daemon_proto_msgTypes[73] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4910,7 +5154,7 @@ func (x *ListProfilesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListProfilesResponse.ProtoReflect.Descriptor instead. func (*ListProfilesResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{67} + return file_daemon_proto_rawDescGZIP(), []int{73} } func (x *ListProfilesResponse) GetProfiles() []*Profile { @@ -4931,7 +5175,7 @@ type Profile struct { func (x *Profile) Reset() { *x = Profile{} - mi := &file_daemon_proto_msgTypes[68] + mi := &file_daemon_proto_msgTypes[74] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4943,7 +5187,7 @@ func (x *Profile) String() string { func (*Profile) ProtoMessage() {} func (x *Profile) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[68] + mi := &file_daemon_proto_msgTypes[74] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4956,7 +5200,7 @@ func (x *Profile) ProtoReflect() protoreflect.Message { // Deprecated: Use Profile.ProtoReflect.Descriptor instead. func (*Profile) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{68} + return file_daemon_proto_rawDescGZIP(), []int{74} } func (x *Profile) GetName() string { @@ -4988,7 +5232,7 @@ type GetActiveProfileRequest struct { func (x *GetActiveProfileRequest) Reset() { *x = GetActiveProfileRequest{} - mi := &file_daemon_proto_msgTypes[69] + mi := &file_daemon_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5000,7 +5244,7 @@ func (x *GetActiveProfileRequest) String() string { func (*GetActiveProfileRequest) ProtoMessage() {} func (x *GetActiveProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[69] + mi := &file_daemon_proto_msgTypes[75] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5013,7 +5257,7 @@ func (x *GetActiveProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetActiveProfileRequest.ProtoReflect.Descriptor instead. func (*GetActiveProfileRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{69} + return file_daemon_proto_rawDescGZIP(), []int{75} } type GetActiveProfileResponse struct { @@ -5027,7 +5271,7 @@ type GetActiveProfileResponse struct { func (x *GetActiveProfileResponse) Reset() { *x = GetActiveProfileResponse{} - mi := &file_daemon_proto_msgTypes[70] + mi := &file_daemon_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5039,7 +5283,7 @@ func (x *GetActiveProfileResponse) String() string { func (*GetActiveProfileResponse) ProtoMessage() {} func (x *GetActiveProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[70] + mi := &file_daemon_proto_msgTypes[76] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5052,7 +5296,7 @@ func (x *GetActiveProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetActiveProfileResponse.ProtoReflect.Descriptor instead. func (*GetActiveProfileResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{70} + return file_daemon_proto_rawDescGZIP(), []int{76} } func (x *GetActiveProfileResponse) GetProfileName() string { @@ -5086,7 +5330,7 @@ type LogoutRequest struct { func (x *LogoutRequest) Reset() { *x = LogoutRequest{} - mi := &file_daemon_proto_msgTypes[71] + mi := &file_daemon_proto_msgTypes[77] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5098,7 +5342,7 @@ func (x *LogoutRequest) String() string { func (*LogoutRequest) ProtoMessage() {} func (x *LogoutRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[71] + mi := &file_daemon_proto_msgTypes[77] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5111,7 +5355,7 @@ func (x *LogoutRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use LogoutRequest.ProtoReflect.Descriptor instead. func (*LogoutRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{71} + return file_daemon_proto_rawDescGZIP(), []int{77} } func (x *LogoutRequest) GetProfileName() string { @@ -5136,7 +5380,7 @@ type LogoutResponse struct { func (x *LogoutResponse) Reset() { *x = LogoutResponse{} - mi := &file_daemon_proto_msgTypes[72] + mi := &file_daemon_proto_msgTypes[78] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5148,7 +5392,7 @@ func (x *LogoutResponse) String() string { func (*LogoutResponse) ProtoMessage() {} func (x *LogoutResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[72] + mi := &file_daemon_proto_msgTypes[78] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5161,7 +5405,7 @@ func (x *LogoutResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use LogoutResponse.ProtoReflect.Descriptor instead. func (*LogoutResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{72} + return file_daemon_proto_rawDescGZIP(), []int{78} } type WailsUIReadyRequest struct { @@ -5172,7 +5416,7 @@ type WailsUIReadyRequest struct { func (x *WailsUIReadyRequest) Reset() { *x = WailsUIReadyRequest{} - mi := &file_daemon_proto_msgTypes[73] + mi := &file_daemon_proto_msgTypes[79] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5184,7 +5428,7 @@ func (x *WailsUIReadyRequest) String() string { func (*WailsUIReadyRequest) ProtoMessage() {} func (x *WailsUIReadyRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[73] + mi := &file_daemon_proto_msgTypes[79] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5197,7 +5441,7 @@ func (x *WailsUIReadyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use WailsUIReadyRequest.ProtoReflect.Descriptor instead. func (*WailsUIReadyRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{73} + return file_daemon_proto_rawDescGZIP(), []int{79} } type WailsUIReadyResponse struct { @@ -5208,7 +5452,7 @@ type WailsUIReadyResponse struct { func (x *WailsUIReadyResponse) Reset() { *x = WailsUIReadyResponse{} - mi := &file_daemon_proto_msgTypes[74] + mi := &file_daemon_proto_msgTypes[80] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5220,7 +5464,7 @@ func (x *WailsUIReadyResponse) String() string { func (*WailsUIReadyResponse) ProtoMessage() {} func (x *WailsUIReadyResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[74] + mi := &file_daemon_proto_msgTypes[80] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5233,7 +5477,7 @@ func (x *WailsUIReadyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use WailsUIReadyResponse.ProtoReflect.Descriptor instead. func (*WailsUIReadyResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{74} + return file_daemon_proto_rawDescGZIP(), []int{80} } type GetFeaturesRequest struct { @@ -5244,7 +5488,7 @@ type GetFeaturesRequest struct { func (x *GetFeaturesRequest) Reset() { *x = GetFeaturesRequest{} - mi := &file_daemon_proto_msgTypes[75] + mi := &file_daemon_proto_msgTypes[81] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5256,7 +5500,7 @@ func (x *GetFeaturesRequest) String() string { func (*GetFeaturesRequest) ProtoMessage() {} func (x *GetFeaturesRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[75] + mi := &file_daemon_proto_msgTypes[81] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5269,7 +5513,7 @@ func (x *GetFeaturesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetFeaturesRequest.ProtoReflect.Descriptor instead. func (*GetFeaturesRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{75} + return file_daemon_proto_rawDescGZIP(), []int{81} } type GetFeaturesResponse struct { @@ -5289,7 +5533,7 @@ type GetFeaturesResponse struct { func (x *GetFeaturesResponse) Reset() { *x = GetFeaturesResponse{} - mi := &file_daemon_proto_msgTypes[76] + mi := &file_daemon_proto_msgTypes[82] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5301,7 +5545,7 @@ func (x *GetFeaturesResponse) String() string { func (*GetFeaturesResponse) ProtoMessage() {} func (x *GetFeaturesResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[76] + mi := &file_daemon_proto_msgTypes[82] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5314,7 +5558,7 @@ func (x *GetFeaturesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetFeaturesResponse.ProtoReflect.Descriptor instead. func (*GetFeaturesResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{76} + return file_daemon_proto_rawDescGZIP(), []int{82} } func (x *GetFeaturesResponse) GetDisableProfiles() bool { @@ -5359,7 +5603,7 @@ type MDMManagedFieldsViolation struct { func (x *MDMManagedFieldsViolation) Reset() { *x = MDMManagedFieldsViolation{} - mi := &file_daemon_proto_msgTypes[77] + mi := &file_daemon_proto_msgTypes[83] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5371,7 +5615,7 @@ func (x *MDMManagedFieldsViolation) String() string { func (*MDMManagedFieldsViolation) ProtoMessage() {} func (x *MDMManagedFieldsViolation) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[77] + mi := &file_daemon_proto_msgTypes[83] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5384,7 +5628,7 @@ func (x *MDMManagedFieldsViolation) ProtoReflect() protoreflect.Message { // Deprecated: Use MDMManagedFieldsViolation.ProtoReflect.Descriptor instead. func (*MDMManagedFieldsViolation) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{77} + return file_daemon_proto_rawDescGZIP(), []int{83} } func (x *MDMManagedFieldsViolation) GetFields() []string { @@ -5402,7 +5646,7 @@ type TriggerUpdateRequest struct { func (x *TriggerUpdateRequest) Reset() { *x = TriggerUpdateRequest{} - mi := &file_daemon_proto_msgTypes[78] + mi := &file_daemon_proto_msgTypes[84] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5414,7 +5658,7 @@ func (x *TriggerUpdateRequest) String() string { func (*TriggerUpdateRequest) ProtoMessage() {} func (x *TriggerUpdateRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[78] + mi := &file_daemon_proto_msgTypes[84] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5427,7 +5671,7 @@ func (x *TriggerUpdateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TriggerUpdateRequest.ProtoReflect.Descriptor instead. func (*TriggerUpdateRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{78} + return file_daemon_proto_rawDescGZIP(), []int{84} } type TriggerUpdateResponse struct { @@ -5440,7 +5684,7 @@ type TriggerUpdateResponse struct { func (x *TriggerUpdateResponse) Reset() { *x = TriggerUpdateResponse{} - mi := &file_daemon_proto_msgTypes[79] + mi := &file_daemon_proto_msgTypes[85] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5452,7 +5696,7 @@ func (x *TriggerUpdateResponse) String() string { func (*TriggerUpdateResponse) ProtoMessage() {} func (x *TriggerUpdateResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[79] + mi := &file_daemon_proto_msgTypes[85] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5465,7 +5709,7 @@ func (x *TriggerUpdateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use TriggerUpdateResponse.ProtoReflect.Descriptor instead. func (*TriggerUpdateResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{79} + return file_daemon_proto_rawDescGZIP(), []int{85} } func (x *TriggerUpdateResponse) GetSuccess() bool { @@ -5493,7 +5737,7 @@ type GetPeerSSHHostKeyRequest struct { func (x *GetPeerSSHHostKeyRequest) Reset() { *x = GetPeerSSHHostKeyRequest{} - mi := &file_daemon_proto_msgTypes[80] + mi := &file_daemon_proto_msgTypes[86] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5505,7 +5749,7 @@ func (x *GetPeerSSHHostKeyRequest) String() string { func (*GetPeerSSHHostKeyRequest) ProtoMessage() {} func (x *GetPeerSSHHostKeyRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[80] + mi := &file_daemon_proto_msgTypes[86] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5518,7 +5762,7 @@ func (x *GetPeerSSHHostKeyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetPeerSSHHostKeyRequest.ProtoReflect.Descriptor instead. func (*GetPeerSSHHostKeyRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{80} + return file_daemon_proto_rawDescGZIP(), []int{86} } func (x *GetPeerSSHHostKeyRequest) GetPeerAddress() string { @@ -5545,7 +5789,7 @@ type GetPeerSSHHostKeyResponse struct { func (x *GetPeerSSHHostKeyResponse) Reset() { *x = GetPeerSSHHostKeyResponse{} - mi := &file_daemon_proto_msgTypes[81] + mi := &file_daemon_proto_msgTypes[87] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5557,7 +5801,7 @@ func (x *GetPeerSSHHostKeyResponse) String() string { func (*GetPeerSSHHostKeyResponse) ProtoMessage() {} func (x *GetPeerSSHHostKeyResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[81] + mi := &file_daemon_proto_msgTypes[87] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5570,7 +5814,7 @@ func (x *GetPeerSSHHostKeyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetPeerSSHHostKeyResponse.ProtoReflect.Descriptor instead. func (*GetPeerSSHHostKeyResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{81} + return file_daemon_proto_rawDescGZIP(), []int{87} } func (x *GetPeerSSHHostKeyResponse) GetSshHostKey() []byte { @@ -5612,7 +5856,7 @@ type RequestJWTAuthRequest struct { func (x *RequestJWTAuthRequest) Reset() { *x = RequestJWTAuthRequest{} - mi := &file_daemon_proto_msgTypes[82] + mi := &file_daemon_proto_msgTypes[88] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5624,7 +5868,7 @@ func (x *RequestJWTAuthRequest) String() string { func (*RequestJWTAuthRequest) ProtoMessage() {} func (x *RequestJWTAuthRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[82] + mi := &file_daemon_proto_msgTypes[88] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5637,7 +5881,7 @@ func (x *RequestJWTAuthRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RequestJWTAuthRequest.ProtoReflect.Descriptor instead. func (*RequestJWTAuthRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{82} + return file_daemon_proto_rawDescGZIP(), []int{88} } func (x *RequestJWTAuthRequest) GetHint() string { @@ -5670,7 +5914,7 @@ type RequestJWTAuthResponse struct { func (x *RequestJWTAuthResponse) Reset() { *x = RequestJWTAuthResponse{} - mi := &file_daemon_proto_msgTypes[83] + mi := &file_daemon_proto_msgTypes[89] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5682,7 +5926,7 @@ func (x *RequestJWTAuthResponse) String() string { func (*RequestJWTAuthResponse) ProtoMessage() {} func (x *RequestJWTAuthResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[83] + mi := &file_daemon_proto_msgTypes[89] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5695,7 +5939,7 @@ func (x *RequestJWTAuthResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RequestJWTAuthResponse.ProtoReflect.Descriptor instead. func (*RequestJWTAuthResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{83} + return file_daemon_proto_rawDescGZIP(), []int{89} } func (x *RequestJWTAuthResponse) GetVerificationURI() string { @@ -5760,7 +6004,7 @@ type WaitJWTTokenRequest struct { func (x *WaitJWTTokenRequest) Reset() { *x = WaitJWTTokenRequest{} - mi := &file_daemon_proto_msgTypes[84] + mi := &file_daemon_proto_msgTypes[90] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5772,7 +6016,7 @@ func (x *WaitJWTTokenRequest) String() string { func (*WaitJWTTokenRequest) ProtoMessage() {} func (x *WaitJWTTokenRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[84] + mi := &file_daemon_proto_msgTypes[90] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5785,7 +6029,7 @@ func (x *WaitJWTTokenRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use WaitJWTTokenRequest.ProtoReflect.Descriptor instead. func (*WaitJWTTokenRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{84} + return file_daemon_proto_rawDescGZIP(), []int{90} } func (x *WaitJWTTokenRequest) GetDeviceCode() string { @@ -5817,7 +6061,7 @@ type WaitJWTTokenResponse struct { func (x *WaitJWTTokenResponse) Reset() { *x = WaitJWTTokenResponse{} - mi := &file_daemon_proto_msgTypes[85] + mi := &file_daemon_proto_msgTypes[91] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5829,7 +6073,7 @@ func (x *WaitJWTTokenResponse) String() string { func (*WaitJWTTokenResponse) ProtoMessage() {} func (x *WaitJWTTokenResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[85] + mi := &file_daemon_proto_msgTypes[91] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5842,7 +6086,7 @@ func (x *WaitJWTTokenResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use WaitJWTTokenResponse.ProtoReflect.Descriptor instead. func (*WaitJWTTokenResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{85} + return file_daemon_proto_rawDescGZIP(), []int{91} } func (x *WaitJWTTokenResponse) GetToken() string { @@ -5878,7 +6122,7 @@ type RequestExtendAuthSessionRequest struct { func (x *RequestExtendAuthSessionRequest) Reset() { *x = RequestExtendAuthSessionRequest{} - mi := &file_daemon_proto_msgTypes[86] + mi := &file_daemon_proto_msgTypes[92] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5890,7 +6134,7 @@ func (x *RequestExtendAuthSessionRequest) String() string { func (*RequestExtendAuthSessionRequest) ProtoMessage() {} func (x *RequestExtendAuthSessionRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[86] + mi := &file_daemon_proto_msgTypes[92] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5903,7 +6147,7 @@ func (x *RequestExtendAuthSessionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RequestExtendAuthSessionRequest.ProtoReflect.Descriptor instead. func (*RequestExtendAuthSessionRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{86} + return file_daemon_proto_rawDescGZIP(), []int{92} } func (x *RequestExtendAuthSessionRequest) GetHint() string { @@ -5934,7 +6178,7 @@ type RequestExtendAuthSessionResponse struct { func (x *RequestExtendAuthSessionResponse) Reset() { *x = RequestExtendAuthSessionResponse{} - mi := &file_daemon_proto_msgTypes[87] + mi := &file_daemon_proto_msgTypes[93] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5946,7 +6190,7 @@ func (x *RequestExtendAuthSessionResponse) String() string { func (*RequestExtendAuthSessionResponse) ProtoMessage() {} func (x *RequestExtendAuthSessionResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[87] + mi := &file_daemon_proto_msgTypes[93] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5959,7 +6203,7 @@ func (x *RequestExtendAuthSessionResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RequestExtendAuthSessionResponse.ProtoReflect.Descriptor instead. func (*RequestExtendAuthSessionResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{87} + return file_daemon_proto_rawDescGZIP(), []int{93} } func (x *RequestExtendAuthSessionResponse) GetVerificationURI() string { @@ -6012,7 +6256,7 @@ type WaitExtendAuthSessionRequest struct { func (x *WaitExtendAuthSessionRequest) Reset() { *x = WaitExtendAuthSessionRequest{} - mi := &file_daemon_proto_msgTypes[88] + mi := &file_daemon_proto_msgTypes[94] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6024,7 +6268,7 @@ func (x *WaitExtendAuthSessionRequest) String() string { func (*WaitExtendAuthSessionRequest) ProtoMessage() {} func (x *WaitExtendAuthSessionRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[88] + mi := &file_daemon_proto_msgTypes[94] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6037,7 +6281,7 @@ func (x *WaitExtendAuthSessionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use WaitExtendAuthSessionRequest.ProtoReflect.Descriptor instead. func (*WaitExtendAuthSessionRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{88} + return file_daemon_proto_rawDescGZIP(), []int{94} } func (x *WaitExtendAuthSessionRequest) GetDeviceCode() string { @@ -6066,7 +6310,7 @@ type WaitExtendAuthSessionResponse struct { func (x *WaitExtendAuthSessionResponse) Reset() { *x = WaitExtendAuthSessionResponse{} - mi := &file_daemon_proto_msgTypes[89] + mi := &file_daemon_proto_msgTypes[95] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6078,7 +6322,7 @@ func (x *WaitExtendAuthSessionResponse) String() string { func (*WaitExtendAuthSessionResponse) ProtoMessage() {} func (x *WaitExtendAuthSessionResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[89] + mi := &file_daemon_proto_msgTypes[95] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6091,7 +6335,7 @@ func (x *WaitExtendAuthSessionResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use WaitExtendAuthSessionResponse.ProtoReflect.Descriptor instead. func (*WaitExtendAuthSessionResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{89} + return file_daemon_proto_rawDescGZIP(), []int{95} } func (x *WaitExtendAuthSessionResponse) GetSessionExpiresAt() *timestamppb.Timestamp { @@ -6111,7 +6355,7 @@ type DismissSessionWarningRequest struct { func (x *DismissSessionWarningRequest) Reset() { *x = DismissSessionWarningRequest{} - mi := &file_daemon_proto_msgTypes[90] + mi := &file_daemon_proto_msgTypes[96] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6123,7 +6367,7 @@ func (x *DismissSessionWarningRequest) String() string { func (*DismissSessionWarningRequest) ProtoMessage() {} func (x *DismissSessionWarningRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[90] + mi := &file_daemon_proto_msgTypes[96] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6136,7 +6380,7 @@ func (x *DismissSessionWarningRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DismissSessionWarningRequest.ProtoReflect.Descriptor instead. func (*DismissSessionWarningRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{90} + return file_daemon_proto_rawDescGZIP(), []int{96} } // DismissSessionWarningResponse acknowledges the dismissal. Carries no @@ -6150,7 +6394,7 @@ type DismissSessionWarningResponse struct { func (x *DismissSessionWarningResponse) Reset() { *x = DismissSessionWarningResponse{} - mi := &file_daemon_proto_msgTypes[91] + mi := &file_daemon_proto_msgTypes[97] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6162,7 +6406,7 @@ func (x *DismissSessionWarningResponse) String() string { func (*DismissSessionWarningResponse) ProtoMessage() {} func (x *DismissSessionWarningResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[91] + mi := &file_daemon_proto_msgTypes[97] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6175,7 +6419,7 @@ func (x *DismissSessionWarningResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DismissSessionWarningResponse.ProtoReflect.Descriptor instead. func (*DismissSessionWarningResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{91} + return file_daemon_proto_rawDescGZIP(), []int{97} } // StartCPUProfileRequest for starting CPU profiling @@ -6187,7 +6431,7 @@ type StartCPUProfileRequest struct { func (x *StartCPUProfileRequest) Reset() { *x = StartCPUProfileRequest{} - mi := &file_daemon_proto_msgTypes[92] + mi := &file_daemon_proto_msgTypes[98] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6199,7 +6443,7 @@ func (x *StartCPUProfileRequest) String() string { func (*StartCPUProfileRequest) ProtoMessage() {} func (x *StartCPUProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[92] + mi := &file_daemon_proto_msgTypes[98] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6212,7 +6456,7 @@ func (x *StartCPUProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StartCPUProfileRequest.ProtoReflect.Descriptor instead. func (*StartCPUProfileRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{92} + return file_daemon_proto_rawDescGZIP(), []int{98} } // StartCPUProfileResponse confirms CPU profiling has started @@ -6224,7 +6468,7 @@ type StartCPUProfileResponse struct { func (x *StartCPUProfileResponse) Reset() { *x = StartCPUProfileResponse{} - mi := &file_daemon_proto_msgTypes[93] + mi := &file_daemon_proto_msgTypes[99] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6236,7 +6480,7 @@ func (x *StartCPUProfileResponse) String() string { func (*StartCPUProfileResponse) ProtoMessage() {} func (x *StartCPUProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[93] + mi := &file_daemon_proto_msgTypes[99] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6249,7 +6493,7 @@ func (x *StartCPUProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StartCPUProfileResponse.ProtoReflect.Descriptor instead. func (*StartCPUProfileResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{93} + return file_daemon_proto_rawDescGZIP(), []int{99} } // StopCPUProfileRequest for stopping CPU profiling @@ -6261,7 +6505,7 @@ type StopCPUProfileRequest struct { func (x *StopCPUProfileRequest) Reset() { *x = StopCPUProfileRequest{} - mi := &file_daemon_proto_msgTypes[94] + mi := &file_daemon_proto_msgTypes[100] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6273,7 +6517,7 @@ func (x *StopCPUProfileRequest) String() string { func (*StopCPUProfileRequest) ProtoMessage() {} func (x *StopCPUProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[94] + mi := &file_daemon_proto_msgTypes[100] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6286,7 +6530,7 @@ func (x *StopCPUProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StopCPUProfileRequest.ProtoReflect.Descriptor instead. func (*StopCPUProfileRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{94} + return file_daemon_proto_rawDescGZIP(), []int{100} } // StopCPUProfileResponse confirms CPU profiling has stopped @@ -6298,7 +6542,7 @@ type StopCPUProfileResponse struct { func (x *StopCPUProfileResponse) Reset() { *x = StopCPUProfileResponse{} - mi := &file_daemon_proto_msgTypes[95] + mi := &file_daemon_proto_msgTypes[101] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6310,7 +6554,7 @@ func (x *StopCPUProfileResponse) String() string { func (*StopCPUProfileResponse) ProtoMessage() {} func (x *StopCPUProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[95] + mi := &file_daemon_proto_msgTypes[101] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6323,7 +6567,7 @@ func (x *StopCPUProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StopCPUProfileResponse.ProtoReflect.Descriptor instead. func (*StopCPUProfileResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{95} + return file_daemon_proto_rawDescGZIP(), []int{101} } type InstallerResultRequest struct { @@ -6334,7 +6578,7 @@ type InstallerResultRequest struct { func (x *InstallerResultRequest) Reset() { *x = InstallerResultRequest{} - mi := &file_daemon_proto_msgTypes[96] + mi := &file_daemon_proto_msgTypes[102] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6346,7 +6590,7 @@ func (x *InstallerResultRequest) String() string { func (*InstallerResultRequest) ProtoMessage() {} func (x *InstallerResultRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[96] + mi := &file_daemon_proto_msgTypes[102] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6359,7 +6603,7 @@ func (x *InstallerResultRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use InstallerResultRequest.ProtoReflect.Descriptor instead. func (*InstallerResultRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{96} + return file_daemon_proto_rawDescGZIP(), []int{102} } type InstallerResultResponse struct { @@ -6372,7 +6616,7 @@ type InstallerResultResponse struct { func (x *InstallerResultResponse) Reset() { *x = InstallerResultResponse{} - mi := &file_daemon_proto_msgTypes[97] + mi := &file_daemon_proto_msgTypes[103] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6384,7 +6628,7 @@ func (x *InstallerResultResponse) String() string { func (*InstallerResultResponse) ProtoMessage() {} func (x *InstallerResultResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[97] + mi := &file_daemon_proto_msgTypes[103] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6397,7 +6641,7 @@ func (x *InstallerResultResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use InstallerResultResponse.ProtoReflect.Descriptor instead. func (*InstallerResultResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{97} + return file_daemon_proto_rawDescGZIP(), []int{103} } func (x *InstallerResultResponse) GetSuccess() bool { @@ -6430,7 +6674,7 @@ type ExposeServiceRequest struct { func (x *ExposeServiceRequest) Reset() { *x = ExposeServiceRequest{} - mi := &file_daemon_proto_msgTypes[98] + mi := &file_daemon_proto_msgTypes[104] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6442,7 +6686,7 @@ func (x *ExposeServiceRequest) String() string { func (*ExposeServiceRequest) ProtoMessage() {} func (x *ExposeServiceRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[98] + mi := &file_daemon_proto_msgTypes[104] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6455,7 +6699,7 @@ func (x *ExposeServiceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ExposeServiceRequest.ProtoReflect.Descriptor instead. func (*ExposeServiceRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{98} + return file_daemon_proto_rawDescGZIP(), []int{104} } func (x *ExposeServiceRequest) GetPort() uint32 { @@ -6526,7 +6770,7 @@ type ExposeServiceEvent struct { func (x *ExposeServiceEvent) Reset() { *x = ExposeServiceEvent{} - mi := &file_daemon_proto_msgTypes[99] + mi := &file_daemon_proto_msgTypes[105] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6538,7 +6782,7 @@ func (x *ExposeServiceEvent) String() string { func (*ExposeServiceEvent) ProtoMessage() {} func (x *ExposeServiceEvent) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[99] + mi := &file_daemon_proto_msgTypes[105] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6551,7 +6795,7 @@ func (x *ExposeServiceEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use ExposeServiceEvent.ProtoReflect.Descriptor instead. func (*ExposeServiceEvent) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{99} + return file_daemon_proto_rawDescGZIP(), []int{105} } func (x *ExposeServiceEvent) GetEvent() isExposeServiceEvent_Event { @@ -6592,7 +6836,7 @@ type ExposeServiceReady struct { func (x *ExposeServiceReady) Reset() { *x = ExposeServiceReady{} - mi := &file_daemon_proto_msgTypes[100] + mi := &file_daemon_proto_msgTypes[106] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6604,7 +6848,7 @@ func (x *ExposeServiceReady) String() string { func (*ExposeServiceReady) ProtoMessage() {} func (x *ExposeServiceReady) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[100] + mi := &file_daemon_proto_msgTypes[106] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6617,7 +6861,7 @@ func (x *ExposeServiceReady) ProtoReflect() protoreflect.Message { // Deprecated: Use ExposeServiceReady.ProtoReflect.Descriptor instead. func (*ExposeServiceReady) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{100} + return file_daemon_proto_rawDescGZIP(), []int{106} } func (x *ExposeServiceReady) GetServiceName() string { @@ -6662,7 +6906,7 @@ type StartCaptureRequest struct { func (x *StartCaptureRequest) Reset() { *x = StartCaptureRequest{} - mi := &file_daemon_proto_msgTypes[101] + mi := &file_daemon_proto_msgTypes[107] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6674,7 +6918,7 @@ func (x *StartCaptureRequest) String() string { func (*StartCaptureRequest) ProtoMessage() {} func (x *StartCaptureRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[101] + mi := &file_daemon_proto_msgTypes[107] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6687,7 +6931,7 @@ func (x *StartCaptureRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StartCaptureRequest.ProtoReflect.Descriptor instead. func (*StartCaptureRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{101} + return file_daemon_proto_rawDescGZIP(), []int{107} } func (x *StartCaptureRequest) GetTextOutput() bool { @@ -6741,7 +6985,7 @@ type CapturePacket struct { func (x *CapturePacket) Reset() { *x = CapturePacket{} - mi := &file_daemon_proto_msgTypes[102] + mi := &file_daemon_proto_msgTypes[108] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6753,7 +6997,7 @@ func (x *CapturePacket) String() string { func (*CapturePacket) ProtoMessage() {} func (x *CapturePacket) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[102] + mi := &file_daemon_proto_msgTypes[108] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6766,7 +7010,7 @@ func (x *CapturePacket) ProtoReflect() protoreflect.Message { // Deprecated: Use CapturePacket.ProtoReflect.Descriptor instead. func (*CapturePacket) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{102} + return file_daemon_proto_rawDescGZIP(), []int{108} } func (x *CapturePacket) GetData() []byte { @@ -6787,7 +7031,7 @@ type StartBundleCaptureRequest struct { func (x *StartBundleCaptureRequest) Reset() { *x = StartBundleCaptureRequest{} - mi := &file_daemon_proto_msgTypes[103] + mi := &file_daemon_proto_msgTypes[109] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6799,7 +7043,7 @@ func (x *StartBundleCaptureRequest) String() string { func (*StartBundleCaptureRequest) ProtoMessage() {} func (x *StartBundleCaptureRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[103] + mi := &file_daemon_proto_msgTypes[109] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6812,7 +7056,7 @@ func (x *StartBundleCaptureRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StartBundleCaptureRequest.ProtoReflect.Descriptor instead. func (*StartBundleCaptureRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{103} + return file_daemon_proto_rawDescGZIP(), []int{109} } func (x *StartBundleCaptureRequest) GetTimeout() *durationpb.Duration { @@ -6830,7 +7074,7 @@ type StartBundleCaptureResponse struct { func (x *StartBundleCaptureResponse) Reset() { *x = StartBundleCaptureResponse{} - mi := &file_daemon_proto_msgTypes[104] + mi := &file_daemon_proto_msgTypes[110] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6842,7 +7086,7 @@ func (x *StartBundleCaptureResponse) String() string { func (*StartBundleCaptureResponse) ProtoMessage() {} func (x *StartBundleCaptureResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[104] + mi := &file_daemon_proto_msgTypes[110] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6855,7 +7099,7 @@ func (x *StartBundleCaptureResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StartBundleCaptureResponse.ProtoReflect.Descriptor instead. func (*StartBundleCaptureResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{104} + return file_daemon_proto_rawDescGZIP(), []int{110} } type StopBundleCaptureRequest struct { @@ -6866,7 +7110,7 @@ type StopBundleCaptureRequest struct { func (x *StopBundleCaptureRequest) Reset() { *x = StopBundleCaptureRequest{} - mi := &file_daemon_proto_msgTypes[105] + mi := &file_daemon_proto_msgTypes[111] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6878,7 +7122,7 @@ func (x *StopBundleCaptureRequest) String() string { func (*StopBundleCaptureRequest) ProtoMessage() {} func (x *StopBundleCaptureRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[105] + mi := &file_daemon_proto_msgTypes[111] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6891,7 +7135,7 @@ func (x *StopBundleCaptureRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StopBundleCaptureRequest.ProtoReflect.Descriptor instead. func (*StopBundleCaptureRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{105} + return file_daemon_proto_rawDescGZIP(), []int{111} } type StopBundleCaptureResponse struct { @@ -6902,7 +7146,7 @@ type StopBundleCaptureResponse struct { func (x *StopBundleCaptureResponse) Reset() { *x = StopBundleCaptureResponse{} - mi := &file_daemon_proto_msgTypes[106] + mi := &file_daemon_proto_msgTypes[112] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6914,7 +7158,7 @@ func (x *StopBundleCaptureResponse) String() string { func (*StopBundleCaptureResponse) ProtoMessage() {} func (x *StopBundleCaptureResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[106] + mi := &file_daemon_proto_msgTypes[112] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6927,7 +7171,7 @@ func (x *StopBundleCaptureResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StopBundleCaptureResponse.ProtoReflect.Descriptor instead. func (*StopBundleCaptureResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{106} + return file_daemon_proto_rawDescGZIP(), []int{112} } type PortInfo_Range struct { @@ -6940,7 +7184,7 @@ type PortInfo_Range struct { func (x *PortInfo_Range) Reset() { *x = PortInfo_Range{} - mi := &file_daemon_proto_msgTypes[108] + mi := &file_daemon_proto_msgTypes[114] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6952,7 +7196,7 @@ func (x *PortInfo_Range) String() string { func (*PortInfo_Range) ProtoMessage() {} func (x *PortInfo_Range) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[108] + mi := &file_daemon_proto_msgTypes[114] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7070,11 +7314,14 @@ 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\"\x8c\x01\n" + + "\x05email\x18\x01 \x01(\tR\x05email\"\xac\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\x01\x12\x14\n" + - "\x05async\x18\x04 \x01(\bR\x05asyncB\x0e\n" + + "\x05async\x18\x04 \x01(\bR\x05async\x12\x1e\n" + + "\n" + + "claimOwner\x18\x05 \x01(\bR\n" + + "claimOwnerB\x0e\n" + "\f_profileNameB\v\n" + "\t_usernameJ\x04\b\x03\x10\x04\"\f\n" + "\n" + @@ -7424,7 +7671,15 @@ const file_daemon_proto_rawDesc = "" + "\busername\x18\x01 \x01(\tR\busername\x12 \n" + "\vprofileName\x18\x02 \x01(\tR\vprofileName\"$\n" + "\x12AddProfileResponse\x12\x0e\n" + - "\x02id\x18\x01 \x01(\tR\x02id\"r\n" + + "\x02id\x18\x01 \x01(\tR\x02id\"/\n" + + "\x0fAddOwnerRequest\x12\x1c\n" + + "\tprincipal\x18\x01 \x01(\tR\tprincipal\"\x12\n" + + "\x10AddOwnerResponse\"\x13\n" + + "\x11ResetOwnerRequest\"\x14\n" + + "\x12ResetOwnerResponse\"-\n" + + "\x13ShareProfileRequest\x12\x16\n" + + "\x06shared\x18\x01 \x01(\bR\x06shared\"\x16\n" + + "\x14ShareProfileResponse\"r\n" + "\x14RenameProfileRequest\x12\x1a\n" + "\busername\x18\x01 \x01(\tR\busername\x12\x16\n" + "\x06handle\x18\x02 \x01(\tR\x06handle\x12&\n" + @@ -7583,7 +7838,7 @@ const file_daemon_proto_rawDesc = "" + "\n" + "EXPOSE_UDP\x10\x03\x12\x0e\n" + "\n" + - "EXPOSE_TLS\x10\x042\xa3\x1c\n" + + "EXPOSE_TLS\x10\x042\xf8\x1d\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" + @@ -7619,7 +7874,11 @@ const file_daemon_proto_rawDesc = "" + "\rRenameProfile\x12\x1c.daemon.RenameProfileRequest\x1a\x1d.daemon.RenameProfileResponse\"\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\x12K\n" + + "\fShareProfile\x12\x1b.daemon.ShareProfileRequest\x1a\x1c.daemon.ShareProfileResponse\"\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" + @@ -7648,7 +7907,7 @@ func file_daemon_proto_rawDescGZIP() []byte { } var file_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 4) -var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 110) +var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 116) var file_daemon_proto_goTypes = []any{ (LogLevel)(0), // 0: daemon.LogLevel (ExposeProtocol)(0), // 1: daemon.ExposeProtocol @@ -7716,64 +7975,70 @@ var file_daemon_proto_goTypes = []any{ (*SetConfigResponse)(nil), // 63: daemon.SetConfigResponse (*AddProfileRequest)(nil), // 64: daemon.AddProfileRequest (*AddProfileResponse)(nil), // 65: daemon.AddProfileResponse - (*RenameProfileRequest)(nil), // 66: daemon.RenameProfileRequest - (*RenameProfileResponse)(nil), // 67: daemon.RenameProfileResponse - (*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 - (*WailsUIReadyRequest)(nil), // 77: daemon.WailsUIReadyRequest - (*WailsUIReadyResponse)(nil), // 78: daemon.WailsUIReadyResponse - (*GetFeaturesRequest)(nil), // 79: daemon.GetFeaturesRequest - (*GetFeaturesResponse)(nil), // 80: daemon.GetFeaturesResponse - (*MDMManagedFieldsViolation)(nil), // 81: daemon.MDMManagedFieldsViolation - (*TriggerUpdateRequest)(nil), // 82: daemon.TriggerUpdateRequest - (*TriggerUpdateResponse)(nil), // 83: daemon.TriggerUpdateResponse - (*GetPeerSSHHostKeyRequest)(nil), // 84: daemon.GetPeerSSHHostKeyRequest - (*GetPeerSSHHostKeyResponse)(nil), // 85: daemon.GetPeerSSHHostKeyResponse - (*RequestJWTAuthRequest)(nil), // 86: daemon.RequestJWTAuthRequest - (*RequestJWTAuthResponse)(nil), // 87: daemon.RequestJWTAuthResponse - (*WaitJWTTokenRequest)(nil), // 88: daemon.WaitJWTTokenRequest - (*WaitJWTTokenResponse)(nil), // 89: daemon.WaitJWTTokenResponse - (*RequestExtendAuthSessionRequest)(nil), // 90: daemon.RequestExtendAuthSessionRequest - (*RequestExtendAuthSessionResponse)(nil), // 91: daemon.RequestExtendAuthSessionResponse - (*WaitExtendAuthSessionRequest)(nil), // 92: daemon.WaitExtendAuthSessionRequest - (*WaitExtendAuthSessionResponse)(nil), // 93: daemon.WaitExtendAuthSessionResponse - (*DismissSessionWarningRequest)(nil), // 94: daemon.DismissSessionWarningRequest - (*DismissSessionWarningResponse)(nil), // 95: daemon.DismissSessionWarningResponse - (*StartCPUProfileRequest)(nil), // 96: daemon.StartCPUProfileRequest - (*StartCPUProfileResponse)(nil), // 97: daemon.StartCPUProfileResponse - (*StopCPUProfileRequest)(nil), // 98: daemon.StopCPUProfileRequest - (*StopCPUProfileResponse)(nil), // 99: daemon.StopCPUProfileResponse - (*InstallerResultRequest)(nil), // 100: daemon.InstallerResultRequest - (*InstallerResultResponse)(nil), // 101: daemon.InstallerResultResponse - (*ExposeServiceRequest)(nil), // 102: daemon.ExposeServiceRequest - (*ExposeServiceEvent)(nil), // 103: daemon.ExposeServiceEvent - (*ExposeServiceReady)(nil), // 104: daemon.ExposeServiceReady - (*StartCaptureRequest)(nil), // 105: daemon.StartCaptureRequest - (*CapturePacket)(nil), // 106: daemon.CapturePacket - (*StartBundleCaptureRequest)(nil), // 107: daemon.StartBundleCaptureRequest - (*StartBundleCaptureResponse)(nil), // 108: daemon.StartBundleCaptureResponse - (*StopBundleCaptureRequest)(nil), // 109: daemon.StopBundleCaptureRequest - (*StopBundleCaptureResponse)(nil), // 110: daemon.StopBundleCaptureResponse - nil, // 111: daemon.Network.ResolvedIPsEntry - (*PortInfo_Range)(nil), // 112: daemon.PortInfo.Range - nil, // 113: daemon.SystemEvent.MetadataEntry - (*durationpb.Duration)(nil), // 114: google.protobuf.Duration - (*timestamppb.Timestamp)(nil), // 115: google.protobuf.Timestamp + (*AddOwnerRequest)(nil), // 66: daemon.AddOwnerRequest + (*AddOwnerResponse)(nil), // 67: daemon.AddOwnerResponse + (*ResetOwnerRequest)(nil), // 68: daemon.ResetOwnerRequest + (*ResetOwnerResponse)(nil), // 69: daemon.ResetOwnerResponse + (*ShareProfileRequest)(nil), // 70: daemon.ShareProfileRequest + (*ShareProfileResponse)(nil), // 71: daemon.ShareProfileResponse + (*RenameProfileRequest)(nil), // 72: daemon.RenameProfileRequest + (*RenameProfileResponse)(nil), // 73: daemon.RenameProfileResponse + (*RemoveProfileRequest)(nil), // 74: daemon.RemoveProfileRequest + (*RemoveProfileResponse)(nil), // 75: daemon.RemoveProfileResponse + (*ListProfilesRequest)(nil), // 76: daemon.ListProfilesRequest + (*ListProfilesResponse)(nil), // 77: daemon.ListProfilesResponse + (*Profile)(nil), // 78: daemon.Profile + (*GetActiveProfileRequest)(nil), // 79: daemon.GetActiveProfileRequest + (*GetActiveProfileResponse)(nil), // 80: daemon.GetActiveProfileResponse + (*LogoutRequest)(nil), // 81: daemon.LogoutRequest + (*LogoutResponse)(nil), // 82: daemon.LogoutResponse + (*WailsUIReadyRequest)(nil), // 83: daemon.WailsUIReadyRequest + (*WailsUIReadyResponse)(nil), // 84: daemon.WailsUIReadyResponse + (*GetFeaturesRequest)(nil), // 85: daemon.GetFeaturesRequest + (*GetFeaturesResponse)(nil), // 86: daemon.GetFeaturesResponse + (*MDMManagedFieldsViolation)(nil), // 87: daemon.MDMManagedFieldsViolation + (*TriggerUpdateRequest)(nil), // 88: daemon.TriggerUpdateRequest + (*TriggerUpdateResponse)(nil), // 89: daemon.TriggerUpdateResponse + (*GetPeerSSHHostKeyRequest)(nil), // 90: daemon.GetPeerSSHHostKeyRequest + (*GetPeerSSHHostKeyResponse)(nil), // 91: daemon.GetPeerSSHHostKeyResponse + (*RequestJWTAuthRequest)(nil), // 92: daemon.RequestJWTAuthRequest + (*RequestJWTAuthResponse)(nil), // 93: daemon.RequestJWTAuthResponse + (*WaitJWTTokenRequest)(nil), // 94: daemon.WaitJWTTokenRequest + (*WaitJWTTokenResponse)(nil), // 95: daemon.WaitJWTTokenResponse + (*RequestExtendAuthSessionRequest)(nil), // 96: daemon.RequestExtendAuthSessionRequest + (*RequestExtendAuthSessionResponse)(nil), // 97: daemon.RequestExtendAuthSessionResponse + (*WaitExtendAuthSessionRequest)(nil), // 98: daemon.WaitExtendAuthSessionRequest + (*WaitExtendAuthSessionResponse)(nil), // 99: daemon.WaitExtendAuthSessionResponse + (*DismissSessionWarningRequest)(nil), // 100: daemon.DismissSessionWarningRequest + (*DismissSessionWarningResponse)(nil), // 101: daemon.DismissSessionWarningResponse + (*StartCPUProfileRequest)(nil), // 102: daemon.StartCPUProfileRequest + (*StartCPUProfileResponse)(nil), // 103: daemon.StartCPUProfileResponse + (*StopCPUProfileRequest)(nil), // 104: daemon.StopCPUProfileRequest + (*StopCPUProfileResponse)(nil), // 105: daemon.StopCPUProfileResponse + (*InstallerResultRequest)(nil), // 106: daemon.InstallerResultRequest + (*InstallerResultResponse)(nil), // 107: daemon.InstallerResultResponse + (*ExposeServiceRequest)(nil), // 108: daemon.ExposeServiceRequest + (*ExposeServiceEvent)(nil), // 109: daemon.ExposeServiceEvent + (*ExposeServiceReady)(nil), // 110: daemon.ExposeServiceReady + (*StartCaptureRequest)(nil), // 111: daemon.StartCaptureRequest + (*CapturePacket)(nil), // 112: daemon.CapturePacket + (*StartBundleCaptureRequest)(nil), // 113: daemon.StartBundleCaptureRequest + (*StartBundleCaptureResponse)(nil), // 114: daemon.StartBundleCaptureResponse + (*StopBundleCaptureRequest)(nil), // 115: daemon.StopBundleCaptureRequest + (*StopBundleCaptureResponse)(nil), // 116: daemon.StopBundleCaptureResponse + nil, // 117: daemon.Network.ResolvedIPsEntry + (*PortInfo_Range)(nil), // 118: daemon.PortInfo.Range + nil, // 119: daemon.SystemEvent.MetadataEntry + (*durationpb.Duration)(nil), // 120: google.protobuf.Duration + (*timestamppb.Timestamp)(nil), // 121: google.protobuf.Timestamp } var file_daemon_proto_depIdxs = []int32{ - 114, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration + 120, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration 25, // 1: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus - 115, // 2: daemon.StatusResponse.sessionExpiresAt:type_name -> google.protobuf.Timestamp - 115, // 3: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp - 115, // 4: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp - 114, // 5: daemon.PeerState.latency:type_name -> google.protobuf.Duration + 121, // 2: daemon.StatusResponse.sessionExpiresAt:type_name -> google.protobuf.Timestamp + 121, // 3: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp + 121, // 4: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp + 120, // 5: daemon.PeerState.latency:type_name -> google.protobuf.Duration 23, // 6: daemon.SSHServerState.sessions:type_name -> daemon.SSHSessionInfo 20, // 7: daemon.FullStatus.managementState:type_name -> daemon.ManagementState 19, // 8: daemon.FullStatus.signalState:type_name -> daemon.SignalState @@ -7784,8 +8049,8 @@ var file_daemon_proto_depIdxs = []int32{ 57, // 13: daemon.FullStatus.events:type_name -> daemon.SystemEvent 24, // 14: daemon.FullStatus.sshServerState:type_name -> daemon.SSHServerState 31, // 15: daemon.ListNetworksResponse.routes:type_name -> daemon.Network - 111, // 16: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry - 112, // 17: daemon.PortInfo.range:type_name -> daemon.PortInfo.Range + 117, // 16: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry + 118, // 17: daemon.PortInfo.range:type_name -> daemon.PortInfo.Range 32, // 18: daemon.ForwardingRule.destinationPort:type_name -> daemon.PortInfo 32, // 19: daemon.ForwardingRule.translatedPort:type_name -> daemon.PortInfo 33, // 20: daemon.ForwardingRulesResponse.rules:type_name -> daemon.ForwardingRule @@ -7796,16 +8061,16 @@ var file_daemon_proto_depIdxs = []int32{ 54, // 25: daemon.TracePacketResponse.stages:type_name -> daemon.TraceStage 2, // 26: daemon.SystemEvent.severity:type_name -> daemon.SystemEvent.Severity 3, // 27: daemon.SystemEvent.category:type_name -> daemon.SystemEvent.Category - 115, // 28: daemon.SystemEvent.timestamp:type_name -> google.protobuf.Timestamp - 113, // 29: daemon.SystemEvent.metadata:type_name -> daemon.SystemEvent.MetadataEntry + 121, // 28: daemon.SystemEvent.timestamp:type_name -> google.protobuf.Timestamp + 119, // 29: daemon.SystemEvent.metadata:type_name -> daemon.SystemEvent.MetadataEntry 57, // 30: daemon.GetEventsResponse.events:type_name -> daemon.SystemEvent - 114, // 31: daemon.SetConfigRequest.dnsRouteInterval:type_name -> google.protobuf.Duration - 72, // 32: daemon.ListProfilesResponse.profiles:type_name -> daemon.Profile - 115, // 33: daemon.WaitExtendAuthSessionResponse.sessionExpiresAt:type_name -> google.protobuf.Timestamp + 120, // 31: daemon.SetConfigRequest.dnsRouteInterval:type_name -> google.protobuf.Duration + 78, // 32: daemon.ListProfilesResponse.profiles:type_name -> daemon.Profile + 121, // 33: daemon.WaitExtendAuthSessionResponse.sessionExpiresAt:type_name -> google.protobuf.Timestamp 1, // 34: daemon.ExposeServiceRequest.protocol:type_name -> daemon.ExposeProtocol - 104, // 35: daemon.ExposeServiceEvent.ready:type_name -> daemon.ExposeServiceReady - 114, // 36: daemon.StartCaptureRequest.duration:type_name -> google.protobuf.Duration - 114, // 37: daemon.StartBundleCaptureRequest.timeout:type_name -> google.protobuf.Duration + 110, // 35: daemon.ExposeServiceEvent.ready:type_name -> daemon.ExposeServiceReady + 120, // 36: daemon.StartCaptureRequest.duration:type_name -> google.protobuf.Duration + 120, // 37: daemon.StartBundleCaptureRequest.timeout:type_name -> google.protobuf.Duration 30, // 38: daemon.Network.ResolvedIPsEntry.value:type_name -> daemon.IPList 5, // 39: daemon.DaemonService.Login:input_type -> daemon.LoginRequest 7, // 40: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest @@ -7826,81 +8091,87 @@ var file_daemon_proto_depIdxs = []int32{ 48, // 55: daemon.DaemonService.DeleteState:input_type -> daemon.DeleteStateRequest 50, // 56: daemon.DaemonService.SetSyncResponsePersistence:input_type -> daemon.SetSyncResponsePersistenceRequest 53, // 57: daemon.DaemonService.TracePacket:input_type -> daemon.TracePacketRequest - 105, // 58: daemon.DaemonService.StartCapture:input_type -> daemon.StartCaptureRequest - 107, // 59: daemon.DaemonService.StartBundleCapture:input_type -> daemon.StartBundleCaptureRequest - 109, // 60: daemon.DaemonService.StopBundleCapture:input_type -> daemon.StopBundleCaptureRequest + 111, // 58: daemon.DaemonService.StartCapture:input_type -> daemon.StartCaptureRequest + 113, // 59: daemon.DaemonService.StartBundleCapture:input_type -> daemon.StartBundleCaptureRequest + 115, // 60: daemon.DaemonService.StopBundleCapture:input_type -> daemon.StopBundleCaptureRequest 56, // 61: daemon.DaemonService.SubscribeEvents:input_type -> daemon.SubscribeRequest 58, // 62: daemon.DaemonService.GetEvents:input_type -> daemon.GetEventsRequest 41, // 63: daemon.DaemonService.RegisterUILog:input_type -> daemon.RegisterUILogRequest 60, // 64: daemon.DaemonService.SwitchProfile:input_type -> daemon.SwitchProfileRequest 62, // 65: daemon.DaemonService.SetConfig:input_type -> daemon.SetConfigRequest 64, // 66: daemon.DaemonService.AddProfile:input_type -> daemon.AddProfileRequest - 66, // 67: daemon.DaemonService.RenameProfile:input_type -> daemon.RenameProfileRequest - 68, // 68: daemon.DaemonService.RemoveProfile:input_type -> daemon.RemoveProfileRequest - 70, // 69: daemon.DaemonService.ListProfiles:input_type -> daemon.ListProfilesRequest - 73, // 70: daemon.DaemonService.GetActiveProfile:input_type -> daemon.GetActiveProfileRequest - 75, // 71: daemon.DaemonService.Logout:input_type -> daemon.LogoutRequest - 79, // 72: daemon.DaemonService.GetFeatures:input_type -> daemon.GetFeaturesRequest - 82, // 73: daemon.DaemonService.TriggerUpdate:input_type -> daemon.TriggerUpdateRequest - 84, // 74: daemon.DaemonService.GetPeerSSHHostKey:input_type -> daemon.GetPeerSSHHostKeyRequest - 86, // 75: daemon.DaemonService.RequestJWTAuth:input_type -> daemon.RequestJWTAuthRequest - 88, // 76: daemon.DaemonService.WaitJWTToken:input_type -> daemon.WaitJWTTokenRequest - 90, // 77: daemon.DaemonService.RequestExtendAuthSession:input_type -> daemon.RequestExtendAuthSessionRequest - 92, // 78: daemon.DaemonService.WaitExtendAuthSession:input_type -> daemon.WaitExtendAuthSessionRequest - 94, // 79: daemon.DaemonService.DismissSessionWarning:input_type -> daemon.DismissSessionWarningRequest - 96, // 80: daemon.DaemonService.StartCPUProfile:input_type -> daemon.StartCPUProfileRequest - 98, // 81: daemon.DaemonService.StopCPUProfile:input_type -> daemon.StopCPUProfileRequest - 100, // 82: daemon.DaemonService.GetInstallerResult:input_type -> daemon.InstallerResultRequest - 102, // 83: daemon.DaemonService.ExposeService:input_type -> daemon.ExposeServiceRequest - 77, // 84: daemon.DaemonService.WailsUIReady:input_type -> daemon.WailsUIReadyRequest - 6, // 85: daemon.DaemonService.Login:output_type -> daemon.LoginResponse - 8, // 86: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse - 10, // 87: daemon.DaemonService.Up:output_type -> daemon.UpResponse - 12, // 88: daemon.DaemonService.Status:output_type -> daemon.StatusResponse - 12, // 89: daemon.DaemonService.SubscribeStatus:output_type -> daemon.StatusResponse - 14, // 90: daemon.DaemonService.Down:output_type -> daemon.DownResponse - 16, // 91: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse - 27, // 92: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse - 29, // 93: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse - 29, // 94: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse - 34, // 95: daemon.DaemonService.ForwardingRules:output_type -> daemon.ForwardingRulesResponse - 36, // 96: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse - 38, // 97: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse - 40, // 98: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse - 45, // 99: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse - 47, // 100: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse - 49, // 101: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse - 51, // 102: daemon.DaemonService.SetSyncResponsePersistence:output_type -> daemon.SetSyncResponsePersistenceResponse - 55, // 103: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse - 106, // 104: daemon.DaemonService.StartCapture:output_type -> daemon.CapturePacket - 108, // 105: daemon.DaemonService.StartBundleCapture:output_type -> daemon.StartBundleCaptureResponse - 110, // 106: daemon.DaemonService.StopBundleCapture:output_type -> daemon.StopBundleCaptureResponse - 57, // 107: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent - 59, // 108: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse - 42, // 109: daemon.DaemonService.RegisterUILog:output_type -> daemon.RegisterUILogResponse - 61, // 110: daemon.DaemonService.SwitchProfile:output_type -> daemon.SwitchProfileResponse - 63, // 111: daemon.DaemonService.SetConfig:output_type -> daemon.SetConfigResponse - 65, // 112: daemon.DaemonService.AddProfile:output_type -> daemon.AddProfileResponse - 67, // 113: daemon.DaemonService.RenameProfile:output_type -> daemon.RenameProfileResponse - 69, // 114: daemon.DaemonService.RemoveProfile:output_type -> daemon.RemoveProfileResponse - 71, // 115: daemon.DaemonService.ListProfiles:output_type -> daemon.ListProfilesResponse - 74, // 116: daemon.DaemonService.GetActiveProfile:output_type -> daemon.GetActiveProfileResponse - 76, // 117: daemon.DaemonService.Logout:output_type -> daemon.LogoutResponse - 80, // 118: daemon.DaemonService.GetFeatures:output_type -> daemon.GetFeaturesResponse - 83, // 119: daemon.DaemonService.TriggerUpdate:output_type -> daemon.TriggerUpdateResponse - 85, // 120: daemon.DaemonService.GetPeerSSHHostKey:output_type -> daemon.GetPeerSSHHostKeyResponse - 87, // 121: daemon.DaemonService.RequestJWTAuth:output_type -> daemon.RequestJWTAuthResponse - 89, // 122: daemon.DaemonService.WaitJWTToken:output_type -> daemon.WaitJWTTokenResponse - 91, // 123: daemon.DaemonService.RequestExtendAuthSession:output_type -> daemon.RequestExtendAuthSessionResponse - 93, // 124: daemon.DaemonService.WaitExtendAuthSession:output_type -> daemon.WaitExtendAuthSessionResponse - 95, // 125: daemon.DaemonService.DismissSessionWarning:output_type -> daemon.DismissSessionWarningResponse - 97, // 126: daemon.DaemonService.StartCPUProfile:output_type -> daemon.StartCPUProfileResponse - 99, // 127: daemon.DaemonService.StopCPUProfile:output_type -> daemon.StopCPUProfileResponse - 101, // 128: daemon.DaemonService.GetInstallerResult:output_type -> daemon.InstallerResultResponse - 103, // 129: daemon.DaemonService.ExposeService:output_type -> daemon.ExposeServiceEvent - 78, // 130: daemon.DaemonService.WailsUIReady:output_type -> daemon.WailsUIReadyResponse - 85, // [85:131] is the sub-list for method output_type - 39, // [39:85] is the sub-list for method input_type + 72, // 67: daemon.DaemonService.RenameProfile:input_type -> daemon.RenameProfileRequest + 74, // 68: daemon.DaemonService.RemoveProfile:input_type -> daemon.RemoveProfileRequest + 76, // 69: daemon.DaemonService.ListProfiles:input_type -> daemon.ListProfilesRequest + 79, // 70: daemon.DaemonService.GetActiveProfile:input_type -> daemon.GetActiveProfileRequest + 66, // 71: daemon.DaemonService.AddOwner:input_type -> daemon.AddOwnerRequest + 68, // 72: daemon.DaemonService.ResetOwner:input_type -> daemon.ResetOwnerRequest + 70, // 73: daemon.DaemonService.ShareProfile:input_type -> daemon.ShareProfileRequest + 81, // 74: daemon.DaemonService.Logout:input_type -> daemon.LogoutRequest + 85, // 75: daemon.DaemonService.GetFeatures:input_type -> daemon.GetFeaturesRequest + 88, // 76: daemon.DaemonService.TriggerUpdate:input_type -> daemon.TriggerUpdateRequest + 90, // 77: daemon.DaemonService.GetPeerSSHHostKey:input_type -> daemon.GetPeerSSHHostKeyRequest + 92, // 78: daemon.DaemonService.RequestJWTAuth:input_type -> daemon.RequestJWTAuthRequest + 94, // 79: daemon.DaemonService.WaitJWTToken:input_type -> daemon.WaitJWTTokenRequest + 96, // 80: daemon.DaemonService.RequestExtendAuthSession:input_type -> daemon.RequestExtendAuthSessionRequest + 98, // 81: daemon.DaemonService.WaitExtendAuthSession:input_type -> daemon.WaitExtendAuthSessionRequest + 100, // 82: daemon.DaemonService.DismissSessionWarning:input_type -> daemon.DismissSessionWarningRequest + 102, // 83: daemon.DaemonService.StartCPUProfile:input_type -> daemon.StartCPUProfileRequest + 104, // 84: daemon.DaemonService.StopCPUProfile:input_type -> daemon.StopCPUProfileRequest + 106, // 85: daemon.DaemonService.GetInstallerResult:input_type -> daemon.InstallerResultRequest + 108, // 86: daemon.DaemonService.ExposeService:input_type -> daemon.ExposeServiceRequest + 83, // 87: daemon.DaemonService.WailsUIReady:input_type -> daemon.WailsUIReadyRequest + 6, // 88: daemon.DaemonService.Login:output_type -> daemon.LoginResponse + 8, // 89: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse + 10, // 90: daemon.DaemonService.Up:output_type -> daemon.UpResponse + 12, // 91: daemon.DaemonService.Status:output_type -> daemon.StatusResponse + 12, // 92: daemon.DaemonService.SubscribeStatus:output_type -> daemon.StatusResponse + 14, // 93: daemon.DaemonService.Down:output_type -> daemon.DownResponse + 16, // 94: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse + 27, // 95: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse + 29, // 96: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse + 29, // 97: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse + 34, // 98: daemon.DaemonService.ForwardingRules:output_type -> daemon.ForwardingRulesResponse + 36, // 99: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse + 38, // 100: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse + 40, // 101: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse + 45, // 102: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse + 47, // 103: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse + 49, // 104: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse + 51, // 105: daemon.DaemonService.SetSyncResponsePersistence:output_type -> daemon.SetSyncResponsePersistenceResponse + 55, // 106: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse + 112, // 107: daemon.DaemonService.StartCapture:output_type -> daemon.CapturePacket + 114, // 108: daemon.DaemonService.StartBundleCapture:output_type -> daemon.StartBundleCaptureResponse + 116, // 109: daemon.DaemonService.StopBundleCapture:output_type -> daemon.StopBundleCaptureResponse + 57, // 110: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent + 59, // 111: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse + 42, // 112: daemon.DaemonService.RegisterUILog:output_type -> daemon.RegisterUILogResponse + 61, // 113: daemon.DaemonService.SwitchProfile:output_type -> daemon.SwitchProfileResponse + 63, // 114: daemon.DaemonService.SetConfig:output_type -> daemon.SetConfigResponse + 65, // 115: daemon.DaemonService.AddProfile:output_type -> daemon.AddProfileResponse + 73, // 116: daemon.DaemonService.RenameProfile:output_type -> daemon.RenameProfileResponse + 75, // 117: daemon.DaemonService.RemoveProfile:output_type -> daemon.RemoveProfileResponse + 77, // 118: daemon.DaemonService.ListProfiles:output_type -> daemon.ListProfilesResponse + 80, // 119: daemon.DaemonService.GetActiveProfile:output_type -> daemon.GetActiveProfileResponse + 67, // 120: daemon.DaemonService.AddOwner:output_type -> daemon.AddOwnerResponse + 69, // 121: daemon.DaemonService.ResetOwner:output_type -> daemon.ResetOwnerResponse + 71, // 122: daemon.DaemonService.ShareProfile:output_type -> daemon.ShareProfileResponse + 82, // 123: daemon.DaemonService.Logout:output_type -> daemon.LogoutResponse + 86, // 124: daemon.DaemonService.GetFeatures:output_type -> daemon.GetFeaturesResponse + 89, // 125: daemon.DaemonService.TriggerUpdate:output_type -> daemon.TriggerUpdateResponse + 91, // 126: daemon.DaemonService.GetPeerSSHHostKey:output_type -> daemon.GetPeerSSHHostKeyResponse + 93, // 127: daemon.DaemonService.RequestJWTAuth:output_type -> daemon.RequestJWTAuthResponse + 95, // 128: daemon.DaemonService.WaitJWTToken:output_type -> daemon.WaitJWTTokenResponse + 97, // 129: daemon.DaemonService.RequestExtendAuthSession:output_type -> daemon.RequestExtendAuthSessionResponse + 99, // 130: daemon.DaemonService.WaitExtendAuthSession:output_type -> daemon.WaitExtendAuthSessionResponse + 101, // 131: daemon.DaemonService.DismissSessionWarning:output_type -> daemon.DismissSessionWarningResponse + 103, // 132: daemon.DaemonService.StartCPUProfile:output_type -> daemon.StartCPUProfileResponse + 105, // 133: daemon.DaemonService.StopCPUProfile:output_type -> daemon.StopCPUProfileResponse + 107, // 134: daemon.DaemonService.GetInstallerResult:output_type -> daemon.InstallerResultResponse + 109, // 135: daemon.DaemonService.ExposeService:output_type -> daemon.ExposeServiceEvent + 84, // 136: daemon.DaemonService.WailsUIReady:output_type -> daemon.WailsUIReadyResponse + 88, // [88:137] is the sub-list for method output_type + 39, // [39:88] is the sub-list for method input_type 39, // [39:39] is the sub-list for extension type_name 39, // [39:39] is the sub-list for extension extendee 0, // [0:39] is the sub-list for field type_name @@ -7922,11 +8193,11 @@ func file_daemon_proto_init() { file_daemon_proto_msgTypes[50].OneofWrappers = []any{} file_daemon_proto_msgTypes[56].OneofWrappers = []any{} file_daemon_proto_msgTypes[58].OneofWrappers = []any{} - file_daemon_proto_msgTypes[71].OneofWrappers = []any{} - file_daemon_proto_msgTypes[76].OneofWrappers = []any{} + file_daemon_proto_msgTypes[77].OneofWrappers = []any{} file_daemon_proto_msgTypes[82].OneofWrappers = []any{} - file_daemon_proto_msgTypes[86].OneofWrappers = []any{} - file_daemon_proto_msgTypes[99].OneofWrappers = []any{ + file_daemon_proto_msgTypes[88].OneofWrappers = []any{} + file_daemon_proto_msgTypes[92].OneofWrappers = []any{} + file_daemon_proto_msgTypes[105].OneofWrappers = []any{ (*ExposeServiceEvent_Ready)(nil), } type x struct{} @@ -7935,7 +8206,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: 110, + NumMessages: 116, NumExtensions: 0, NumServices: 1, }, diff --git a/client/proto/daemon.pb.gw.go b/client/proto/daemon.pb.gw.go index b64dfeea1..b223051eb 100644 --- a/client/proto/daemon.pb.gw.go +++ b/client/proto/daemon.pb.gw.go @@ -791,6 +791,78 @@ func local_request_DaemonService_GetActiveProfile_0(ctx context.Context, marshal return msg, metadata, err } +func request_DaemonService_AddOwner_0(ctx context.Context, marshaler runtime.Marshaler, client DaemonServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq AddOwnerRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.AddOwner(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_DaemonService_AddOwner_0(ctx context.Context, marshaler runtime.Marshaler, server DaemonServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq AddOwnerRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.AddOwner(ctx, &protoReq) + return msg, metadata, err +} + +func request_DaemonService_ResetOwner_0(ctx context.Context, marshaler runtime.Marshaler, client DaemonServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ResetOwnerRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.ResetOwner(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_DaemonService_ResetOwner_0(ctx context.Context, marshaler runtime.Marshaler, server DaemonServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ResetOwnerRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ResetOwner(ctx, &protoReq) + return msg, metadata, err +} + +func request_DaemonService_ShareProfile_0(ctx context.Context, marshaler runtime.Marshaler, client DaemonServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ShareProfileRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.ShareProfile(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_DaemonService_ShareProfile_0(ctx context.Context, marshaler runtime.Marshaler, server DaemonServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ShareProfileRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ShareProfile(ctx, &protoReq) + return msg, metadata, err +} + func request_DaemonService_Logout_0(ctx context.Context, marshaler runtime.Marshaler, client DaemonServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq LogoutRequest @@ -1730,6 +1802,66 @@ func RegisterDaemonServiceHandlerServer(ctx context.Context, mux *runtime.ServeM } forward_DaemonService_GetActiveProfile_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) + mux.Handle(http.MethodPost, pattern_DaemonService_AddOwner_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/daemon.DaemonService/AddOwner", runtime.WithHTTPPathPattern("/daemon.DaemonService/AddOwner")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_DaemonService_AddOwner_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_DaemonService_AddOwner_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_DaemonService_ResetOwner_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/daemon.DaemonService/ResetOwner", runtime.WithHTTPPathPattern("/daemon.DaemonService/ResetOwner")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_DaemonService_ResetOwner_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_DaemonService_ResetOwner_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_DaemonService_ShareProfile_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/daemon.DaemonService/ShareProfile", runtime.WithHTTPPathPattern("/daemon.DaemonService/ShareProfile")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_DaemonService_ShareProfile_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_DaemonService_ShareProfile_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) mux.Handle(http.MethodPost, pattern_DaemonService_Logout_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -2581,6 +2713,57 @@ func RegisterDaemonServiceHandlerClient(ctx context.Context, mux *runtime.ServeM } forward_DaemonService_GetActiveProfile_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) + mux.Handle(http.MethodPost, pattern_DaemonService_AddOwner_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/daemon.DaemonService/AddOwner", runtime.WithHTTPPathPattern("/daemon.DaemonService/AddOwner")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_DaemonService_AddOwner_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_DaemonService_AddOwner_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_DaemonService_ResetOwner_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/daemon.DaemonService/ResetOwner", runtime.WithHTTPPathPattern("/daemon.DaemonService/ResetOwner")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_DaemonService_ResetOwner_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_DaemonService_ResetOwner_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_DaemonService_ShareProfile_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/daemon.DaemonService/ShareProfile", runtime.WithHTTPPathPattern("/daemon.DaemonService/ShareProfile")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_DaemonService_ShareProfile_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_DaemonService_ShareProfile_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) mux.Handle(http.MethodPost, pattern_DaemonService_Logout_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -2855,6 +3038,9 @@ var ( pattern_DaemonService_RemoveProfile_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"daemon.DaemonService", "RemoveProfile"}, "")) pattern_DaemonService_ListProfiles_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"daemon.DaemonService", "ListProfiles"}, "")) pattern_DaemonService_GetActiveProfile_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"daemon.DaemonService", "GetActiveProfile"}, "")) + pattern_DaemonService_AddOwner_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"daemon.DaemonService", "AddOwner"}, "")) + pattern_DaemonService_ResetOwner_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"daemon.DaemonService", "ResetOwner"}, "")) + pattern_DaemonService_ShareProfile_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"daemon.DaemonService", "ShareProfile"}, "")) pattern_DaemonService_Logout_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"daemon.DaemonService", "Logout"}, "")) pattern_DaemonService_GetFeatures_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"daemon.DaemonService", "GetFeatures"}, "")) pattern_DaemonService_TriggerUpdate_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"daemon.DaemonService", "TriggerUpdate"}, "")) @@ -2904,6 +3090,9 @@ var ( forward_DaemonService_RemoveProfile_0 = runtime.ForwardResponseMessage forward_DaemonService_ListProfiles_0 = runtime.ForwardResponseMessage forward_DaemonService_GetActiveProfile_0 = runtime.ForwardResponseMessage + forward_DaemonService_AddOwner_0 = runtime.ForwardResponseMessage + forward_DaemonService_ResetOwner_0 = runtime.ForwardResponseMessage + forward_DaemonService_ShareProfile_0 = runtime.ForwardResponseMessage forward_DaemonService_Logout_0 = runtime.ForwardResponseMessage forward_DaemonService_GetFeatures_0 = runtime.ForwardResponseMessage forward_DaemonService_TriggerUpdate_0 = runtime.ForwardResponseMessage diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index 8d5294eb7..18e410259 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -104,6 +104,18 @@ service DaemonService { rpc GetActiveProfile(GetActiveProfileRequest) returns (GetActiveProfileResponse) {} + // AddOwner adds a principal to the active profile's owner list. Requires the + // caller to be privileged (root/administrator) or an existing owner. + rpc AddOwner(AddOwnerRequest) returns (AddOwnerResponse) {} + + // ResetOwner clears the active profile's owner list, returning it to the + // unowned state (next caller re-claims via trust-on-first-use). Privileged only. + rpc ResetOwner(ResetOwnerRequest) returns (ResetOwnerResponse) {} + + // ShareProfile marks the active profile shared (any local caller) or unshared. + // Requires the caller to be an owner or privileged. + rpc ShareProfile(ShareProfileRequest) returns (ShareProfileResponse) {} + // Logout disconnects from the network and deletes the peer from the management server rpc Logout(LogoutRequest) returns (LogoutResponse) {} @@ -270,6 +282,10 @@ message UpRequest { // RPC blocks until the engine is running or gives up, which is the behaviour // needed by the CLI. bool async = 4; + + // claimOwner, when true, claims ownership of the active profile for the + // calling user (adds their kernel principal to the profile's owner list). + bool claimOwner = 5; } message UpResponse {} @@ -774,6 +790,24 @@ message AddProfileResponse { string id = 1; } +message AddOwnerRequest { + // principal is a typed owner string: "uid:1000", "gid:1000", + // "group:netbird-admins" (Unix, NSS-resolved), or "sid:S-1-5-..." (Windows). + string principal = 1; +} + +message AddOwnerResponse {} + +message ResetOwnerRequest {} + +message ResetOwnerResponse {} + +message ShareProfileRequest { + bool shared = 1; +} + +message ShareProfileResponse {} + message RenameProfileRequest { string username = 1; // handle: an exact ID, a unique ID prefix, or a unique display name. diff --git a/client/proto/daemon_grpc.pb.go b/client/proto/daemon_grpc.pb.go index 2d01d474d..9b67e56a4 100644 --- a/client/proto/daemon_grpc.pb.go +++ b/client/proto/daemon_grpc.pb.go @@ -51,6 +51,9 @@ 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_ShareProfile_FullMethodName = "/daemon.DaemonService/ShareProfile" DaemonService_Logout_FullMethodName = "/daemon.DaemonService/Logout" DaemonService_GetFeatures_FullMethodName = "/daemon.DaemonService/GetFeatures" DaemonService_TriggerUpdate_FullMethodName = "/daemon.DaemonService/TriggerUpdate" @@ -132,6 +135,15 @@ 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 principal to the active profile's owner list. Requires the + // caller to be privileged (root/administrator) 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 + // unowned state (next caller re-claims via trust-on-first-use). Privileged only. + ResetOwner(ctx context.Context, in *ResetOwnerRequest, opts ...grpc.CallOption) (*ResetOwnerResponse, error) + // ShareProfile marks the active profile shared (any local caller) or unshared. + // Requires the caller to be an owner or privileged. + ShareProfile(ctx context.Context, in *ShareProfileRequest, opts ...grpc.CallOption) (*ShareProfileResponse, 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) @@ -528,6 +540,36 @@ 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) ShareProfile(ctx context.Context, in *ShareProfileRequest, opts ...grpc.CallOption) (*ShareProfileResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ShareProfileResponse) + err := c.cc.Invoke(ctx, DaemonService_ShareProfile_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) @@ -742,6 +784,15 @@ type DaemonServiceServer interface { RemoveProfile(context.Context, *RemoveProfileRequest) (*RemoveProfileResponse, error) ListProfiles(context.Context, *ListProfilesRequest) (*ListProfilesResponse, error) GetActiveProfile(context.Context, *GetActiveProfileRequest) (*GetActiveProfileResponse, error) + // AddOwner adds a principal to the active profile's owner list. Requires the + // caller to be privileged (root/administrator) or an existing owner. + AddOwner(context.Context, *AddOwnerRequest) (*AddOwnerResponse, error) + // ResetOwner clears the active profile's owner list, returning it to the + // unowned state (next caller re-claims via trust-on-first-use). Privileged only. + ResetOwner(context.Context, *ResetOwnerRequest) (*ResetOwnerResponse, error) + // ShareProfile marks the active profile shared (any local caller) or unshared. + // Requires the caller to be an owner or privileged. + ShareProfile(context.Context, *ShareProfileRequest) (*ShareProfileResponse, 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) @@ -887,6 +938,15 @@ 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) ShareProfile(context.Context, *ShareProfileRequest) (*ShareProfileResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ShareProfile not implemented") +} func (UnimplementedDaemonServiceServer) Logout(context.Context, *LogoutRequest) (*LogoutResponse, error) { return nil, status.Error(codes.Unimplemented, "method Logout not implemented") } @@ -1505,6 +1565,60 @@ 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_ShareProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ShareProfileRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).ShareProfile(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DaemonService_ShareProfile_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).ShareProfile(ctx, req.(*ShareProfileRequest)) + } + 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 { @@ -1873,6 +1987,18 @@ var DaemonService_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetActiveProfile", Handler: _DaemonService_GetActiveProfile_Handler, }, + { + MethodName: "AddOwner", + Handler: _DaemonService_AddOwner_Handler, + }, + { + MethodName: "ResetOwner", + Handler: _DaemonService_ResetOwner_Handler, + }, + { + MethodName: "ShareProfile", + Handler: _DaemonService_ShareProfile_Handler, + }, { MethodName: "Logout", Handler: _DaemonService_Logout_Handler, diff --git a/client/server/ownership.go b/client/server/ownership.go new file mode 100644 index 000000000..3d94dff4a --- /dev/null +++ b/client/server/ownership.go @@ -0,0 +1,204 @@ +package server + +import ( + "context" + "fmt" + "slices" + + log "github.com/sirupsen/logrus" + "google.golang.org/grpc/codes" + gstatus "google.golang.org/grpc/status" + + "github.com/netbirdio/netbird/client/internal/ipcauth" + "github.com/netbirdio/netbird/client/internal/profilemanager" + "github.com/netbirdio/netbird/client/proto" + "github.com/netbirdio/netbird/util" +) + +// The daemon Server implements ipcauth.ProfilePolicy so the gRPC interceptor can +// authorize each RPC against the active profile's ownership. +var _ ipcauth.ProfilePolicy = (*Server)(nil) + +// ActiveProfileOwnership returns the active profile's ownership policy. Reads +// the in-memory active config (kept current by the handlers), falling back to +// the on-disk active profile when the daemon hasn't loaded one yet. +func (s *Server) ActiveProfileOwnership() ipcauth.Ownership { + s.mutex.Lock() + defer s.mutex.Unlock() + + cfg := s.config + if cfg == nil { + loaded, err := s.loadActiveProfileConfigLocked() + if err != nil { + log.Warnf("ownership: cannot load active profile config, treating as unowned: %v", err) + return ipcauth.Ownership{} + } + cfg = loaded + } + return ipcauth.Ownership{Owners: cfg.Owners, Shared: cfg.Shared} +} + +// ClaimActiveProfileOwnerIfUnowned atomically claims the active profile for id +// when it has no owners and is not shared (trust-on-first-use). Returns whether +// id is now an owner. Concurrent first-callers are serialized by s.mutex, so +// exactly one wins the claim; the others get false and are authorized normally. +func (s *Server) ClaimActiveProfileOwnerIfUnowned(id ipcauth.Identity) (bool, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + cfg := s.config + if cfg == nil { + loaded, err := s.loadActiveProfileConfigLocked() + if err != nil { + return false, fmt.Errorf("load active profile config: %w", err) + } + cfg = loaded + } + + if len(cfg.Owners) > 0 || cfg.Shared { + return false, nil // already owned or shared — someone won the race + } + + cfg.Owners = []string{ipcauth.OwnerPrincipalForIdentity(id)} + if err := s.persistActiveProfileConfigLocked(cfg); err != nil { + cfg.Owners = nil // revert in-memory on persistence failure + return false, fmt.Errorf("persist claimed ownership: %w", err) + } + s.config = cfg + log.Infof("profile ownership claimed by %s (trust-on-first-use)", id) + return true, nil +} + +// activeProfileConfigPathLocked resolves the active profile's config file path. +func (s *Server) activeProfileConfigPathLocked() (string, error) { + activeProf, err := s.profileManager.GetActiveProfileState() + if err != nil { + return "", fmt.Errorf("get active profile: %w", err) + } + path, err := activeProf.FilePath() + if err != nil { + return "", fmt.Errorf("resolve active profile path: %w", err) + } + return path, nil +} + +// loadActiveProfileConfigLocked reads the active profile's config from disk. +func (s *Server) loadActiveProfileConfigLocked() (*profilemanager.Config, error) { + path, err := s.activeProfileConfigPathLocked() + if err != nil { + return nil, err + } + return profilemanager.GetConfig(path) +} + +// persistActiveProfileConfigLocked writes cfg to the active profile's config file. +func (s *Server) persistActiveProfileConfigLocked(cfg *profilemanager.Config) error { + path, err := s.activeProfileConfigPathLocked() + if err != nil { + return err + } + return util.WriteJson(context.Background(), path, cfg) +} + +// activeConfigLocked returns the in-memory active config, loading it from disk +// if the daemon hasn't cached one. Caller must hold s.mutex. +func (s *Server) activeConfigLocked() (*profilemanager.Config, error) { + if s.config != nil { + return s.config, nil + } + return s.loadActiveProfileConfigLocked() +} + +// claimForCallerLocked adds the caller's principal to cfg (if absent) and +// persists. No-op for privileged callers (they need no ownership entry). Caller +// must hold s.mutex. +func (s *Server) claimForCallerLocked(id ipcauth.Identity, cfg *profilemanager.Config) error { + if id.IsPrivileged() { + return nil + } + principal := ipcauth.OwnerPrincipalForIdentity(id) + if slices.Contains(cfg.Owners, principal) { + return nil + } + cfg.Owners = append(cfg.Owners, principal) + if err := s.persistActiveProfileConfigLocked(cfg); err != nil { + cfg.Owners = cfg.Owners[:len(cfg.Owners)-1] // revert on failure + return err + } + s.config = cfg + return nil +} + +// AddOwner adds a principal to the active profile's owner list. The interceptor +// has already confirmed the caller is an owner or privileged; the handler just +// validates and persists. +func (s *Server) AddOwner(_ context.Context, msg *proto.AddOwnerRequest) (*proto.AddOwnerResponse, error) { + principal := msg.GetPrincipal() + if _, ok := ipcauth.ParsePrincipal(principal); !ok { + return nil, gstatus.Errorf(codes.InvalidArgument, "invalid owner principal %q (expected uid:/gid:/group:/sid:)", principal) + } + + s.mutex.Lock() + defer s.mutex.Unlock() + + cfg, err := s.activeConfigLocked() + if err != nil { + return nil, fmt.Errorf("load active profile config: %w", err) + } + if slices.Contains(cfg.Owners, principal) { + return &proto.AddOwnerResponse{}, nil + } + cfg.Owners = append(cfg.Owners, principal) + if err := s.persistActiveProfileConfigLocked(cfg); err != nil { + cfg.Owners = cfg.Owners[:len(cfg.Owners)-1] + return nil, fmt.Errorf("persist owner: %w", err) + } + s.config = cfg + log.Infof("added owner %q to the active profile", principal) + return &proto.AddOwnerResponse{}, nil +} + +// ResetOwner clears the active profile's owner list (and shared flag), returning +// it to the unowned state so the next caller re-claims via trust-on-first-use. +// Privileged-only, so co-owners cannot evict each other. +func (s *Server) ResetOwner(ctx context.Context, _ *proto.ResetOwnerRequest) (*proto.ResetOwnerResponse, error) { + id, ok := ipcauth.IdentityFromContext(ctx) + if !ok || !id.IsPrivileged() { + return nil, gstatus.Error(codes.PermissionDenied, "reset-owner requires root/administrator") + } + + s.mutex.Lock() + defer s.mutex.Unlock() + + cfg, err := s.activeConfigLocked() + if err != nil { + return nil, fmt.Errorf("load active profile config: %w", err) + } + cfg.Owners = nil + cfg.Shared = false + if err := s.persistActiveProfileConfigLocked(cfg); err != nil { + return nil, fmt.Errorf("persist owner reset: %w", err) + } + s.config = cfg + log.Infof("active profile owner list reset; next caller will re-claim (trust-on-first-use)") + return &proto.ResetOwnerResponse{}, nil +} + +// ShareProfile marks the active profile shared or unshared. The interceptor has +// already confirmed the caller is an owner or privileged. +func (s *Server) ShareProfile(_ context.Context, msg *proto.ShareProfileRequest) (*proto.ShareProfileResponse, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + cfg, err := s.activeConfigLocked() + if err != nil { + return nil, fmt.Errorf("load active profile config: %w", err) + } + cfg.Shared = msg.GetShared() + if err := s.persistActiveProfileConfigLocked(cfg); err != nil { + return nil, fmt.Errorf("persist shared flag: %w", err) + } + s.config = cfg + log.Infof("active profile shared flag set to %t", msg.GetShared()) + return &proto.ShareProfileResponse{}, nil +} diff --git a/client/server/profile_authz.go b/client/server/profile_authz.go new file mode 100644 index 000000000..b1a3876ae --- /dev/null +++ b/client/server/profile_authz.go @@ -0,0 +1,53 @@ +package server + +import ( + "context" + "runtime" + "strings" + + log "github.com/sirupsen/logrus" + "google.golang.org/grpc/codes" + gstatus "google.golang.org/grpc/status" + + "github.com/netbirdio/netbird/client/internal/ipcauth" +) + +// bindCallerUsername enforces that a non-privileged caller may only operate on +// its OWN user's profiles. Profiles live in per-username directories, but the +// username is a client-supplied gRPC field the server historically never +// checked; binding it to the caller's kernel identity closes the "pass another +// user's username" hole. Privileged callers (root / elevated-admin) may manage +// any user's profiles. +func (s *Server) bindCallerUsername(ctx context.Context, requested string) error { + if requested == "" { + return nil // handlers validate emptiness themselves; nothing to bind + } + + id, ok := ipcauth.IdentityFromContext(ctx) + if !ok { + return gstatus.Error(codes.PermissionDenied, "caller identity could not be verified") + } + if id.IsPrivileged() { + return nil + } + + caller, err := usernameForIdentity(id) + if err != nil { + log.Warnf("profile authz: resolve caller username for %s: %v", id, err) + return gstatus.Error(codes.PermissionDenied, "could not resolve caller identity to a username") + } + if !usernamesEqual(requested, caller) { + return gstatus.Errorf(codes.PermissionDenied, + "not authorized to operate on another user's profiles (caller %q requested %q)", caller, requested) + } + return nil +} + +// usernamesEqual compares usernames case-insensitively on Windows (domain +// accounts) and exactly on Unix. +func usernamesEqual(a, b string) bool { + if runtime.GOOS == "windows" { + return strings.EqualFold(a, b) + } + return a == b +} diff --git a/client/server/profile_authz_unix.go b/client/server/profile_authz_unix.go new file mode 100644 index 000000000..eafa7fd23 --- /dev/null +++ b/client/server/profile_authz_unix.go @@ -0,0 +1,20 @@ +//go:build !windows + +package server + +import ( + "strconv" + + "github.com/netbirdio/netbird/client/internal/ipcauth" + "github.com/netbirdio/netbird/client/internal/shell" +) + +// usernameForIdentity resolves a Unix caller's UID to its username via NSS +// (getent), so LDAP/AD users resolve correctly under CGO_ENABLED=0. +func usernameForIdentity(id ipcauth.Identity) (string, error) { + u, err := shell.GetUserFromGetent(strconv.FormatUint(uint64(id.UID), 10)) + if err != nil { + return "", err + } + return u.Username, nil +} diff --git a/client/server/profile_authz_windows.go b/client/server/profile_authz_windows.go new file mode 100644 index 000000000..b71755654 --- /dev/null +++ b/client/server/profile_authz_windows.go @@ -0,0 +1,18 @@ +//go:build windows + +package server + +import ( + "os/user" + + "github.com/netbirdio/netbird/client/internal/ipcauth" +) + +// usernameForIdentity resolves a Windows caller's SID to its account name. +func usernameForIdentity(id ipcauth.Identity) (string, error) { + u, err := user.LookupId(id.SID) + if err != nil { + return "", err + } + return u.Username, nil +} diff --git a/client/server/server.go b/client/server/server.go index 8047006fe..8a071d079 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -23,6 +23,7 @@ import ( "github.com/netbirdio/netbird/client/internal/auth" "github.com/netbirdio/netbird/client/internal/expose" + "github.com/netbirdio/netbird/client/internal/ipcauth" "github.com/netbirdio/netbird/client/internal/profilemanager" sleephandler "github.com/netbirdio/netbird/client/internal/sleep/handler" "github.com/netbirdio/netbird/client/mdm" @@ -402,6 +403,12 @@ func (s *Server) SetConfig(callerCtx context.Context, msg *proto.SetConfigReques } } + // M-SSHGATE: turning on SSH root login / disabling SSH auth requires a + // privileged caller (root or elevated admin). + if err := requirePrivilegedForDangerousSSH(callerCtx, msg.EnableSSHRoot, msg.DisableSSHAuth); err != nil { + return nil, err + } + // MDM gate: refuse the whole request if any of its fields is enforced // by the active MDM policy. The error carries an MDMManagedFields- // Violation detail listing the offending key names. Non-conflicting @@ -537,6 +544,12 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro } } + // M-SSHGATE: turning on SSH root login / disabling SSH auth requires a + // privileged caller (root or elevated admin). + if err := requirePrivilegedForDangerousSSH(callerCtx, msg.EnableSSHRoot, msg.DisableSSHAuth); err != nil { + return nil, err + } + s.mutex.Lock() if s.actCancel != nil { s.actCancel() @@ -943,6 +956,17 @@ func (s *Server) Up(callerCtx context.Context, msg *proto.UpRequest) (*proto.UpR } s.config = config + // --owner: explicitly claim ownership of the active profile for the caller. + if msg != nil && msg.GetClaimOwner() { + if id, ok := ipcauth.IdentityFromContext(callerCtx); ok { + if err := s.claimForCallerLocked(id, s.config); err != nil { + s.mutex.Unlock() + log.Errorf("failed to claim profile ownership: %v", err) + return nil, fmt.Errorf("claim owner: %w", err) + } + } + } + s.statusRecorder.UpdateManagementAddress(s.config.ManagementURL.String()) s.statusRecorder.UpdateRosenpass(s.config.RosenpassEnabled, s.config.RosenpassPermissive) @@ -1048,6 +1072,16 @@ func (s *Server) SwitchProfile(callerCtx context.Context, msg *proto.SwitchProfi } if msg != nil && msg.ProfileName != nil { + targetUsername := "" + if msg.Username != nil { + targetUsername = *msg.Username + } + // The interceptor already gated this against the CURRENT active profile; + // also bind the TARGET profile's username so a caller can't switch into + // another user's profile. + if err := s.bindCallerUsername(callerCtx, targetUsername); err != nil { + return nil, err + } if _, err := s.switchProfileIfNeeded(*msg.ProfileName, msg.Username, activeProf); err != nil { log.Errorf("failed to switch profile: %v", err) return nil, err @@ -2005,7 +2039,18 @@ func (s *Server) AddProfile(ctx context.Context, msg *proto.AddProfileRequest) ( return nil, gstatus.Errorf(codes.InvalidArgument, "profile name and username must be provided") } - created, err := s.profileManager.AddProfile(msg.ProfileName, msg.Username) + if err := s.bindCallerUsername(ctx, msg.Username); err != nil { + return nil, err + } + + // Auto-isolate the new profile to its creator. When root/admin creates it we + // leave it unowned so the intended user claims it via trust-on-first-use. + var initialOwners []string + if id, ok := ipcauth.IdentityFromContext(ctx); ok && !id.IsPrivileged() { + initialOwners = []string{ipcauth.OwnerPrincipalForIdentity(id)} + } + + created, err := s.profileManager.AddProfile(msg.ProfileName, msg.Username, initialOwners) if err != nil { log.Errorf("failed to create profile: %v", err) return nil, fmt.Errorf("failed to create profile: %w", err) @@ -2028,6 +2073,10 @@ func (s *Server) RenameProfile(ctx context.Context, msg *proto.RenameProfileRequ return nil, gstatus.Errorf(codes.InvalidArgument, "profile name, username and new profile name must be provided") } + if err := s.bindCallerUsername(ctx, msg.Username); err != nil { + return nil, err + } + resolved, err := s.resolveProfileHandle(msg.Handle, msg.Username) if err != nil { return nil, err @@ -2057,6 +2106,10 @@ func (s *Server) RemoveProfile(ctx context.Context, msg *proto.RemoveProfileRequ return nil, gstatus.Errorf(codes.InvalidArgument, "profile name must be provided") } + if err := s.bindCallerUsername(ctx, msg.Username); err != nil { + return nil, err + } + resolved, err := s.resolveProfileHandle(msg.ProfileName, msg.Username) if err != nil { return nil, err @@ -2125,6 +2178,10 @@ func (s *Server) ListProfiles(ctx context.Context, msg *proto.ListProfilesReques return nil, gstatus.Errorf(codes.InvalidArgument, "username must be provided") } + if err := s.bindCallerUsername(ctx, msg.Username); err != nil { + return nil, err + } + profiles, err := s.profileManager.ListProfiles(msg.Username) if err != nil { log.Errorf("failed to list profiles: %v", err) diff --git a/client/server/ssh_gate.go b/client/server/ssh_gate.go new file mode 100644 index 000000000..0365c7c9f --- /dev/null +++ b/client/server/ssh_gate.go @@ -0,0 +1,44 @@ +package server + +import ( + "context" + + log "github.com/sirupsen/logrus" + "google.golang.org/grpc/codes" + gstatus "google.golang.org/grpc/status" + + "github.com/netbirdio/netbird/client/internal/ipcauth" +) + +// requirePrivilegedForDangerousSSH enforces M-SSHGATE: enabling SSH root login +// or disabling SSH authentication turns the root/LocalSystem daemon's SSH server +// into an unauthenticated root shell (local-to-remote-root escalation), so only +// a privileged caller (Unix root, or Windows elevated-admin/LocalSystem) may set +// these flags to true over the local IPC. +// +// It gates the request fields, not the resulting config: a value already +// persisted (e.g. set previously by root, or by MDM/managed config) is untouched; +// only a new attempt to turn them on via SetConfig/Login is checked. +// +// When the caller identity cannot be verified (e.g. the daemon is on a TCP +// control channel with no peer credentials) it fails closed — refusing rather +// than letting an unauthenticated local caller flip these flags. +func requirePrivilegedForDangerousSSH(ctx context.Context, enableSSHRoot, disableSSHAuth *bool) error { + dangerous := (enableSSHRoot != nil && *enableSSHRoot) || (disableSSHAuth != nil && *disableSSHAuth) + if !dangerous { + return nil + } + + id, ok := ipcauth.IdentityFromContext(ctx) + if !ok { + log.Warnf("denying SSH root/no-auth config change: caller identity unavailable on this control channel") + return gstatus.Error(codes.PermissionDenied, + "enabling SSH root login or disabling SSH authentication requires root/administrator, but the caller identity could not be verified on this daemon control channel") + } + if !id.IsPrivileged() { + log.Warnf("denying SSH root/no-auth config change from non-privileged caller %s", id) + return gstatus.Errorf(codes.PermissionDenied, + "enabling SSH root login or disabling SSH authentication requires root/administrator (caller %s is not privileged); rerun as root/administrator", id) + } + return nil +} diff --git a/client/server/ssh_gate_test.go b/client/server/ssh_gate_test.go new file mode 100644 index 000000000..089c262b7 --- /dev/null +++ b/client/server/ssh_gate_test.go @@ -0,0 +1,52 @@ +package server + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/peer" + gstatus "google.golang.org/grpc/status" + + "github.com/netbirdio/netbird/client/internal/ipcauth" +) + +func ctxWithIdentity(id ipcauth.Identity) context.Context { + return peer.NewContext(context.Background(), &peer.Peer{AuthInfo: ipcauth.AuthInfo{Identity: id}}) +} + +func boolPtr(b bool) *bool { return &b } + +func TestRequirePrivilegedForDangerousSSH(t *testing.T) { + root := ipcauth.Identity{UID: 0} + user := ipcauth.Identity{UID: 1000} + + tests := []struct { + name string + ctx context.Context + enableSSHRoot *bool + disableSSHAuth *bool + wantDenied bool + }{ + {"no flags, no identity", context.Background(), nil, nil, false}, + {"flags false, non-priv", ctxWithIdentity(user), boolPtr(false), boolPtr(false), false}, + {"enableSSHRoot by root", ctxWithIdentity(root), boolPtr(true), nil, false}, + {"enableSSHRoot by non-priv", ctxWithIdentity(user), boolPtr(true), nil, true}, + {"disableSSHAuth by non-priv", ctxWithIdentity(user), nil, boolPtr(true), true}, + {"enableSSHRoot no identity (fail closed)", context.Background(), boolPtr(true), nil, true}, + {"both by root", ctxWithIdentity(root), boolPtr(true), boolPtr(true), false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := requirePrivilegedForDangerousSSH(tt.ctx, tt.enableSSHRoot, tt.disableSSHAuth) + if tt.wantDenied { + assert.Error(t, err) + assert.Equal(t, codes.PermissionDenied, gstatus.Code(err)) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/client/ssh/server/command_execution_unix.go b/client/ssh/server/command_execution_unix.go index 279b89341..7e2067530 100644 --- a/client/ssh/server/command_execution_unix.go +++ b/client/ssh/server/command_execution_unix.go @@ -20,6 +20,8 @@ import ( "github.com/creack/pty" "github.com/gliderlabs/ssh" log "github.com/sirupsen/logrus" + + shellutil "github.com/netbirdio/netbird/client/internal/shell" ) // ptyManager manages Pty file operations with thread safety @@ -146,10 +148,10 @@ func (s *Server) createShellCommand(ctx context.Context, shell string, args []st // prepareCommandEnv prepares environment variables for command execution on Unix func (s *Server) prepareCommandEnv(_ *log.Entry, localUser *user.User, session ssh.Session) []string { - env := prepareUserEnv(localUser, getUserShell(localUser.Uid)) + env := shellutil.PrepareUserEnv(localUser, shellutil.GetUserShell(localUser.Uid)) env = append(env, prepareSSHEnv(session)...) for _, v := range session.Environ() { - if acceptEnv(v) { + if shellutil.AcceptEnv(v) { env = append(env, v) } } diff --git a/client/ssh/server/command_execution_windows.go b/client/ssh/server/command_execution_windows.go index e1ba777f6..ccff4d455 100644 --- a/client/ssh/server/command_execution_windows.go +++ b/client/ssh/server/command_execution_windows.go @@ -15,6 +15,7 @@ import ( "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" + shellutil "github.com/netbirdio/netbird/client/internal/shell" "github.com/netbirdio/netbird/client/ssh/server/winpty" ) @@ -247,10 +248,10 @@ func (s *Server) prepareCommandEnv(logger *log.Entry, localUser *user.User, sess userEnv, err := s.getUserEnvironment(logger, username, domain) if err != nil { log.Debugf("failed to get user environment for %s\\%s, using fallback: %v", domain, username, err) - env := prepareUserEnv(localUser, getUserShell(localUser.Uid)) + env := shellutil.PrepareUserEnv(localUser, shellutil.GetUserShell(localUser.Uid)) env = append(env, prepareSSHEnv(session)...) for _, v := range session.Environ() { - if acceptEnv(v) { + if shellutil.AcceptEnv(v) { env = append(env, v) } } @@ -260,7 +261,7 @@ func (s *Server) prepareCommandEnv(logger *log.Entry, localUser *user.User, sess env := userEnv env = append(env, prepareSSHEnv(session)...) for _, v := range session.Environ() { - if acceptEnv(v) { + if shellutil.AcceptEnv(v) { env = append(env, v) } } @@ -273,7 +274,7 @@ func (s *Server) handlePtyLogin(logger *log.Entry, session ssh.Session, privileg return false } - shell := getUserShell(privilegeResult.User.Uid) + shell := shellutil.GetUserShell(privilegeResult.User.Uid) logger.Infof("starting interactive shell: %s", shell) s.executeCommandWithPty(logger, session, nil, privilegeResult, ptyReq, nil) @@ -384,7 +385,7 @@ func (s *Server) executeCommandWithPty(logger *log.Entry, session ssh.Session, _ } username, domain := s.parseUsername(localUser.Username) - shell := getUserShell(localUser.Uid) + shell := shellutil.GetUserShell(localUser.Uid) req := PtyExecutionRequest{ Shell: shell, diff --git a/client/ssh/server/getent_cgo_unix.go b/client/ssh/server/getent_cgo_unix.go deleted file mode 100644 index 4afbfc627..000000000 --- a/client/ssh/server/getent_cgo_unix.go +++ /dev/null @@ -1,24 +0,0 @@ -//go:build cgo && !osusergo && !windows - -package server - -import "os/user" - -// lookupWithGetent with CGO delegates directly to os/user.Lookup. -// When CGO is enabled, os/user uses libc (getpwnam_r) which goes through -// the NSS stack natively. If it fails, the user truly doesn't exist and -// getent would also fail. -func lookupWithGetent(username string) (*user.User, error) { - return user.Lookup(username) -} - -// currentUserWithGetent with CGO delegates directly to os/user.Current. -func currentUserWithGetent() (*user.User, error) { - return user.Current() -} - -// groupIdsWithFallback with CGO delegates directly to user.GroupIds. -// libc's getgrouplist handles NSS groups natively. -func groupIdsWithFallback(u *user.User) ([]string, error) { - return u.GroupIds() -} diff --git a/client/ssh/server/getent_windows.go b/client/ssh/server/getent_windows.go deleted file mode 100644 index 3e76b3e8e..000000000 --- a/client/ssh/server/getent_windows.go +++ /dev/null @@ -1,26 +0,0 @@ -//go:build windows - -package server - -import "os/user" - -// lookupWithGetent on Windows just delegates to os/user.Lookup. -// Windows does not use NSS/getent; its user lookup works without CGO. -func lookupWithGetent(username string) (*user.User, error) { - return user.Lookup(username) -} - -// currentUserWithGetent on Windows just delegates to os/user.Current. -func currentUserWithGetent() (*user.User, error) { - return user.Current() -} - -// getShellFromGetent is a no-op on Windows; shell resolution uses PowerShell detection. -func getShellFromGetent(_ string) string { - return "" -} - -// groupIdsWithFallback on Windows just delegates to u.GroupIds(). -func groupIdsWithFallback(u *user.User) ([]string, error) { - return u.GroupIds() -} diff --git a/client/ssh/server/ssh_env.go b/client/ssh/server/ssh_env.go new file mode 100644 index 000000000..5fa543351 --- /dev/null +++ b/client/ssh/server/ssh_env.go @@ -0,0 +1,35 @@ +package server + +import ( + "fmt" + "net" + "strconv" + + "github.com/gliderlabs/ssh" +) + +// prepareSSHEnv prepares SSH protocol-specific environment variables +// These variables provide information about the SSH connection itself +func prepareSSHEnv(session ssh.Session) []string { + remoteAddr := session.RemoteAddr() + localAddr := session.LocalAddr() + + remoteHost, remotePort, err := net.SplitHostPort(remoteAddr.String()) + if err != nil { + remoteHost = remoteAddr.String() + remotePort = "0" + } + + localHost, localPort, err := net.SplitHostPort(localAddr.String()) + if err != nil { + localHost = localAddr.String() + localPort = strconv.Itoa(InternalSSHPort) + } + + return []string{ + // SSH_CLIENT format: "client_ip client_port server_port" + fmt.Sprintf("SSH_CLIENT=%s %s %s", remoteHost, remotePort, localPort), + // SSH_CONNECTION format: "client_ip client_port server_ip server_port" + fmt.Sprintf("SSH_CONNECTION=%s %s %s %s", remoteHost, remotePort, localHost, localPort), + } +} diff --git a/client/ssh/server/user_utils.go b/client/ssh/server/user_utils.go index bc2aa2d7d..64f59c318 100644 --- a/client/ssh/server/user_utils.go +++ b/client/ssh/server/user_utils.go @@ -9,6 +9,8 @@ import ( "strings" log "github.com/sirupsen/logrus" + + shellutil "github.com/netbirdio/netbird/client/internal/shell" ) var ( @@ -23,8 +25,8 @@ func isPlatformUnix() bool { // Dependency injection variables for testing - allows mocking dynamic runtime checks var ( - getCurrentUser = currentUserWithGetent - lookupUser = lookupWithGetent + getCurrentUser = shellutil.CurrentUserWithGetent + lookupUser = shellutil.LookupWithGetent getCurrentOS = func() string { return runtime.GOOS } getIsProcessPrivileged = isCurrentProcessPrivileged diff --git a/client/ssh/server/userswitching_unix.go b/client/ssh/server/userswitching_unix.go index 220e2240f..7d3b7c620 100644 --- a/client/ssh/server/userswitching_unix.go +++ b/client/ssh/server/userswitching_unix.go @@ -16,6 +16,8 @@ import ( "github.com/gliderlabs/ssh" log "github.com/sirupsen/logrus" + + shellutil "github.com/netbirdio/netbird/client/internal/shell" ) // POSIX portable filename character set regex: [a-zA-Z0-9._-] @@ -160,7 +162,7 @@ func (s *Server) parseUserCredentials(localUser *user.User) (uint32, uint32, []u // getSupplementaryGroups retrieves supplementary group IDs for a user. // Uses id/getent fallback for NSS users in CGO_ENABLED=0 builds. func (s *Server) getSupplementaryGroups(u *user.User) ([]uint32, error) { - groupIDStrings, err := groupIdsWithFallback(u) + groupIDStrings, err := shellutil.GroupIdsWithFallback(u) if err != nil { return nil, fmt.Errorf("get group IDs for user %s: %w", u.Username, err) } @@ -196,7 +198,7 @@ func (s *Server) createExecutorCommand(logger *log.Entry, session ssh.Session, l GID: gid, Groups: groups, WorkingDir: localUser.HomeDir, - Shell: getUserShell(localUser.Uid), + Shell: shellutil.GetUserShell(localUser.Uid), Command: session.RawCommand(), PTY: hasPty, } @@ -228,7 +230,7 @@ func (s *Server) createPtyCommand(privilegeResult PrivilegeCheckResult, ptyReq s func (s *Server) createDirectPtyCommand(session ssh.Session, localUser *user.User, ptyReq ssh.Pty) *exec.Cmd { log.Debugf("creating direct Pty command for user %s (no user switching needed)", localUser.Username) - shell := getUserShell(localUser.Uid) + shell := shellutil.GetUserShell(localUser.Uid) args := s.getShellCommandArgs(shell, session.RawCommand()) cmd := s.createShellCommand(session.Context(), shell, args) @@ -245,12 +247,12 @@ func (s *Server) preparePtyEnv(localUser *user.User, ptyReq ssh.Pty, session ssh termType = "xterm-256color" } - env := prepareUserEnv(localUser, getUserShell(localUser.Uid)) + env := shellutil.PrepareUserEnv(localUser, shellutil.GetUserShell(localUser.Uid)) env = append(env, prepareSSHEnv(session)...) env = append(env, fmt.Sprintf("TERM=%s", termType)) for _, v := range session.Environ() { - if acceptEnv(v) { + if shellutil.AcceptEnv(v) { env = append(env, v) } } diff --git a/client/ssh/server/userswitching_windows.go b/client/ssh/server/userswitching_windows.go index 260e1301e..41a3cc3b8 100644 --- a/client/ssh/server/userswitching_windows.go +++ b/client/ssh/server/userswitching_windows.go @@ -13,6 +13,8 @@ import ( "github.com/gliderlabs/ssh" log "github.com/sirupsen/logrus" "golang.org/x/sys/windows" + + shellutil "github.com/netbirdio/netbird/client/internal/shell" ) // validateUsername validates Windows usernames according to SAM Account Name rules @@ -104,7 +106,7 @@ func (s *Server) createExecutorCommand(logger *log.Entry, session ssh.Session, l func (s *Server) createUserSwitchCommand(logger *log.Entry, session ssh.Session, localUser *user.User) (*exec.Cmd, func(), error) { username, domain := s.parseUsername(localUser.Username) - shell := getUserShell(localUser.Uid) + shell := shellutil.GetUserShell(localUser.Uid) rawCmd := session.RawCommand() var command string diff --git a/client/ui/grpc.go b/client/ui/grpc.go index c8e3aed76..76ca9d7fc 100644 --- a/client/ui/grpc.go +++ b/client/ui/grpc.go @@ -3,16 +3,16 @@ package main import ( + "context" "fmt" "runtime" - "strings" "sync" "time" "google.golang.org/grpc" "google.golang.org/grpc/backoff" - "google.golang.org/grpc/credentials/insecure" + "github.com/netbirdio/netbird/client/cmd" "github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/ui/desktop" ) @@ -36,9 +36,7 @@ func (c *Conn) Client() (proto.DaemonServiceClient, error) { return c.client, nil } - cc, err := grpc.NewClient( - strings.TrimPrefix(c.addr, "tcp://"), - grpc.WithTransportCredentials(insecure.NewCredentials()), + opts := []grpc.DialOption{ grpc.WithUserAgent(desktop.GetUIUserAgent()), // Cap reconnect backoff at 5s; gRPC's default 120s MaxDelay would // leave the UI waiting 30-60s to notice a freshly-started daemon. @@ -50,6 +48,12 @@ func (c *Conn) Client() (proto.DaemonServiceClient, error) { MaxDelay: 5 * time.Second, }, }), + } + + cc, err := cmd.DialClientGRPCServer( + context.Background(), + c.addr, + opts..., ) if err != nil { return nil, fmt.Errorf("dial daemon: %w", err) @@ -61,7 +65,7 @@ func (c *Conn) Client() (proto.DaemonServiceClient, error) { // DaemonAddr returns the default daemon gRPC address: a Unix socket on Linux/macOS, TCP loopback on Windows. func DaemonAddr() string { if runtime.GOOS == "windows" { - return "tcp://127.0.0.1:41731" + return "npipe://netbird" } return "unix:///var/run/netbird.sock" } diff --git a/go.mod b/go.mod index ca798decc..73a3079e7 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( require ( github.com/DeRuina/timberjack v1.4.2 + github.com/Microsoft/go-winio v0.6.2 github.com/awnumar/memguard v0.23.0 github.com/aws/aws-sdk-go-v2 v1.38.3 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 @@ -156,7 +157,6 @@ require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect - github.com/Microsoft/go-winio v0.6.2 // indirect github.com/adrg/xdg v0.5.3 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect