diff --git a/client/cmd/root.go b/client/cmd/root.go index c872fe9f6..b59963fd6 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -151,6 +151,7 @@ func init() { rootCmd.AddCommand(logoutCmd) rootCmd.AddCommand(versionCmd) rootCmd.AddCommand(sshCmd) + rootCmd.AddCommand(vncCmd) rootCmd.AddCommand(networksCMD) rootCmd.AddCommand(forwardingRulesCmd) rootCmd.AddCommand(debugCmd) diff --git a/client/cmd/ssh.go b/client/cmd/ssh.go index 0acf0b133..c8400f9f8 100644 --- a/client/cmd/ssh.go +++ b/client/cmd/ssh.go @@ -36,7 +36,10 @@ const ( enableSSHLocalPortForwardFlag = "enable-ssh-local-port-forwarding" enableSSHRemotePortForwardFlag = "enable-ssh-remote-port-forwarding" disableSSHAuthFlag = "disable-ssh-auth" - sshJWTCacheTTLFlag = "ssh-jwt-cache-ttl" + jwtCacheTTLFlag = "jwt-cache-ttl" + + // Alias for backward compatibility. + sshJWTCacheTTLFlag = "ssh-jwt-cache-ttl" ) var ( @@ -61,7 +64,7 @@ var ( enableSSHLocalPortForward bool enableSSHRemotePortForward bool disableSSHAuth bool - sshJWTCacheTTL int + jwtCacheTTL int ) func init() { @@ -71,7 +74,9 @@ func init() { upCmd.PersistentFlags().BoolVar(&enableSSHLocalPortForward, enableSSHLocalPortForwardFlag, false, "Enable local port forwarding for SSH server") upCmd.PersistentFlags().BoolVar(&enableSSHRemotePortForward, enableSSHRemotePortForwardFlag, false, "Enable remote port forwarding for SSH server") upCmd.PersistentFlags().BoolVar(&disableSSHAuth, disableSSHAuthFlag, false, "Disable SSH authentication") - upCmd.PersistentFlags().IntVar(&sshJWTCacheTTL, sshJWTCacheTTLFlag, 0, "SSH JWT token cache TTL in seconds (0=disabled)") + upCmd.PersistentFlags().IntVar(&jwtCacheTTL, jwtCacheTTLFlag, 0, "JWT token cache TTL in seconds (0=disabled)") + upCmd.PersistentFlags().IntVar(&jwtCacheTTL, sshJWTCacheTTLFlag, 0, "JWT token cache TTL in seconds (alias for --jwt-cache-ttl)") + _ = upCmd.PersistentFlags().MarkDeprecated(sshJWTCacheTTLFlag, "use --jwt-cache-ttl instead") sshCmd.PersistentFlags().IntVarP(&port, "port", "p", sshserver.DefaultSSHPort, "Remote SSH port") sshCmd.PersistentFlags().StringVarP(&username, "user", "u", "", sshUsernameDesc) diff --git a/client/cmd/up.go b/client/cmd/up.go index f5766522a..9f2e53237 100644 --- a/client/cmd/up.go +++ b/client/cmd/up.go @@ -356,6 +356,9 @@ func setupSetConfigReq(customDNSAddressConverted []byte, cmd *cobra.Command, pro if cmd.Flag(serverSSHAllowedFlag).Changed { req.ServerSSHAllowed = &serverSSHAllowed } + if cmd.Flag(serverVNCAllowedFlag).Changed { + req.ServerVNCAllowed = &serverVNCAllowed + } if cmd.Flag(enableSSHRootFlag).Changed { req.EnableSSHRoot = &enableSSHRoot } @@ -371,9 +374,12 @@ func setupSetConfigReq(customDNSAddressConverted []byte, cmd *cobra.Command, pro if cmd.Flag(disableSSHAuthFlag).Changed { req.DisableSSHAuth = &disableSSHAuth } - if cmd.Flag(sshJWTCacheTTLFlag).Changed { - sshJWTCacheTTL32 := int32(sshJWTCacheTTL) - req.SshJWTCacheTTL = &sshJWTCacheTTL32 + if cmd.Flag(disableVNCAuthFlag).Changed { + req.DisableVNCAuth = &disableVNCAuth + } + if cmd.Flag(jwtCacheTTLFlag).Changed || cmd.Flag(sshJWTCacheTTLFlag).Changed { + jwtCacheTTL32 := int32(jwtCacheTTL) + req.SshJWTCacheTTL = &jwtCacheTTL32 } if cmd.Flag(interfaceNameFlag).Changed { if err := parseInterfaceName(interfaceName); err != nil { @@ -458,6 +464,9 @@ func setupConfig(customDNSAddressConverted []byte, cmd *cobra.Command, configFil if cmd.Flag(serverSSHAllowedFlag).Changed { ic.ServerSSHAllowed = &serverSSHAllowed } + if cmd.Flag(serverVNCAllowedFlag).Changed { + ic.ServerVNCAllowed = &serverVNCAllowed + } if cmd.Flag(enableSSHRootFlag).Changed { ic.EnableSSHRoot = &enableSSHRoot @@ -479,8 +488,12 @@ func setupConfig(customDNSAddressConverted []byte, cmd *cobra.Command, configFil ic.DisableSSHAuth = &disableSSHAuth } - if cmd.Flag(sshJWTCacheTTLFlag).Changed { - ic.SSHJWTCacheTTL = &sshJWTCacheTTL + if cmd.Flag(disableVNCAuthFlag).Changed { + ic.DisableVNCAuth = &disableVNCAuth + } + + if cmd.Flag(jwtCacheTTLFlag).Changed || cmd.Flag(sshJWTCacheTTLFlag).Changed { + ic.SSHJWTCacheTTL = &jwtCacheTTL } if cmd.Flag(interfaceNameFlag).Changed { @@ -582,6 +595,9 @@ func setupLoginRequest(providedSetupKey string, customDNSAddressConverted []byte if cmd.Flag(serverSSHAllowedFlag).Changed { loginRequest.ServerSSHAllowed = &serverSSHAllowed } + if cmd.Flag(serverVNCAllowedFlag).Changed { + loginRequest.ServerVNCAllowed = &serverVNCAllowed + } if cmd.Flag(enableSSHRootFlag).Changed { loginRequest.EnableSSHRoot = &enableSSHRoot @@ -603,9 +619,13 @@ func setupLoginRequest(providedSetupKey string, customDNSAddressConverted []byte loginRequest.DisableSSHAuth = &disableSSHAuth } - if cmd.Flag(sshJWTCacheTTLFlag).Changed { - sshJWTCacheTTL32 := int32(sshJWTCacheTTL) - loginRequest.SshJWTCacheTTL = &sshJWTCacheTTL32 + if cmd.Flag(disableVNCAuthFlag).Changed { + loginRequest.DisableVNCAuth = &disableVNCAuth + } + + if cmd.Flag(jwtCacheTTLFlag).Changed || cmd.Flag(sshJWTCacheTTLFlag).Changed { + jwtCacheTTL32 := int32(jwtCacheTTL) + loginRequest.SshJWTCacheTTL = &jwtCacheTTL32 } if cmd.Flag(disableAutoConnectFlag).Changed { diff --git a/client/cmd/vnc.go b/client/cmd/vnc.go new file mode 100644 index 000000000..2d310354f --- /dev/null +++ b/client/cmd/vnc.go @@ -0,0 +1,271 @@ +package cmd + +import ( + "context" + "encoding/binary" + "fmt" + "io" + "net" + "os" + "os/signal" + "os/user" + "strings" + "syscall" + "time" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "github.com/netbirdio/netbird/client/internal" + "github.com/netbirdio/netbird/client/internal/profilemanager" + "github.com/netbirdio/netbird/client/proto" + nbssh "github.com/netbirdio/netbird/client/ssh" + "github.com/netbirdio/netbird/util" +) + +var ( + vncUsername string + vncHost string + vncMode string + vncListen string + vncNoBrowser bool + vncNoCache bool +) + +func init() { + vncCmd.PersistentFlags().StringVar(&vncUsername, "user", "", "OS username for session mode") + vncCmd.PersistentFlags().StringVar(&vncMode, "mode", "attach", "Connection mode: attach (view current display) or session (virtual desktop)") + vncCmd.PersistentFlags().StringVar(&vncListen, "listen", "", "Start local VNC proxy on this address (e.g., :5900) for external VNC viewers") + vncCmd.PersistentFlags().BoolVar(&vncNoBrowser, noBrowserFlag, false, noBrowserDesc) + vncCmd.PersistentFlags().BoolVar(&vncNoCache, "no-cache", false, "Skip cached JWT token and force fresh authentication") +} + +var vncCmd = &cobra.Command{ + Use: "vnc [flags] [user@]host", + Short: "Connect to a NetBird peer via VNC", + Long: `Connect to a NetBird peer using VNC with JWT-based authentication. +The target peer must have the VNC server enabled. + +Two modes are available: + - attach: view the current physical display (remote support) + - session: start a virtual desktop as the specified user (passwordless login) + +Use --listen to start a local proxy for external VNC viewers: + netbird vnc --listen :5900 peer-hostname + vncviewer localhost:5900 + +Examples: + netbird vnc peer-hostname + netbird vnc --mode session --user alice peer-hostname + netbird vnc --listen :5900 peer-hostname`, + Args: cobra.MinimumNArgs(1), + RunE: vncFn, +} + +func vncFn(cmd *cobra.Command, args []string) error { + SetFlagsFromEnvVars(rootCmd) + SetFlagsFromEnvVars(cmd) + cmd.SetOut(cmd.OutOrStdout()) + + logOutput := "console" + if firstLogFile := util.FindFirstLogPath(logFiles); firstLogFile != "" && firstLogFile != defaultLogFile { + logOutput = firstLogFile + } + if err := util.InitLog(logLevel, logOutput); err != nil { + return fmt.Errorf("init log: %w", err) + } + + if err := parseVNCHostArg(args[0]); err != nil { + return err + } + + ctx := internal.CtxInitState(cmd.Context()) + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT) + vncCtx, cancel := context.WithCancel(ctx) + + errCh := make(chan error, 1) + go func() { + if err := runVNC(vncCtx, cmd); err != nil { + errCh <- err + } + cancel() + }() + + select { + case <-sig: + cancel() + <-vncCtx.Done() + return nil + case err := <-errCh: + return err + case <-vncCtx.Done(): + } + + return nil +} + +func parseVNCHostArg(arg string) error { + if strings.Contains(arg, "@") { + parts := strings.SplitN(arg, "@", 2) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return fmt.Errorf("invalid user@host format") + } + if vncUsername == "" { + vncUsername = parts[0] + } + vncHost = parts[1] + if vncMode == "attach" { + vncMode = "session" + } + } else { + vncHost = arg + } + + if vncMode == "session" && vncUsername == "" { + if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" { + vncUsername = sudoUser + } else if currentUser, err := user.Current(); err == nil { + vncUsername = currentUser.Username + } + } + + return nil +} + +func runVNC(ctx context.Context, cmd *cobra.Command) error { + grpcAddr := strings.TrimPrefix(daemonAddr, "tcp://") + grpcConn, err := grpc.NewClient(grpcAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return fmt.Errorf("connect to daemon: %w", err) + } + defer func() { _ = grpcConn.Close() }() + + daemonClient := proto.NewDaemonServiceClient(grpcConn) + + if vncMode == "session" { + cmd.Printf("Connecting to %s@%s [session mode]...\n", vncUsername, vncHost) + } else { + cmd.Printf("Connecting to %s [attach mode]...\n", vncHost) + } + + // Obtain JWT token. If the daemon has no SSO configured, proceed without one + // (the server will accept unauthenticated connections if --disable-vnc-auth is set). + var jwtToken string + hint := profilemanager.GetLoginHint() + var browserOpener func(string) error + if !vncNoBrowser { + browserOpener = util.OpenBrowser + } + + token, err := nbssh.RequestJWTToken(ctx, daemonClient, nil, cmd.ErrOrStderr(), !vncNoCache, hint, browserOpener) + if err != nil { + log.Debugf("JWT authentication unavailable, connecting without token: %v", err) + } else { + jwtToken = token + log.Debug("JWT authentication successful") + } + + // Connect to the VNC server on the standard port (5900). The peer's firewall + // DNATs 5900 -> 25900 (internal), so both ports work on the overlay network. + vncAddr := net.JoinHostPort(vncHost, "5900") + vncConn, err := net.DialTimeout("tcp", vncAddr, vncDialTimeout) + if err != nil { + return fmt.Errorf("connect to VNC at %s: %w", vncAddr, err) + } + defer vncConn.Close() + + // Send session header with mode, username, and JWT. + if err := sendVNCHeader(vncConn, vncMode, vncUsername, jwtToken); err != nil { + return fmt.Errorf("send VNC header: %w", err) + } + + cmd.Printf("VNC connected to %s\n", vncHost) + + if vncListen != "" { + return runVNCLocalProxy(ctx, cmd, vncConn) + } + + // No --listen flag: inform the user they need to use --listen for external viewers. + cmd.Printf("VNC tunnel established. Use --listen :5900 to proxy for local VNC viewers.\n") + cmd.Printf("Press Ctrl+C to disconnect.\n") + <-ctx.Done() + return nil +} + +const vncDialTimeout = 15 * time.Second + +// sendVNCHeader writes the NetBird VNC session header. +func sendVNCHeader(conn net.Conn, mode, username, jwt string) error { + var modeByte byte + if mode == "session" { + modeByte = 1 + } + + usernameBytes := []byte(username) + jwtBytes := []byte(jwt) + hdr := make([]byte, 3+len(usernameBytes)+2+len(jwtBytes)) + hdr[0] = modeByte + binary.BigEndian.PutUint16(hdr[1:3], uint16(len(usernameBytes))) + off := 3 + copy(hdr[off:], usernameBytes) + off += len(usernameBytes) + binary.BigEndian.PutUint16(hdr[off:off+2], uint16(len(jwtBytes))) + off += 2 + copy(hdr[off:], jwtBytes) + + _, err := conn.Write(hdr) + return err +} + +// runVNCLocalProxy listens on the given address and proxies incoming +// connections to the already-established VNC tunnel. +func runVNCLocalProxy(ctx context.Context, cmd *cobra.Command, vncConn net.Conn) error { + listener, err := net.Listen("tcp", vncListen) + if err != nil { + return fmt.Errorf("listen on %s: %w", vncListen, err) + } + defer listener.Close() + + cmd.Printf("VNC proxy listening on %s - connect with your VNC viewer\n", listener.Addr()) + cmd.Printf("Press Ctrl+C to stop.\n") + + go func() { + <-ctx.Done() + listener.Close() + }() + + // Accept a single viewer connection. VNC is single-session: the RFB + // handshake completes on vncConn for the first viewer, so subsequent + // viewers would get a mid-stream connection. The loop handles transient + // accept errors until a valid connection arrives. + for { + clientConn, err := listener.Accept() + if err != nil { + select { + case <-ctx.Done(): + return nil + default: + } + log.Debugf("accept VNC proxy client: %v", err) + continue + } + + cmd.Printf("VNC viewer connected from %s\n", clientConn.RemoteAddr()) + + // Bidirectional copy. + done := make(chan struct{}) + go func() { + io.Copy(vncConn, clientConn) + close(done) + }() + io.Copy(clientConn, vncConn) + <-done + clientConn.Close() + + cmd.Printf("VNC viewer disconnected\n") + return nil + } +} diff --git a/client/cmd/vnc_agent.go b/client/cmd/vnc_agent.go new file mode 100644 index 000000000..b14ac780e --- /dev/null +++ b/client/cmd/vnc_agent.go @@ -0,0 +1,62 @@ +//go:build windows + +package cmd + +import ( + "net/netip" + "os" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + vncserver "github.com/netbirdio/netbird/client/vnc/server" +) + +var vncAgentPort string + +func init() { + vncAgentCmd.Flags().StringVar(&vncAgentPort, "port", "15900", "Port for the VNC agent to listen on") + rootCmd.AddCommand(vncAgentCmd) +} + +// vncAgentCmd runs a VNC server in the current user session, listening on +// localhost. It is spawned by the NetBird service (Session 0) via +// CreateProcessAsUser into the interactive console session. +var vncAgentCmd = &cobra.Command{ + Use: "vnc-agent", + Short: "Run VNC capture agent (internal, spawned by service)", + Hidden: true, + RunE: func(cmd *cobra.Command, args []string) error { + // Agent's stderr is piped to the service which relogs it. + // Use JSON format with caller info for structured parsing. + log.SetReportCaller(true) + log.SetFormatter(&log.JSONFormatter{}) + log.SetOutput(os.Stderr) + + sessionID := vncserver.GetCurrentSessionID() + log.Infof("VNC agent starting on 127.0.0.1:%s (session %d)", vncAgentPort, sessionID) + + capturer := vncserver.NewDesktopCapturer() + injector := vncserver.NewWindowsInputInjector() + srv := vncserver.New(capturer, injector, "") + // Auth is handled by the service. The agent verifies a token on each + // connection to ensure only the service process can connect. + // The token is passed via environment variable to avoid exposing it + // in the process command line (visible via tasklist/wmic). + srv.SetDisableAuth(true) + srv.SetAgentToken(os.Getenv("NB_VNC_AGENT_TOKEN")) + + port, err := netip.ParseAddrPort("127.0.0.1:" + vncAgentPort) + if err != nil { + return err + } + + loopback := netip.PrefixFrom(netip.AddrFrom4([4]byte{127, 0, 0, 0}), 8) + if err := srv.Start(cmd.Context(), port, loopback); err != nil { + return err + } + + <-cmd.Context().Done() + return srv.Stop() + }, +} diff --git a/client/cmd/vnc_flags.go b/client/cmd/vnc_flags.go new file mode 100644 index 000000000..088b79281 --- /dev/null +++ b/client/cmd/vnc_flags.go @@ -0,0 +1,16 @@ +package cmd + +const ( + serverVNCAllowedFlag = "allow-server-vnc" + disableVNCAuthFlag = "disable-vnc-auth" +) + +var ( + serverVNCAllowed bool + disableVNCAuth bool +) + +func init() { + upCmd.PersistentFlags().BoolVar(&serverVNCAllowed, serverVNCAllowedFlag, false, "Allow embedded VNC server on peer") + upCmd.PersistentFlags().BoolVar(&disableVNCAuth, disableVNCAuthFlag, false, "Disable JWT authentication for VNC") +} diff --git a/client/cmd/vnc_recordings.go b/client/cmd/vnc_recordings.go new file mode 100644 index 000000000..13aff669d --- /dev/null +++ b/client/cmd/vnc_recordings.go @@ -0,0 +1,229 @@ +package cmd + +import ( + "crypto/ecdh" + "crypto/rand" + "encoding/base64" + "fmt" + "os" + "os/signal" + "path/filepath" + "strings" + "syscall" + "text/tabwriter" + "time" + + "github.com/spf13/cobra" + + vncserver "github.com/netbirdio/netbird/client/vnc/server" + "github.com/netbirdio/netbird/util" +) + +var vncRecDir string + +func init() { + vncRecPlayCmd.Flags().StringVar(&vncRecDir, "dir", "", "Recording directory (default: auto-detect)") + vncRecListCmd.Flags().StringVar(&vncRecDir, "dir", "", "Recording directory (default: auto-detect)") + vncRecCmd.AddCommand(vncRecListCmd) + vncRecCmd.AddCommand(vncRecPlayCmd) + vncRecCmd.AddCommand(vncRecKeygenCmd) + vncCmd.AddCommand(vncRecCmd) +} + +var vncRecCmd = &cobra.Command{ + Use: "rec", + Short: "Manage VNC session recordings", +} + +var vncRecKeygenCmd = &cobra.Command{ + Use: "keygen", + Short: "Generate an X25519 keypair for recording encryption", + Long: `Generates an X25519 keypair. Put the public key in management settings +(Session Recording > Encryption Key). Keep the private key safe for decrypting recordings.`, + RunE: vncRecKeygenFn, +} + +var vncRecListCmd = &cobra.Command{ + Use: "list", + Short: "List VNC session recordings", + RunE: vncRecListFn, +} + +var vncRecPlayCmd = &cobra.Command{ + Use: "play ", + Short: "Open a VNC recording in the browser", + Long: `Opens a browser-based player with playback controls: +play/pause, seek, speed (0.25x to 8x), keyboard shortcuts. + +Examples: + netbird vnc rec play last + netbird vnc rec play 20260416-104433_vnc.rec`, + Args: cobra.ExactArgs(1), + RunE: vncRecPlayFn, +} + + +func vncRecListFn(cmd *cobra.Command, _ []string) error { + dir, err := resolveVNCRecDir() + if err != nil { + return err + } + + entries, err := os.ReadDir(dir) + if err != nil { + return fmt.Errorf("read recording dir %s: %w", dir, err) + } + + w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 2, ' ', 0) + fmt.Fprintln(w, "FILE\tSIZE\tDIMENSIONS\tUSER\tREMOTE\tMODE\tDATE") + + for _, entry := range entries { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".rec") { + continue + } + filePath := filepath.Join(dir, entry.Name()) + info, err := entry.Info() + if err != nil { + continue + } + + header, err := vncserver.ReadRecordingHeader(filePath) + if err != nil { + fmt.Fprintf(w, "%s\t%s\t?\t?\t?\t?\t?\n", entry.Name(), vncFormatSize(info.Size())) + continue + } + + fmt.Fprintf(w, "%s\t%s\t%dx%d\t%s\t%s\t%s\t%s\n", + entry.Name(), + vncFormatSize(info.Size()), + header.Width, header.Height, + header.Meta.User, + header.Meta.RemoteAddr, + header.Meta.Mode, + header.StartTime.Format("2006-01-02 15:04:05"), + ) + } + + return w.Flush() +} + +func vncRecPlayFn(cmd *cobra.Command, args []string) error { + filePath, err := resolveVNCRecFile(args[0]) + if err != nil { + return err + } + + header, err := vncserver.ReadRecordingHeader(filePath) + if err != nil { + return fmt.Errorf("read recording: %w", err) + } + + cmd.Printf("Recording: %s (%dx%d)\n", filepath.Base(filePath), header.Width, header.Height) + + url, err := vncserver.ServeWebPlayer(filePath, "localhost:0") + if err != nil { + return fmt.Errorf("start web player: %w", err) + } + cmd.Printf("Player: %s\n", url) + if err := util.OpenBrowser(url); err != nil { + cmd.Printf("Open %s in your browser\n", url) + } + cmd.Printf("Press Ctrl+C to stop.\n") + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) + <-sig + return nil +} + + +func vncRecKeygenFn(cmd *cobra.Command, _ []string) error { + priv, err := ecdh.X25519().GenerateKey(rand.Reader) + if err != nil { + return fmt.Errorf("generate key: %w", err) + } + + privB64 := base64.StdEncoding.EncodeToString(priv.Bytes()) + pubB64 := base64.StdEncoding.EncodeToString(priv.PublicKey().Bytes()) + + cmd.Printf("Private key (keep secret, for decrypting recordings):\n %s\n\n", privB64) + cmd.Printf("Public key (paste into management Settings > Session Recording > Encryption Key):\n %s\n", pubB64) + return nil +} + +func vncFormatSize(size int64) string { + switch { + case size >= 1<<20: + return fmt.Sprintf("%.1fM", float64(size)/float64(1<<20)) + case size >= 1<<10: + return fmt.Sprintf("%.1fK", float64(size)/float64(1<<10)) + default: + return fmt.Sprintf("%dB", size) + } +} + +func resolveVNCRecDir() (string, error) { + if vncRecDir != "" { + return vncRecDir, nil + } + candidates := []string{ + "/var/lib/netbird/recordings/vnc", + filepath.Join(os.Getenv("HOME"), ".netbird/recordings/vnc"), + } + for _, dir := range candidates { + if fi, err := os.Stat(dir); err == nil && fi.IsDir() { + return dir, nil + } + } + return "", fmt.Errorf("no VNC recording directory found; use --dir to specify") +} + +func resolveVNCRecFile(arg string) (string, error) { + if strings.Contains(arg, "/") || strings.Contains(arg, string(os.PathSeparator)) { + return arg, nil + } + + dir, err := resolveVNCRecDir() + if err != nil && arg != "last" { + return arg, nil + } + + if arg == "last" { + if err != nil { + return "", err + } + return findLatestRec(dir) + } + + full := filepath.Join(dir, arg) + if _, err := os.Stat(full); err == nil { + return full, nil + } + return arg, nil +} + +func findLatestRec(dir string) (string, error) { + entries, err := os.ReadDir(dir) + if err != nil { + return "", fmt.Errorf("read dir: %w", err) + } + + var latest string + var latestTime time.Time + for _, entry := range entries { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".rec") { + continue + } + info, err := entry.Info() + if err != nil { + continue + } + if info.ModTime().After(latestTime) { + latestTime = info.ModTime() + latest = filepath.Join(dir, entry.Name()) + } + } + if latest == "" { + return "", fmt.Errorf("no recordings found in %s", dir) + } + return latest, nil +} diff --git a/client/internal/auth/auth.go b/client/internal/auth/auth.go index bdfd07430..32ae8d245 100644 --- a/client/internal/auth/auth.go +++ b/client/internal/auth/auth.go @@ -315,6 +315,7 @@ func (a *Auth) setSystemInfoFlags(info *system.Info) { a.config.RosenpassEnabled, a.config.RosenpassPermissive, a.config.ServerSSHAllowed, + a.config.ServerVNCAllowed, a.config.DisableClientRoutes, a.config.DisableServerRoutes, a.config.DisableDNS, @@ -327,6 +328,7 @@ func (a *Auth) setSystemInfoFlags(info *system.Info) { a.config.EnableSSHLocalPortForwarding, a.config.EnableSSHRemotePortForwarding, a.config.DisableSSHAuth, + a.config.DisableVNCAuth, ) } diff --git a/client/internal/connect.go b/client/internal/connect.go index ac498f719..c6fe34282 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -546,11 +546,13 @@ func createEngineConfig(key wgtypes.Key, config *profilemanager.Config, peerConf RosenpassEnabled: config.RosenpassEnabled, RosenpassPermissive: config.RosenpassPermissive, ServerSSHAllowed: util.ReturnBoolWithDefaultTrue(config.ServerSSHAllowed), + ServerVNCAllowed: config.ServerVNCAllowed != nil && *config.ServerVNCAllowed, EnableSSHRoot: config.EnableSSHRoot, EnableSSHSFTP: config.EnableSSHSFTP, EnableSSHLocalPortForwarding: config.EnableSSHLocalPortForwarding, EnableSSHRemotePortForwarding: config.EnableSSHRemotePortForwarding, DisableSSHAuth: config.DisableSSHAuth, + DisableVNCAuth: config.DisableVNCAuth, DNSRouteInterval: config.DNSRouteInterval, DisableClientRoutes: config.DisableClientRoutes, @@ -627,6 +629,7 @@ func loginToManagement(ctx context.Context, client mgm.Client, pubSSHKey []byte, config.RosenpassEnabled, config.RosenpassPermissive, config.ServerSSHAllowed, + config.ServerVNCAllowed, config.DisableClientRoutes, config.DisableServerRoutes, config.DisableDNS, @@ -639,6 +642,7 @@ func loginToManagement(ctx context.Context, client mgm.Client, pubSSHKey []byte, config.EnableSSHLocalPortForwarding, config.EnableSSHRemotePortForwarding, config.DisableSSHAuth, + config.DisableVNCAuth, ) return client.Login(sysInfo, pubSSHKey, config.DNSLabels) } diff --git a/client/internal/engine.go b/client/internal/engine.go index b49e02c6d..adb6b550d 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -117,11 +117,13 @@ type EngineConfig struct { RosenpassPermissive bool ServerSSHAllowed bool + ServerVNCAllowed bool EnableSSHRoot *bool EnableSSHSFTP *bool EnableSSHLocalPortForwarding *bool EnableSSHRemotePortForwarding *bool DisableSSHAuth *bool + DisableVNCAuth *bool DNSRouteInterval time.Duration @@ -198,6 +200,7 @@ type Engine struct { networkMonitor *networkmonitor.NetworkMonitor sshServer sshServer + vncSrv vncServer statusRecorder *peer.Status @@ -311,6 +314,10 @@ func (e *Engine) Stop() error { log.Warnf("failed to stop SSH server: %v", err) } + if err := e.stopVNCServer(); err != nil { + log.Warnf("failed to stop VNC server: %v", err) + } + e.cleanupSSHConfig() if e.ingressGatewayMgr != nil { @@ -998,6 +1005,7 @@ func (e *Engine) updateChecksIfNew(checks []*mgmProto.Checks) error { e.config.RosenpassEnabled, e.config.RosenpassPermissive, &e.config.ServerSSHAllowed, + &e.config.ServerVNCAllowed, e.config.DisableClientRoutes, e.config.DisableServerRoutes, e.config.DisableDNS, @@ -1010,6 +1018,7 @@ func (e *Engine) updateChecksIfNew(checks []*mgmProto.Checks) error { e.config.EnableSSHLocalPortForwarding, e.config.EnableSSHRemotePortForwarding, e.config.DisableSSHAuth, + e.config.DisableVNCAuth, ) if err := e.mgmClient.SyncMeta(info); err != nil { @@ -1037,6 +1046,10 @@ func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error { } } + if err := e.updateVNC(conf.GetSshConfig()); err != nil { + log.Warnf("failed handling VNC server setup: %v", err) + } + state := e.statusRecorder.GetLocalPeerState() state.IP = e.wgInterface.Address().String() state.PubKey = e.config.WgPrivateKey.PublicKey().String() @@ -1139,6 +1152,7 @@ func (e *Engine) receiveManagementEvents() { e.config.RosenpassEnabled, e.config.RosenpassPermissive, &e.config.ServerSSHAllowed, + &e.config.ServerVNCAllowed, e.config.DisableClientRoutes, e.config.DisableServerRoutes, e.config.DisableDNS, @@ -1151,6 +1165,7 @@ func (e *Engine) receiveManagementEvents() { e.config.EnableSSHLocalPortForwarding, e.config.EnableSSHRemotePortForwarding, e.config.DisableSSHAuth, + e.config.DisableVNCAuth, ) err = e.mgmClient.Sync(e.ctx, info, e.handleSync) @@ -1325,6 +1340,11 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error { } e.updateSSHServerAuth(networkMap.GetSshAuth()) + + // VNC auth: use dedicated VNCAuth if present. + if vncAuth := networkMap.GetVncAuth(); vncAuth != nil { + e.updateVNCServerAuth(vncAuth) + } } // must set the exclude list after the peers are added. Without it the manager can not figure out the peers parameters from the store @@ -1734,6 +1754,7 @@ func (e *Engine) readInitialSettings() ([]*route.Route, *nbdns.Config, bool, err e.config.RosenpassEnabled, e.config.RosenpassPermissive, &e.config.ServerSSHAllowed, + &e.config.ServerVNCAllowed, e.config.DisableClientRoutes, e.config.DisableServerRoutes, e.config.DisableDNS, @@ -1746,6 +1767,7 @@ func (e *Engine) readInitialSettings() ([]*route.Route, *nbdns.Config, bool, err e.config.EnableSSHLocalPortForwarding, e.config.EnableSSHRemotePortForwarding, e.config.DisableSSHAuth, + e.config.DisableVNCAuth, ) netMap, err := e.mgmClient.GetNetworkMap(info) diff --git a/client/internal/engine_vnc.go b/client/internal/engine_vnc.go new file mode 100644 index 000000000..e334357a9 --- /dev/null +++ b/client/internal/engine_vnc.go @@ -0,0 +1,309 @@ +package internal + +import ( + "context" + "errors" + "fmt" + "net/netip" + "os" + "path/filepath" + + log "github.com/sirupsen/logrus" + + firewallManager "github.com/netbirdio/netbird/client/firewall/manager" + nftypes "github.com/netbirdio/netbird/client/internal/netflow/types" + sshauth "github.com/netbirdio/netbird/client/ssh/auth" + vncserver "github.com/netbirdio/netbird/client/vnc/server" + mgmProto "github.com/netbirdio/netbird/shared/management/proto" + sshuserhash "github.com/netbirdio/netbird/shared/sshauth" +) + +const envVNCForceRecording = "NB_VNC_FORCE_RECORDING" + +const ( + vncExternalPort uint16 = 5900 + vncInternalPort uint16 = 25900 +) + +type vncServer interface { + Start(ctx context.Context, addr netip.AddrPort, network netip.Prefix) error + Stop() error +} + +func (e *Engine) setupVNCPortRedirection() error { + if e.firewall == nil || e.wgInterface == nil { + return nil + } + + localAddr := e.wgInterface.Address().IP + if !localAddr.IsValid() { + return errors.New("invalid local NetBird address") + } + + if err := e.firewall.AddInboundDNAT(localAddr, firewallManager.ProtocolTCP, vncExternalPort, vncInternalPort); err != nil { + return fmt.Errorf("add VNC port redirection: %w", err) + } + log.Infof("VNC port redirection: %s:%d -> %s:%d", localAddr, vncExternalPort, localAddr, vncInternalPort) + + return nil +} + +func (e *Engine) cleanupVNCPortRedirection() error { + if e.firewall == nil || e.wgInterface == nil { + return nil + } + + localAddr := e.wgInterface.Address().IP + if !localAddr.IsValid() { + return errors.New("invalid local NetBird address") + } + + if err := e.firewall.RemoveInboundDNAT(localAddr, firewallManager.ProtocolTCP, vncExternalPort, vncInternalPort); err != nil { + return fmt.Errorf("remove VNC port redirection: %w", err) + } + + return nil +} + +// updateVNC handles starting/stopping the VNC server based on the config flag. +// sshConf provides the JWT identity provider config (shared with SSH). +func (e *Engine) updateVNC(sshConf *mgmProto.SSHConfig) error { + if !e.config.ServerVNCAllowed { + if e.vncSrv != nil { + log.Info("VNC server disabled, stopping") + } + return e.stopVNCServer() + } + + if e.config.BlockInbound { + log.Info("VNC server disabled because inbound connections are blocked") + return e.stopVNCServer() + } + + if e.vncSrv != nil { + // Update JWT config on existing server in case management sent new config. + e.updateVNCServerJWT(sshConf) + return nil + } + + return e.startVNCServer(sshConf) +} + +func (e *Engine) startVNCServer(sshConf *mgmProto.SSHConfig) error { + if e.wgInterface == nil { + return errors.New("wg interface not initialized") + } + + capturer, injector := newPlatformVNC() + if capturer == nil || injector == nil { + log.Debug("VNC server not supported on this platform") + return nil + } + + netbirdIP := e.wgInterface.Address().IP + + srv := vncserver.New(capturer, injector, "") + if vncNeedsServiceMode() { + log.Info("VNC: running in Session 0, enabling service mode (agent proxy)") + srv.SetServiceMode(true) + } + + // Configure VNC authentication. + if e.config.DisableVNCAuth != nil && *e.config.DisableVNCAuth { + log.Info("VNC: authentication disabled by config") + srv.SetDisableAuth(true) + } else if protoJWT := sshConf.GetJwtConfig(); protoJWT != nil { + audiences := protoJWT.GetAudiences() + if len(audiences) == 0 && protoJWT.GetAudience() != "" { + audiences = []string{protoJWT.GetAudience()} + } + srv.SetJWTConfig(&vncserver.JWTConfig{ + Issuer: protoJWT.GetIssuer(), + Audiences: audiences, + KeysLocation: protoJWT.GetKeysLocation(), + MaxTokenAge: protoJWT.GetMaxTokenAge(), + }) + log.Debugf("VNC: JWT authentication configured (issuer=%s)", protoJWT.GetIssuer()) + } + + e.configureVNCRecording(srv, sshConf) + + if netstackNet := e.wgInterface.GetNet(); netstackNet != nil { + srv.SetNetstackNet(netstackNet) + } + + listenAddr := netip.AddrPortFrom(netbirdIP, vncInternalPort) + network := e.wgInterface.Address().Network + if err := srv.Start(e.ctx, listenAddr, network); err != nil { + return fmt.Errorf("start VNC server: %w", err) + } + + e.vncSrv = srv + + if registrar, ok := e.firewall.(interface { + RegisterNetstackService(protocol nftypes.Protocol, port uint16) + }); ok { + registrar.RegisterNetstackService(nftypes.TCP, vncInternalPort) + log.Debugf("registered VNC service for TCP:%d", vncInternalPort) + } + + if err := e.setupVNCPortRedirection(); err != nil { + log.Warnf("setup VNC port redirection: %v", err) + } + + log.Info("VNC server enabled") + return nil +} + +// configureVNCRecording enables session recording on the VNC server from the +// management-supplied settings. The env var NB_VNC_FORCE_RECORDING overrides +// the API for local development: when set, recording is always enabled and +// writes into that directory. Otherwise recordings go next to the state file +// under vnc-recordings/. +func (e *Engine) configureVNCRecording(srv *vncserver.Server, sshConf *mgmProto.SSHConfig) { + recDir := os.Getenv(envVNCForceRecording) + apiEnabled := sshConf.GetEnableRecording() + + if recDir == "" && !apiEnabled { + log.Debugf("VNC recording disabled (env=%q, api=%v)", recDir, apiEnabled) + return + } + + if recDir == "" { + base := e.defaultRecordingBase() + if base == "" { + log.Warn("VNC recording requested by management but no state directory is available") + return + } + recDir = filepath.Join(base, "vnc-recordings") + } else { + recDir = filepath.Join(recDir, "vnc") + } + + srv.SetRecordingDir(recDir) + log.Infof("VNC recording enabled (dir=%s, source=%s)", recDir, recordingSource(apiEnabled)) + + encKey := string(sshConf.GetRecordingEncryptionKey()) + if encKey == "" { + encKey = os.Getenv("NB_VNC_RECORDING_ENCRYPTION_KEY") + } + if encKey != "" { + srv.SetRecordingEncryptionKey(encKey) + log.Info("VNC recording encryption enabled") + } +} + +func (e *Engine) defaultRecordingBase() string { + if e.stateManager == nil { + return "" + } + p := e.stateManager.FilePath() + if p == "" { + return "" + } + return filepath.Dir(p) +} + +func recordingSource(api bool) string { + if api { + return "management" + } + return "env" +} + +// updateVNCServerJWT configures the JWT validation for the VNC server using +// the same JWT config as SSH (same identity provider). +func (e *Engine) updateVNCServerJWT(sshConf *mgmProto.SSHConfig) { + if e.vncSrv == nil { + return + } + + vncSrv, ok := e.vncSrv.(*vncserver.Server) + if !ok { + return + } + + if e.config.DisableVNCAuth != nil && *e.config.DisableVNCAuth { + vncSrv.SetDisableAuth(true) + return + } + + protoJWT := sshConf.GetJwtConfig() + if protoJWT == nil { + return + } + + audiences := protoJWT.GetAudiences() + if len(audiences) == 0 && protoJWT.GetAudience() != "" { + audiences = []string{protoJWT.GetAudience()} + } + + vncSrv.SetJWTConfig(&vncserver.JWTConfig{ + Issuer: protoJWT.GetIssuer(), + Audiences: audiences, + KeysLocation: protoJWT.GetKeysLocation(), + MaxTokenAge: protoJWT.GetMaxTokenAge(), + }) +} + +// updateVNCServerAuth updates VNC fine-grained access control from management. +func (e *Engine) updateVNCServerAuth(vncAuth *mgmProto.VNCAuth) { + if vncAuth == nil || e.vncSrv == nil { + return + } + + vncSrv, ok := e.vncSrv.(*vncserver.Server) + if !ok { + return + } + + protoUsers := vncAuth.GetAuthorizedUsers() + authorizedUsers := make([]sshuserhash.UserIDHash, len(protoUsers)) + for i, hash := range protoUsers { + if len(hash) != 16 { + log.Warnf("invalid VNC auth hash length %d, expected 16", len(hash)) + return + } + authorizedUsers[i] = sshuserhash.UserIDHash(hash) + } + + machineUsers := make(map[string][]uint32) + for osUser, indexes := range vncAuth.GetMachineUsers() { + machineUsers[osUser] = indexes.GetIndexes() + } + + vncSrv.UpdateVNCAuth(&sshauth.Config{ + UserIDClaim: vncAuth.GetUserIDClaim(), + AuthorizedUsers: authorizedUsers, + MachineUsers: machineUsers, + }) +} + +// GetVNCServerStatus returns whether the VNC server is running. +func (e *Engine) GetVNCServerStatus() bool { + return e.vncSrv != nil +} + +func (e *Engine) stopVNCServer() error { + if e.vncSrv == nil { + return nil + } + + if err := e.cleanupVNCPortRedirection(); err != nil { + log.Warnf("cleanup VNC port redirection: %v", err) + } + + if registrar, ok := e.firewall.(interface { + UnregisterNetstackService(protocol nftypes.Protocol, port uint16) + }); ok { + registrar.UnregisterNetstackService(nftypes.TCP, vncInternalPort) + } + + log.Info("stopping VNC server") + err := e.vncSrv.Stop() + e.vncSrv = nil + if err != nil { + return fmt.Errorf("stop VNC server: %w", err) + } + return nil +} diff --git a/client/internal/engine_vnc_darwin.go b/client/internal/engine_vnc_darwin.go new file mode 100644 index 000000000..0f034eb14 --- /dev/null +++ b/client/internal/engine_vnc_darwin.go @@ -0,0 +1,23 @@ +//go:build darwin && !ios + +package internal + +import ( + log "github.com/sirupsen/logrus" + + vncserver "github.com/netbirdio/netbird/client/vnc/server" +) + +func newPlatformVNC() (vncserver.ScreenCapturer, vncserver.InputInjector) { + capturer := vncserver.NewMacPoller() + injector, err := vncserver.NewMacInputInjector() + if err != nil { + log.Debugf("VNC: macOS input injector: %v", err) + return capturer, &vncserver.StubInputInjector{} + } + return capturer, injector +} + +func vncNeedsServiceMode() bool { + return false +} diff --git a/client/internal/engine_vnc_stub.go b/client/internal/engine_vnc_stub.go new file mode 100644 index 000000000..38e12ddb5 --- /dev/null +++ b/client/internal/engine_vnc_stub.go @@ -0,0 +1,13 @@ +//go:build !windows && !darwin && !freebsd && !(linux && !android) + +package internal + +import vncserver "github.com/netbirdio/netbird/client/vnc/server" + +func newPlatformVNC() (vncserver.ScreenCapturer, vncserver.InputInjector) { + return nil, nil +} + +func vncNeedsServiceMode() bool { + return false +} diff --git a/client/internal/engine_vnc_windows.go b/client/internal/engine_vnc_windows.go new file mode 100644 index 000000000..74b4c9ec8 --- /dev/null +++ b/client/internal/engine_vnc_windows.go @@ -0,0 +1,13 @@ +//go:build windows + +package internal + +import vncserver "github.com/netbirdio/netbird/client/vnc/server" + +func newPlatformVNC() (vncserver.ScreenCapturer, vncserver.InputInjector) { + return vncserver.NewDesktopCapturer(), vncserver.NewWindowsInputInjector() +} + +func vncNeedsServiceMode() bool { + return vncserver.GetCurrentSessionID() == 0 +} diff --git a/client/internal/engine_vnc_x11.go b/client/internal/engine_vnc_x11.go new file mode 100644 index 000000000..99d04e791 --- /dev/null +++ b/client/internal/engine_vnc_x11.go @@ -0,0 +1,23 @@ +//go:build (linux && !android) || freebsd + +package internal + +import ( + log "github.com/sirupsen/logrus" + + vncserver "github.com/netbirdio/netbird/client/vnc/server" +) + +func newPlatformVNC() (vncserver.ScreenCapturer, vncserver.InputInjector) { + capturer := vncserver.NewX11Poller("") + injector, err := vncserver.NewX11InputInjector("") + if err != nil { + log.Debugf("VNC: X11 input injector: %v", err) + return capturer, &vncserver.StubInputInjector{} + } + return capturer, injector +} + +func vncNeedsServiceMode() bool { + return false +} diff --git a/client/internal/profilemanager/config.go b/client/internal/profilemanager/config.go index 20c615d57..909e8739e 100644 --- a/client/internal/profilemanager/config.go +++ b/client/internal/profilemanager/config.go @@ -64,11 +64,13 @@ type ConfigInput struct { StateFilePath string PreSharedKey *string ServerSSHAllowed *bool + ServerVNCAllowed *bool EnableSSHRoot *bool EnableSSHSFTP *bool EnableSSHLocalPortForwarding *bool EnableSSHRemotePortForwarding *bool DisableSSHAuth *bool + DisableVNCAuth *bool SSHJWTCacheTTL *int NATExternalIPs []string CustomDNSAddress []byte @@ -114,11 +116,13 @@ type Config struct { RosenpassEnabled bool RosenpassPermissive bool ServerSSHAllowed *bool + ServerVNCAllowed *bool EnableSSHRoot *bool EnableSSHSFTP *bool EnableSSHLocalPortForwarding *bool EnableSSHRemotePortForwarding *bool DisableSSHAuth *bool + DisableVNCAuth *bool SSHJWTCacheTTL *int DisableClientRoutes bool @@ -415,6 +419,21 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) { updated = true } + if input.ServerVNCAllowed != nil { + if config.ServerVNCAllowed == nil || *input.ServerVNCAllowed != *config.ServerVNCAllowed { + if *input.ServerVNCAllowed { + log.Infof("enabling VNC server") + } else { + log.Infof("disabling VNC server") + } + config.ServerVNCAllowed = input.ServerVNCAllowed + updated = true + } + } else if config.ServerVNCAllowed == nil { + config.ServerVNCAllowed = util.True() + updated = true + } + if input.EnableSSHRoot != nil && input.EnableSSHRoot != config.EnableSSHRoot { if *input.EnableSSHRoot { log.Infof("enabling SSH root login") @@ -465,6 +484,16 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) { updated = true } + if input.DisableVNCAuth != nil && input.DisableVNCAuth != config.DisableVNCAuth { + if *input.DisableVNCAuth { + log.Infof("disabling VNC authentication") + } else { + log.Infof("enabling VNC authentication") + } + config.DisableVNCAuth = input.DisableVNCAuth + updated = true + } + if input.SSHJWTCacheTTL != nil && input.SSHJWTCacheTTL != config.SSHJWTCacheTTL { log.Infof("updating SSH JWT cache TTL to %d seconds", *input.SSHJWTCacheTTL) config.SSHJWTCacheTTL = input.SSHJWTCacheTTL diff --git a/client/internal/statemanager/manager.go b/client/internal/statemanager/manager.go index 2c9e46290..7d4cf3deb 100644 --- a/client/internal/statemanager/manager.go +++ b/client/internal/statemanager/manager.go @@ -74,6 +74,14 @@ func New(filePath string) *Manager { } } +// FilePath returns the path of the underlying state file. +func (m *Manager) FilePath() string { + if m == nil { + return "" + } + return m.filePath +} + // Start starts the state manager periodic save routine func (m *Manager) Start() { if m == nil { diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index 6506307d3..0024accd8 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -242,7 +242,7 @@ func (x SystemEvent_Severity) Number() protoreflect.EnumNumber { // Deprecated: Use SystemEvent_Severity.Descriptor instead. func (SystemEvent_Severity) EnumDescriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{53, 0} + return file_daemon_proto_rawDescGZIP(), []int{54, 0} } type SystemEvent_Category int32 @@ -297,7 +297,7 @@ func (x SystemEvent_Category) Number() protoreflect.EnumNumber { // Deprecated: Use SystemEvent_Category.Descriptor instead. func (SystemEvent_Category) EnumDescriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{53, 1} + return file_daemon_proto_rawDescGZIP(), []int{54, 1} } type EmptyRequest struct { @@ -472,6 +472,8 @@ type LoginRequest struct { EnableSSHRemotePortForwarding *bool `protobuf:"varint,37,opt,name=enableSSHRemotePortForwarding,proto3,oneof" json:"enableSSHRemotePortForwarding,omitempty"` DisableSSHAuth *bool `protobuf:"varint,38,opt,name=disableSSHAuth,proto3,oneof" json:"disableSSHAuth,omitempty"` SshJWTCacheTTL *int32 `protobuf:"varint,39,opt,name=sshJWTCacheTTL,proto3,oneof" json:"sshJWTCacheTTL,omitempty"` + ServerVNCAllowed *bool `protobuf:"varint,41,opt,name=serverVNCAllowed,proto3,oneof" json:"serverVNCAllowed,omitempty"` + DisableVNCAuth *bool `protobuf:"varint,42,opt,name=disableVNCAuth,proto3,oneof" json:"disableVNCAuth,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -780,6 +782,20 @@ func (x *LoginRequest) GetSshJWTCacheTTL() int32 { return 0 } +func (x *LoginRequest) GetServerVNCAllowed() bool { + if x != nil && x.ServerVNCAllowed != nil { + return *x.ServerVNCAllowed + } + return false +} + +func (x *LoginRequest) GetDisableVNCAuth() bool { + if x != nil && x.DisableVNCAuth != nil { + return *x.DisableVNCAuth + } + return false +} + type LoginResponse struct { state protoimpl.MessageState `protogen:"open.v1"` NeedsSSOLogin bool `protobuf:"varint,1,opt,name=needsSSOLogin,proto3" json:"needsSSOLogin,omitempty"` @@ -1312,6 +1328,8 @@ type GetConfigResponse struct { EnableSSHRemotePortForwarding bool `protobuf:"varint,23,opt,name=enableSSHRemotePortForwarding,proto3" json:"enableSSHRemotePortForwarding,omitempty"` DisableSSHAuth bool `protobuf:"varint,25,opt,name=disableSSHAuth,proto3" json:"disableSSHAuth,omitempty"` SshJWTCacheTTL int32 `protobuf:"varint,26,opt,name=sshJWTCacheTTL,proto3" json:"sshJWTCacheTTL,omitempty"` + ServerVNCAllowed bool `protobuf:"varint,28,opt,name=serverVNCAllowed,proto3" json:"serverVNCAllowed,omitempty"` + DisableVNCAuth bool `protobuf:"varint,29,opt,name=disableVNCAuth,proto3" json:"disableVNCAuth,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1528,6 +1546,20 @@ func (x *GetConfigResponse) GetSshJWTCacheTTL() int32 { return 0 } +func (x *GetConfigResponse) GetServerVNCAllowed() bool { + if x != nil { + return x.ServerVNCAllowed + } + return false +} + +func (x *GetConfigResponse) GetDisableVNCAuth() bool { + if x != nil { + return x.DisableVNCAuth + } + return false +} + // PeerState contains the latest state of a peer type PeerState struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -2183,6 +2215,51 @@ func (x *SSHServerState) GetSessions() []*SSHSessionInfo { return nil } +// VNCServerState contains the latest state of the VNC server +type VNCServerState struct { + state protoimpl.MessageState `protogen:"open.v1"` + Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VNCServerState) Reset() { + *x = VNCServerState{} + mi := &file_daemon_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VNCServerState) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VNCServerState) ProtoMessage() {} + +func (x *VNCServerState) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[23] + 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 VNCServerState.ProtoReflect.Descriptor instead. +func (*VNCServerState) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{23} +} + +func (x *VNCServerState) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + // FullStatus contains the full state held by the Status instance type FullStatus struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -2196,13 +2273,14 @@ type FullStatus struct { Events []*SystemEvent `protobuf:"bytes,7,rep,name=events,proto3" json:"events,omitempty"` LazyConnectionEnabled bool `protobuf:"varint,9,opt,name=lazyConnectionEnabled,proto3" json:"lazyConnectionEnabled,omitempty"` SshServerState *SSHServerState `protobuf:"bytes,10,opt,name=sshServerState,proto3" json:"sshServerState,omitempty"` + VncServerState *VNCServerState `protobuf:"bytes,11,opt,name=vncServerState,proto3" json:"vncServerState,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *FullStatus) Reset() { *x = FullStatus{} - mi := &file_daemon_proto_msgTypes[23] + mi := &file_daemon_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2214,7 +2292,7 @@ func (x *FullStatus) String() string { func (*FullStatus) ProtoMessage() {} func (x *FullStatus) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[23] + mi := &file_daemon_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2227,7 +2305,7 @@ func (x *FullStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use FullStatus.ProtoReflect.Descriptor instead. func (*FullStatus) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{23} + return file_daemon_proto_rawDescGZIP(), []int{24} } func (x *FullStatus) GetManagementState() *ManagementState { @@ -2300,6 +2378,13 @@ func (x *FullStatus) GetSshServerState() *SSHServerState { return nil } +func (x *FullStatus) GetVncServerState() *VNCServerState { + if x != nil { + return x.VncServerState + } + return nil +} + // Networks type ListNetworksRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -2309,7 +2394,7 @@ type ListNetworksRequest struct { func (x *ListNetworksRequest) Reset() { *x = ListNetworksRequest{} - mi := &file_daemon_proto_msgTypes[24] + mi := &file_daemon_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2321,7 +2406,7 @@ func (x *ListNetworksRequest) String() string { func (*ListNetworksRequest) ProtoMessage() {} func (x *ListNetworksRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[24] + mi := &file_daemon_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2334,7 +2419,7 @@ func (x *ListNetworksRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListNetworksRequest.ProtoReflect.Descriptor instead. func (*ListNetworksRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{24} + return file_daemon_proto_rawDescGZIP(), []int{25} } type ListNetworksResponse struct { @@ -2346,7 +2431,7 @@ type ListNetworksResponse struct { func (x *ListNetworksResponse) Reset() { *x = ListNetworksResponse{} - mi := &file_daemon_proto_msgTypes[25] + mi := &file_daemon_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2358,7 +2443,7 @@ func (x *ListNetworksResponse) String() string { func (*ListNetworksResponse) ProtoMessage() {} func (x *ListNetworksResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[25] + mi := &file_daemon_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2371,7 +2456,7 @@ func (x *ListNetworksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListNetworksResponse.ProtoReflect.Descriptor instead. func (*ListNetworksResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{25} + return file_daemon_proto_rawDescGZIP(), []int{26} } func (x *ListNetworksResponse) GetRoutes() []*Network { @@ -2392,7 +2477,7 @@ type SelectNetworksRequest struct { func (x *SelectNetworksRequest) Reset() { *x = SelectNetworksRequest{} - mi := &file_daemon_proto_msgTypes[26] + mi := &file_daemon_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2404,7 +2489,7 @@ func (x *SelectNetworksRequest) String() string { func (*SelectNetworksRequest) ProtoMessage() {} func (x *SelectNetworksRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[26] + mi := &file_daemon_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2417,7 +2502,7 @@ func (x *SelectNetworksRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SelectNetworksRequest.ProtoReflect.Descriptor instead. func (*SelectNetworksRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{26} + return file_daemon_proto_rawDescGZIP(), []int{27} } func (x *SelectNetworksRequest) GetNetworkIDs() []string { @@ -2449,7 +2534,7 @@ type SelectNetworksResponse struct { func (x *SelectNetworksResponse) Reset() { *x = SelectNetworksResponse{} - mi := &file_daemon_proto_msgTypes[27] + mi := &file_daemon_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2461,7 +2546,7 @@ func (x *SelectNetworksResponse) String() string { func (*SelectNetworksResponse) ProtoMessage() {} func (x *SelectNetworksResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[27] + mi := &file_daemon_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2474,7 +2559,7 @@ func (x *SelectNetworksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SelectNetworksResponse.ProtoReflect.Descriptor instead. func (*SelectNetworksResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{27} + return file_daemon_proto_rawDescGZIP(), []int{28} } type IPList struct { @@ -2486,7 +2571,7 @@ type IPList struct { func (x *IPList) Reset() { *x = IPList{} - mi := &file_daemon_proto_msgTypes[28] + mi := &file_daemon_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2498,7 +2583,7 @@ func (x *IPList) String() string { func (*IPList) ProtoMessage() {} func (x *IPList) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[28] + mi := &file_daemon_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2511,7 +2596,7 @@ func (x *IPList) ProtoReflect() protoreflect.Message { // Deprecated: Use IPList.ProtoReflect.Descriptor instead. func (*IPList) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{28} + return file_daemon_proto_rawDescGZIP(), []int{29} } func (x *IPList) GetIps() []string { @@ -2534,7 +2619,7 @@ type Network struct { func (x *Network) Reset() { *x = Network{} - mi := &file_daemon_proto_msgTypes[29] + mi := &file_daemon_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2546,7 +2631,7 @@ func (x *Network) String() string { func (*Network) ProtoMessage() {} func (x *Network) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[29] + mi := &file_daemon_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2559,7 +2644,7 @@ func (x *Network) ProtoReflect() protoreflect.Message { // Deprecated: Use Network.ProtoReflect.Descriptor instead. func (*Network) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{29} + return file_daemon_proto_rawDescGZIP(), []int{30} } func (x *Network) GetID() string { @@ -2611,7 +2696,7 @@ type PortInfo struct { func (x *PortInfo) Reset() { *x = PortInfo{} - mi := &file_daemon_proto_msgTypes[30] + mi := &file_daemon_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2623,7 +2708,7 @@ func (x *PortInfo) String() string { func (*PortInfo) ProtoMessage() {} func (x *PortInfo) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[30] + mi := &file_daemon_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2636,7 +2721,7 @@ func (x *PortInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use PortInfo.ProtoReflect.Descriptor instead. func (*PortInfo) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{30} + return file_daemon_proto_rawDescGZIP(), []int{31} } func (x *PortInfo) GetPortSelection() isPortInfo_PortSelection { @@ -2693,7 +2778,7 @@ type ForwardingRule struct { func (x *ForwardingRule) Reset() { *x = ForwardingRule{} - mi := &file_daemon_proto_msgTypes[31] + mi := &file_daemon_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2705,7 +2790,7 @@ func (x *ForwardingRule) String() string { func (*ForwardingRule) ProtoMessage() {} func (x *ForwardingRule) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[31] + mi := &file_daemon_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2718,7 +2803,7 @@ func (x *ForwardingRule) ProtoReflect() protoreflect.Message { // Deprecated: Use ForwardingRule.ProtoReflect.Descriptor instead. func (*ForwardingRule) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{31} + return file_daemon_proto_rawDescGZIP(), []int{32} } func (x *ForwardingRule) GetProtocol() string { @@ -2765,7 +2850,7 @@ type ForwardingRulesResponse struct { func (x *ForwardingRulesResponse) Reset() { *x = ForwardingRulesResponse{} - mi := &file_daemon_proto_msgTypes[32] + mi := &file_daemon_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2777,7 +2862,7 @@ func (x *ForwardingRulesResponse) String() string { func (*ForwardingRulesResponse) ProtoMessage() {} func (x *ForwardingRulesResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[32] + mi := &file_daemon_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2790,7 +2875,7 @@ func (x *ForwardingRulesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ForwardingRulesResponse.ProtoReflect.Descriptor instead. func (*ForwardingRulesResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{32} + return file_daemon_proto_rawDescGZIP(), []int{33} } func (x *ForwardingRulesResponse) GetRules() []*ForwardingRule { @@ -2813,7 +2898,7 @@ type DebugBundleRequest struct { func (x *DebugBundleRequest) Reset() { *x = DebugBundleRequest{} - mi := &file_daemon_proto_msgTypes[33] + mi := &file_daemon_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2825,7 +2910,7 @@ func (x *DebugBundleRequest) String() string { func (*DebugBundleRequest) ProtoMessage() {} func (x *DebugBundleRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[33] + mi := &file_daemon_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2838,7 +2923,7 @@ func (x *DebugBundleRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DebugBundleRequest.ProtoReflect.Descriptor instead. func (*DebugBundleRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{33} + return file_daemon_proto_rawDescGZIP(), []int{34} } func (x *DebugBundleRequest) GetAnonymize() bool { @@ -2880,7 +2965,7 @@ type DebugBundleResponse struct { func (x *DebugBundleResponse) Reset() { *x = DebugBundleResponse{} - mi := &file_daemon_proto_msgTypes[34] + mi := &file_daemon_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2892,7 +2977,7 @@ func (x *DebugBundleResponse) String() string { func (*DebugBundleResponse) ProtoMessage() {} func (x *DebugBundleResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[34] + mi := &file_daemon_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2905,7 +2990,7 @@ func (x *DebugBundleResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DebugBundleResponse.ProtoReflect.Descriptor instead. func (*DebugBundleResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{34} + return file_daemon_proto_rawDescGZIP(), []int{35} } func (x *DebugBundleResponse) GetPath() string { @@ -2937,7 +3022,7 @@ type GetLogLevelRequest struct { func (x *GetLogLevelRequest) Reset() { *x = GetLogLevelRequest{} - mi := &file_daemon_proto_msgTypes[35] + mi := &file_daemon_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2949,7 +3034,7 @@ func (x *GetLogLevelRequest) String() string { func (*GetLogLevelRequest) ProtoMessage() {} func (x *GetLogLevelRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[35] + mi := &file_daemon_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2962,7 +3047,7 @@ func (x *GetLogLevelRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetLogLevelRequest.ProtoReflect.Descriptor instead. func (*GetLogLevelRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{35} + return file_daemon_proto_rawDescGZIP(), []int{36} } type GetLogLevelResponse struct { @@ -2974,7 +3059,7 @@ type GetLogLevelResponse struct { func (x *GetLogLevelResponse) Reset() { *x = GetLogLevelResponse{} - mi := &file_daemon_proto_msgTypes[36] + mi := &file_daemon_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2986,7 +3071,7 @@ func (x *GetLogLevelResponse) String() string { func (*GetLogLevelResponse) ProtoMessage() {} func (x *GetLogLevelResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[36] + mi := &file_daemon_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2999,7 +3084,7 @@ func (x *GetLogLevelResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetLogLevelResponse.ProtoReflect.Descriptor instead. func (*GetLogLevelResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{36} + return file_daemon_proto_rawDescGZIP(), []int{37} } func (x *GetLogLevelResponse) GetLevel() LogLevel { @@ -3018,7 +3103,7 @@ type SetLogLevelRequest struct { func (x *SetLogLevelRequest) Reset() { *x = SetLogLevelRequest{} - mi := &file_daemon_proto_msgTypes[37] + mi := &file_daemon_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3030,7 +3115,7 @@ func (x *SetLogLevelRequest) String() string { func (*SetLogLevelRequest) ProtoMessage() {} func (x *SetLogLevelRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[37] + mi := &file_daemon_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3043,7 +3128,7 @@ func (x *SetLogLevelRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetLogLevelRequest.ProtoReflect.Descriptor instead. func (*SetLogLevelRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{37} + return file_daemon_proto_rawDescGZIP(), []int{38} } func (x *SetLogLevelRequest) GetLevel() LogLevel { @@ -3061,7 +3146,7 @@ type SetLogLevelResponse struct { func (x *SetLogLevelResponse) Reset() { *x = SetLogLevelResponse{} - mi := &file_daemon_proto_msgTypes[38] + mi := &file_daemon_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3073,7 +3158,7 @@ func (x *SetLogLevelResponse) String() string { func (*SetLogLevelResponse) ProtoMessage() {} func (x *SetLogLevelResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[38] + mi := &file_daemon_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3086,7 +3171,7 @@ func (x *SetLogLevelResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SetLogLevelResponse.ProtoReflect.Descriptor instead. func (*SetLogLevelResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{38} + return file_daemon_proto_rawDescGZIP(), []int{39} } // State represents a daemon state entry @@ -3099,7 +3184,7 @@ type State struct { func (x *State) Reset() { *x = State{} - mi := &file_daemon_proto_msgTypes[39] + mi := &file_daemon_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3111,7 +3196,7 @@ func (x *State) String() string { func (*State) ProtoMessage() {} func (x *State) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[39] + mi := &file_daemon_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3124,7 +3209,7 @@ func (x *State) ProtoReflect() protoreflect.Message { // Deprecated: Use State.ProtoReflect.Descriptor instead. func (*State) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{39} + return file_daemon_proto_rawDescGZIP(), []int{40} } func (x *State) GetName() string { @@ -3143,7 +3228,7 @@ type ListStatesRequest struct { func (x *ListStatesRequest) Reset() { *x = ListStatesRequest{} - mi := &file_daemon_proto_msgTypes[40] + mi := &file_daemon_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3155,7 +3240,7 @@ func (x *ListStatesRequest) String() string { func (*ListStatesRequest) ProtoMessage() {} func (x *ListStatesRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[40] + mi := &file_daemon_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3168,7 +3253,7 @@ func (x *ListStatesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListStatesRequest.ProtoReflect.Descriptor instead. func (*ListStatesRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{40} + return file_daemon_proto_rawDescGZIP(), []int{41} } // ListStatesResponse contains a list of states @@ -3181,7 +3266,7 @@ type ListStatesResponse struct { func (x *ListStatesResponse) Reset() { *x = ListStatesResponse{} - mi := &file_daemon_proto_msgTypes[41] + mi := &file_daemon_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3193,7 +3278,7 @@ func (x *ListStatesResponse) String() string { func (*ListStatesResponse) ProtoMessage() {} func (x *ListStatesResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[41] + mi := &file_daemon_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3206,7 +3291,7 @@ func (x *ListStatesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListStatesResponse.ProtoReflect.Descriptor instead. func (*ListStatesResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{41} + return file_daemon_proto_rawDescGZIP(), []int{42} } func (x *ListStatesResponse) GetStates() []*State { @@ -3227,7 +3312,7 @@ type CleanStateRequest struct { func (x *CleanStateRequest) Reset() { *x = CleanStateRequest{} - mi := &file_daemon_proto_msgTypes[42] + mi := &file_daemon_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3239,7 +3324,7 @@ func (x *CleanStateRequest) String() string { func (*CleanStateRequest) ProtoMessage() {} func (x *CleanStateRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[42] + mi := &file_daemon_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3252,7 +3337,7 @@ func (x *CleanStateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CleanStateRequest.ProtoReflect.Descriptor instead. func (*CleanStateRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{42} + return file_daemon_proto_rawDescGZIP(), []int{43} } func (x *CleanStateRequest) GetStateName() string { @@ -3279,7 +3364,7 @@ type CleanStateResponse struct { func (x *CleanStateResponse) Reset() { *x = CleanStateResponse{} - mi := &file_daemon_proto_msgTypes[43] + mi := &file_daemon_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3291,7 +3376,7 @@ func (x *CleanStateResponse) String() string { func (*CleanStateResponse) ProtoMessage() {} func (x *CleanStateResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[43] + mi := &file_daemon_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3304,7 +3389,7 @@ func (x *CleanStateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CleanStateResponse.ProtoReflect.Descriptor instead. func (*CleanStateResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{43} + return file_daemon_proto_rawDescGZIP(), []int{44} } func (x *CleanStateResponse) GetCleanedStates() int32 { @@ -3325,7 +3410,7 @@ type DeleteStateRequest struct { func (x *DeleteStateRequest) Reset() { *x = DeleteStateRequest{} - mi := &file_daemon_proto_msgTypes[44] + mi := &file_daemon_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3337,7 +3422,7 @@ func (x *DeleteStateRequest) String() string { func (*DeleteStateRequest) ProtoMessage() {} func (x *DeleteStateRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[44] + mi := &file_daemon_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3350,7 +3435,7 @@ func (x *DeleteStateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteStateRequest.ProtoReflect.Descriptor instead. func (*DeleteStateRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{44} + return file_daemon_proto_rawDescGZIP(), []int{45} } func (x *DeleteStateRequest) GetStateName() string { @@ -3377,7 +3462,7 @@ type DeleteStateResponse struct { func (x *DeleteStateResponse) Reset() { *x = DeleteStateResponse{} - mi := &file_daemon_proto_msgTypes[45] + mi := &file_daemon_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3389,7 +3474,7 @@ func (x *DeleteStateResponse) String() string { func (*DeleteStateResponse) ProtoMessage() {} func (x *DeleteStateResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[45] + mi := &file_daemon_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3402,7 +3487,7 @@ func (x *DeleteStateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteStateResponse.ProtoReflect.Descriptor instead. func (*DeleteStateResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{45} + return file_daemon_proto_rawDescGZIP(), []int{46} } func (x *DeleteStateResponse) GetDeletedStates() int32 { @@ -3421,7 +3506,7 @@ type SetSyncResponsePersistenceRequest struct { func (x *SetSyncResponsePersistenceRequest) Reset() { *x = SetSyncResponsePersistenceRequest{} - mi := &file_daemon_proto_msgTypes[46] + mi := &file_daemon_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3433,7 +3518,7 @@ func (x *SetSyncResponsePersistenceRequest) String() string { func (*SetSyncResponsePersistenceRequest) ProtoMessage() {} func (x *SetSyncResponsePersistenceRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[46] + mi := &file_daemon_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3446,7 +3531,7 @@ func (x *SetSyncResponsePersistenceRequest) ProtoReflect() protoreflect.Message // Deprecated: Use SetSyncResponsePersistenceRequest.ProtoReflect.Descriptor instead. func (*SetSyncResponsePersistenceRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{46} + return file_daemon_proto_rawDescGZIP(), []int{47} } func (x *SetSyncResponsePersistenceRequest) GetEnabled() bool { @@ -3464,7 +3549,7 @@ type SetSyncResponsePersistenceResponse struct { func (x *SetSyncResponsePersistenceResponse) Reset() { *x = SetSyncResponsePersistenceResponse{} - mi := &file_daemon_proto_msgTypes[47] + mi := &file_daemon_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3476,7 +3561,7 @@ func (x *SetSyncResponsePersistenceResponse) String() string { func (*SetSyncResponsePersistenceResponse) ProtoMessage() {} func (x *SetSyncResponsePersistenceResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[47] + mi := &file_daemon_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3489,7 +3574,7 @@ func (x *SetSyncResponsePersistenceResponse) ProtoReflect() protoreflect.Message // Deprecated: Use SetSyncResponsePersistenceResponse.ProtoReflect.Descriptor instead. func (*SetSyncResponsePersistenceResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{47} + return file_daemon_proto_rawDescGZIP(), []int{48} } type TCPFlags struct { @@ -3506,7 +3591,7 @@ type TCPFlags struct { func (x *TCPFlags) Reset() { *x = TCPFlags{} - mi := &file_daemon_proto_msgTypes[48] + mi := &file_daemon_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3518,7 +3603,7 @@ func (x *TCPFlags) String() string { func (*TCPFlags) ProtoMessage() {} func (x *TCPFlags) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[48] + mi := &file_daemon_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3531,7 +3616,7 @@ func (x *TCPFlags) ProtoReflect() protoreflect.Message { // Deprecated: Use TCPFlags.ProtoReflect.Descriptor instead. func (*TCPFlags) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{48} + return file_daemon_proto_rawDescGZIP(), []int{49} } func (x *TCPFlags) GetSyn() bool { @@ -3593,7 +3678,7 @@ type TracePacketRequest struct { func (x *TracePacketRequest) Reset() { *x = TracePacketRequest{} - mi := &file_daemon_proto_msgTypes[49] + mi := &file_daemon_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3605,7 +3690,7 @@ func (x *TracePacketRequest) String() string { func (*TracePacketRequest) ProtoMessage() {} func (x *TracePacketRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[49] + mi := &file_daemon_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3618,7 +3703,7 @@ func (x *TracePacketRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TracePacketRequest.ProtoReflect.Descriptor instead. func (*TracePacketRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{49} + return file_daemon_proto_rawDescGZIP(), []int{50} } func (x *TracePacketRequest) GetSourceIp() string { @@ -3696,7 +3781,7 @@ type TraceStage struct { func (x *TraceStage) Reset() { *x = TraceStage{} - mi := &file_daemon_proto_msgTypes[50] + mi := &file_daemon_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3708,7 +3793,7 @@ func (x *TraceStage) String() string { func (*TraceStage) ProtoMessage() {} func (x *TraceStage) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[50] + mi := &file_daemon_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3721,7 +3806,7 @@ func (x *TraceStage) ProtoReflect() protoreflect.Message { // Deprecated: Use TraceStage.ProtoReflect.Descriptor instead. func (*TraceStage) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{50} + return file_daemon_proto_rawDescGZIP(), []int{51} } func (x *TraceStage) GetName() string { @@ -3762,7 +3847,7 @@ type TracePacketResponse struct { func (x *TracePacketResponse) Reset() { *x = TracePacketResponse{} - mi := &file_daemon_proto_msgTypes[51] + mi := &file_daemon_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3774,7 +3859,7 @@ func (x *TracePacketResponse) String() string { func (*TracePacketResponse) ProtoMessage() {} func (x *TracePacketResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[51] + mi := &file_daemon_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3787,7 +3872,7 @@ func (x *TracePacketResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use TracePacketResponse.ProtoReflect.Descriptor instead. func (*TracePacketResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{51} + return file_daemon_proto_rawDescGZIP(), []int{52} } func (x *TracePacketResponse) GetStages() []*TraceStage { @@ -3812,7 +3897,7 @@ type SubscribeRequest struct { func (x *SubscribeRequest) Reset() { *x = SubscribeRequest{} - mi := &file_daemon_proto_msgTypes[52] + mi := &file_daemon_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3824,7 +3909,7 @@ func (x *SubscribeRequest) String() string { func (*SubscribeRequest) ProtoMessage() {} func (x *SubscribeRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[52] + mi := &file_daemon_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3837,7 +3922,7 @@ func (x *SubscribeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SubscribeRequest.ProtoReflect.Descriptor instead. func (*SubscribeRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{52} + return file_daemon_proto_rawDescGZIP(), []int{53} } type SystemEvent struct { @@ -3855,7 +3940,7 @@ type SystemEvent struct { func (x *SystemEvent) Reset() { *x = SystemEvent{} - mi := &file_daemon_proto_msgTypes[53] + mi := &file_daemon_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3867,7 +3952,7 @@ func (x *SystemEvent) String() string { func (*SystemEvent) ProtoMessage() {} func (x *SystemEvent) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[53] + mi := &file_daemon_proto_msgTypes[54] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3880,7 +3965,7 @@ func (x *SystemEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use SystemEvent.ProtoReflect.Descriptor instead. func (*SystemEvent) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{53} + return file_daemon_proto_rawDescGZIP(), []int{54} } func (x *SystemEvent) GetId() string { @@ -3940,7 +4025,7 @@ type GetEventsRequest struct { func (x *GetEventsRequest) Reset() { *x = GetEventsRequest{} - mi := &file_daemon_proto_msgTypes[54] + mi := &file_daemon_proto_msgTypes[55] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3952,7 +4037,7 @@ func (x *GetEventsRequest) String() string { func (*GetEventsRequest) ProtoMessage() {} func (x *GetEventsRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[54] + mi := &file_daemon_proto_msgTypes[55] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3965,7 +4050,7 @@ func (x *GetEventsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetEventsRequest.ProtoReflect.Descriptor instead. func (*GetEventsRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{54} + return file_daemon_proto_rawDescGZIP(), []int{55} } type GetEventsResponse struct { @@ -3977,7 +4062,7 @@ type GetEventsResponse struct { func (x *GetEventsResponse) Reset() { *x = GetEventsResponse{} - mi := &file_daemon_proto_msgTypes[55] + mi := &file_daemon_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3989,7 +4074,7 @@ func (x *GetEventsResponse) String() string { func (*GetEventsResponse) ProtoMessage() {} func (x *GetEventsResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[55] + mi := &file_daemon_proto_msgTypes[56] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4002,7 +4087,7 @@ func (x *GetEventsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetEventsResponse.ProtoReflect.Descriptor instead. func (*GetEventsResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{55} + return file_daemon_proto_rawDescGZIP(), []int{56} } func (x *GetEventsResponse) GetEvents() []*SystemEvent { @@ -4022,7 +4107,7 @@ type SwitchProfileRequest struct { func (x *SwitchProfileRequest) Reset() { *x = SwitchProfileRequest{} - mi := &file_daemon_proto_msgTypes[56] + mi := &file_daemon_proto_msgTypes[57] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4034,7 +4119,7 @@ func (x *SwitchProfileRequest) String() string { func (*SwitchProfileRequest) ProtoMessage() {} func (x *SwitchProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[56] + mi := &file_daemon_proto_msgTypes[57] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4047,7 +4132,7 @@ func (x *SwitchProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SwitchProfileRequest.ProtoReflect.Descriptor instead. func (*SwitchProfileRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{56} + return file_daemon_proto_rawDescGZIP(), []int{57} } func (x *SwitchProfileRequest) GetProfileName() string { @@ -4072,7 +4157,7 @@ type SwitchProfileResponse struct { func (x *SwitchProfileResponse) Reset() { *x = SwitchProfileResponse{} - mi := &file_daemon_proto_msgTypes[57] + mi := &file_daemon_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4084,7 +4169,7 @@ func (x *SwitchProfileResponse) String() string { func (*SwitchProfileResponse) ProtoMessage() {} func (x *SwitchProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[57] + mi := &file_daemon_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4097,7 +4182,7 @@ func (x *SwitchProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SwitchProfileResponse.ProtoReflect.Descriptor instead. func (*SwitchProfileResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{57} + return file_daemon_proto_rawDescGZIP(), []int{58} } type SetConfigRequest struct { @@ -4139,13 +4224,15 @@ type SetConfigRequest struct { EnableSSHRemotePortForwarding *bool `protobuf:"varint,32,opt,name=enableSSHRemotePortForwarding,proto3,oneof" json:"enableSSHRemotePortForwarding,omitempty"` DisableSSHAuth *bool `protobuf:"varint,33,opt,name=disableSSHAuth,proto3,oneof" json:"disableSSHAuth,omitempty"` SshJWTCacheTTL *int32 `protobuf:"varint,34,opt,name=sshJWTCacheTTL,proto3,oneof" json:"sshJWTCacheTTL,omitempty"` + ServerVNCAllowed *bool `protobuf:"varint,36,opt,name=serverVNCAllowed,proto3,oneof" json:"serverVNCAllowed,omitempty"` + DisableVNCAuth *bool `protobuf:"varint,37,opt,name=disableVNCAuth,proto3,oneof" json:"disableVNCAuth,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SetConfigRequest) Reset() { *x = SetConfigRequest{} - mi := &file_daemon_proto_msgTypes[58] + mi := &file_daemon_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4157,7 +4244,7 @@ func (x *SetConfigRequest) String() string { func (*SetConfigRequest) ProtoMessage() {} func (x *SetConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[58] + mi := &file_daemon_proto_msgTypes[59] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4170,7 +4257,7 @@ func (x *SetConfigRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetConfigRequest.ProtoReflect.Descriptor instead. func (*SetConfigRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{58} + return file_daemon_proto_rawDescGZIP(), []int{59} } func (x *SetConfigRequest) GetUsername() string { @@ -4411,6 +4498,20 @@ func (x *SetConfigRequest) GetSshJWTCacheTTL() int32 { return 0 } +func (x *SetConfigRequest) GetServerVNCAllowed() bool { + if x != nil && x.ServerVNCAllowed != nil { + return *x.ServerVNCAllowed + } + return false +} + +func (x *SetConfigRequest) GetDisableVNCAuth() bool { + if x != nil && x.DisableVNCAuth != nil { + return *x.DisableVNCAuth + } + return false +} + type SetConfigResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -4419,7 +4520,7 @@ type SetConfigResponse struct { func (x *SetConfigResponse) Reset() { *x = SetConfigResponse{} - mi := &file_daemon_proto_msgTypes[59] + mi := &file_daemon_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4431,7 +4532,7 @@ func (x *SetConfigResponse) String() string { func (*SetConfigResponse) ProtoMessage() {} func (x *SetConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[59] + mi := &file_daemon_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4444,7 +4545,7 @@ func (x *SetConfigResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SetConfigResponse.ProtoReflect.Descriptor instead. func (*SetConfigResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{59} + return file_daemon_proto_rawDescGZIP(), []int{60} } type AddProfileRequest struct { @@ -4457,7 +4558,7 @@ type AddProfileRequest struct { func (x *AddProfileRequest) Reset() { *x = AddProfileRequest{} - mi := &file_daemon_proto_msgTypes[60] + mi := &file_daemon_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4469,7 +4570,7 @@ func (x *AddProfileRequest) String() string { func (*AddProfileRequest) ProtoMessage() {} func (x *AddProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[60] + mi := &file_daemon_proto_msgTypes[61] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4482,7 +4583,7 @@ func (x *AddProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AddProfileRequest.ProtoReflect.Descriptor instead. func (*AddProfileRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{60} + return file_daemon_proto_rawDescGZIP(), []int{61} } func (x *AddProfileRequest) GetUsername() string { @@ -4507,7 +4608,7 @@ type AddProfileResponse struct { func (x *AddProfileResponse) Reset() { *x = AddProfileResponse{} - mi := &file_daemon_proto_msgTypes[61] + mi := &file_daemon_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4519,7 +4620,7 @@ func (x *AddProfileResponse) String() string { func (*AddProfileResponse) ProtoMessage() {} func (x *AddProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[61] + mi := &file_daemon_proto_msgTypes[62] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4532,7 +4633,7 @@ func (x *AddProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AddProfileResponse.ProtoReflect.Descriptor instead. func (*AddProfileResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{61} + return file_daemon_proto_rawDescGZIP(), []int{62} } type RemoveProfileRequest struct { @@ -4545,7 +4646,7 @@ type RemoveProfileRequest struct { func (x *RemoveProfileRequest) Reset() { *x = RemoveProfileRequest{} - mi := &file_daemon_proto_msgTypes[62] + mi := &file_daemon_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4557,7 +4658,7 @@ func (x *RemoveProfileRequest) String() string { func (*RemoveProfileRequest) ProtoMessage() {} func (x *RemoveProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[62] + mi := &file_daemon_proto_msgTypes[63] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4570,7 +4671,7 @@ func (x *RemoveProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveProfileRequest.ProtoReflect.Descriptor instead. func (*RemoveProfileRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{62} + return file_daemon_proto_rawDescGZIP(), []int{63} } func (x *RemoveProfileRequest) GetUsername() string { @@ -4595,7 +4696,7 @@ type RemoveProfileResponse struct { func (x *RemoveProfileResponse) Reset() { *x = RemoveProfileResponse{} - mi := &file_daemon_proto_msgTypes[63] + mi := &file_daemon_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4607,7 +4708,7 @@ func (x *RemoveProfileResponse) String() string { func (*RemoveProfileResponse) ProtoMessage() {} func (x *RemoveProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[63] + mi := &file_daemon_proto_msgTypes[64] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4620,7 +4721,7 @@ func (x *RemoveProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveProfileResponse.ProtoReflect.Descriptor instead. func (*RemoveProfileResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{63} + return file_daemon_proto_rawDescGZIP(), []int{64} } type ListProfilesRequest struct { @@ -4632,7 +4733,7 @@ type ListProfilesRequest struct { func (x *ListProfilesRequest) Reset() { *x = ListProfilesRequest{} - mi := &file_daemon_proto_msgTypes[64] + mi := &file_daemon_proto_msgTypes[65] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4644,7 +4745,7 @@ func (x *ListProfilesRequest) String() string { func (*ListProfilesRequest) ProtoMessage() {} func (x *ListProfilesRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[64] + mi := &file_daemon_proto_msgTypes[65] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4657,7 +4758,7 @@ func (x *ListProfilesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListProfilesRequest.ProtoReflect.Descriptor instead. func (*ListProfilesRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{64} + return file_daemon_proto_rawDescGZIP(), []int{65} } func (x *ListProfilesRequest) GetUsername() string { @@ -4676,7 +4777,7 @@ type ListProfilesResponse struct { func (x *ListProfilesResponse) Reset() { *x = ListProfilesResponse{} - mi := &file_daemon_proto_msgTypes[65] + mi := &file_daemon_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4688,7 +4789,7 @@ func (x *ListProfilesResponse) String() string { func (*ListProfilesResponse) ProtoMessage() {} func (x *ListProfilesResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[65] + mi := &file_daemon_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4701,7 +4802,7 @@ func (x *ListProfilesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListProfilesResponse.ProtoReflect.Descriptor instead. func (*ListProfilesResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{65} + return file_daemon_proto_rawDescGZIP(), []int{66} } func (x *ListProfilesResponse) GetProfiles() []*Profile { @@ -4721,7 +4822,7 @@ type Profile struct { func (x *Profile) Reset() { *x = Profile{} - mi := &file_daemon_proto_msgTypes[66] + mi := &file_daemon_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4733,7 +4834,7 @@ func (x *Profile) String() string { func (*Profile) ProtoMessage() {} func (x *Profile) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[66] + mi := &file_daemon_proto_msgTypes[67] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4746,7 +4847,7 @@ func (x *Profile) ProtoReflect() protoreflect.Message { // Deprecated: Use Profile.ProtoReflect.Descriptor instead. func (*Profile) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{66} + return file_daemon_proto_rawDescGZIP(), []int{67} } func (x *Profile) GetName() string { @@ -4771,7 +4872,7 @@ type GetActiveProfileRequest struct { func (x *GetActiveProfileRequest) Reset() { *x = GetActiveProfileRequest{} - mi := &file_daemon_proto_msgTypes[67] + mi := &file_daemon_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4783,7 +4884,7 @@ func (x *GetActiveProfileRequest) String() string { func (*GetActiveProfileRequest) ProtoMessage() {} func (x *GetActiveProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[67] + mi := &file_daemon_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4796,7 +4897,7 @@ func (x *GetActiveProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetActiveProfileRequest.ProtoReflect.Descriptor instead. func (*GetActiveProfileRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{67} + return file_daemon_proto_rawDescGZIP(), []int{68} } type GetActiveProfileResponse struct { @@ -4809,7 +4910,7 @@ type GetActiveProfileResponse struct { func (x *GetActiveProfileResponse) Reset() { *x = GetActiveProfileResponse{} - mi := &file_daemon_proto_msgTypes[68] + mi := &file_daemon_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4821,7 +4922,7 @@ func (x *GetActiveProfileResponse) String() string { func (*GetActiveProfileResponse) ProtoMessage() {} func (x *GetActiveProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[68] + mi := &file_daemon_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4834,7 +4935,7 @@ func (x *GetActiveProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetActiveProfileResponse.ProtoReflect.Descriptor instead. func (*GetActiveProfileResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{68} + return file_daemon_proto_rawDescGZIP(), []int{69} } func (x *GetActiveProfileResponse) GetProfileName() string { @@ -4861,7 +4962,7 @@ type LogoutRequest struct { func (x *LogoutRequest) Reset() { *x = LogoutRequest{} - mi := &file_daemon_proto_msgTypes[69] + mi := &file_daemon_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4873,7 +4974,7 @@ func (x *LogoutRequest) String() string { func (*LogoutRequest) ProtoMessage() {} func (x *LogoutRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[69] + mi := &file_daemon_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4886,7 +4987,7 @@ func (x *LogoutRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use LogoutRequest.ProtoReflect.Descriptor instead. func (*LogoutRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{69} + return file_daemon_proto_rawDescGZIP(), []int{70} } func (x *LogoutRequest) GetProfileName() string { @@ -4911,7 +5012,7 @@ type LogoutResponse struct { func (x *LogoutResponse) Reset() { *x = LogoutResponse{} - mi := &file_daemon_proto_msgTypes[70] + mi := &file_daemon_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4923,7 +5024,7 @@ func (x *LogoutResponse) String() string { func (*LogoutResponse) ProtoMessage() {} func (x *LogoutResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[70] + mi := &file_daemon_proto_msgTypes[71] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4936,7 +5037,7 @@ func (x *LogoutResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use LogoutResponse.ProtoReflect.Descriptor instead. func (*LogoutResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{70} + return file_daemon_proto_rawDescGZIP(), []int{71} } type GetFeaturesRequest struct { @@ -4947,7 +5048,7 @@ type GetFeaturesRequest struct { func (x *GetFeaturesRequest) Reset() { *x = GetFeaturesRequest{} - mi := &file_daemon_proto_msgTypes[71] + mi := &file_daemon_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4959,7 +5060,7 @@ func (x *GetFeaturesRequest) String() string { func (*GetFeaturesRequest) ProtoMessage() {} func (x *GetFeaturesRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[71] + mi := &file_daemon_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4972,7 +5073,7 @@ func (x *GetFeaturesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetFeaturesRequest.ProtoReflect.Descriptor instead. func (*GetFeaturesRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{71} + return file_daemon_proto_rawDescGZIP(), []int{72} } type GetFeaturesResponse struct { @@ -4986,7 +5087,7 @@ type GetFeaturesResponse struct { func (x *GetFeaturesResponse) Reset() { *x = GetFeaturesResponse{} - mi := &file_daemon_proto_msgTypes[72] + mi := &file_daemon_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4998,7 +5099,7 @@ func (x *GetFeaturesResponse) String() string { func (*GetFeaturesResponse) ProtoMessage() {} func (x *GetFeaturesResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[72] + mi := &file_daemon_proto_msgTypes[73] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5011,7 +5112,7 @@ func (x *GetFeaturesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetFeaturesResponse.ProtoReflect.Descriptor instead. func (*GetFeaturesResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{72} + return file_daemon_proto_rawDescGZIP(), []int{73} } func (x *GetFeaturesResponse) GetDisableProfiles() bool { @@ -5043,7 +5144,7 @@ type TriggerUpdateRequest struct { func (x *TriggerUpdateRequest) Reset() { *x = TriggerUpdateRequest{} - mi := &file_daemon_proto_msgTypes[73] + mi := &file_daemon_proto_msgTypes[74] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5055,7 +5156,7 @@ func (x *TriggerUpdateRequest) String() string { func (*TriggerUpdateRequest) ProtoMessage() {} func (x *TriggerUpdateRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[73] + mi := &file_daemon_proto_msgTypes[74] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5068,7 +5169,7 @@ func (x *TriggerUpdateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TriggerUpdateRequest.ProtoReflect.Descriptor instead. func (*TriggerUpdateRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{73} + return file_daemon_proto_rawDescGZIP(), []int{74} } type TriggerUpdateResponse struct { @@ -5081,7 +5182,7 @@ type TriggerUpdateResponse struct { func (x *TriggerUpdateResponse) Reset() { *x = TriggerUpdateResponse{} - mi := &file_daemon_proto_msgTypes[74] + mi := &file_daemon_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5093,7 +5194,7 @@ func (x *TriggerUpdateResponse) String() string { func (*TriggerUpdateResponse) ProtoMessage() {} func (x *TriggerUpdateResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[74] + mi := &file_daemon_proto_msgTypes[75] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5106,7 +5207,7 @@ func (x *TriggerUpdateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use TriggerUpdateResponse.ProtoReflect.Descriptor instead. func (*TriggerUpdateResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{74} + return file_daemon_proto_rawDescGZIP(), []int{75} } func (x *TriggerUpdateResponse) GetSuccess() bool { @@ -5134,7 +5235,7 @@ type GetPeerSSHHostKeyRequest struct { func (x *GetPeerSSHHostKeyRequest) Reset() { *x = GetPeerSSHHostKeyRequest{} - mi := &file_daemon_proto_msgTypes[75] + mi := &file_daemon_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5146,7 +5247,7 @@ func (x *GetPeerSSHHostKeyRequest) String() string { func (*GetPeerSSHHostKeyRequest) ProtoMessage() {} func (x *GetPeerSSHHostKeyRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[75] + mi := &file_daemon_proto_msgTypes[76] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5159,7 +5260,7 @@ func (x *GetPeerSSHHostKeyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetPeerSSHHostKeyRequest.ProtoReflect.Descriptor instead. func (*GetPeerSSHHostKeyRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{75} + return file_daemon_proto_rawDescGZIP(), []int{76} } func (x *GetPeerSSHHostKeyRequest) GetPeerAddress() string { @@ -5186,7 +5287,7 @@ type GetPeerSSHHostKeyResponse struct { func (x *GetPeerSSHHostKeyResponse) Reset() { *x = GetPeerSSHHostKeyResponse{} - mi := &file_daemon_proto_msgTypes[76] + mi := &file_daemon_proto_msgTypes[77] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5198,7 +5299,7 @@ func (x *GetPeerSSHHostKeyResponse) String() string { func (*GetPeerSSHHostKeyResponse) ProtoMessage() {} func (x *GetPeerSSHHostKeyResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[76] + mi := &file_daemon_proto_msgTypes[77] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5211,7 +5312,7 @@ func (x *GetPeerSSHHostKeyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetPeerSSHHostKeyResponse.ProtoReflect.Descriptor instead. func (*GetPeerSSHHostKeyResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{76} + return file_daemon_proto_rawDescGZIP(), []int{77} } func (x *GetPeerSSHHostKeyResponse) GetSshHostKey() []byte { @@ -5253,7 +5354,7 @@ type RequestJWTAuthRequest struct { func (x *RequestJWTAuthRequest) Reset() { *x = RequestJWTAuthRequest{} - mi := &file_daemon_proto_msgTypes[77] + mi := &file_daemon_proto_msgTypes[78] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5265,7 +5366,7 @@ func (x *RequestJWTAuthRequest) String() string { func (*RequestJWTAuthRequest) ProtoMessage() {} func (x *RequestJWTAuthRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[77] + mi := &file_daemon_proto_msgTypes[78] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5278,7 +5379,7 @@ func (x *RequestJWTAuthRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RequestJWTAuthRequest.ProtoReflect.Descriptor instead. func (*RequestJWTAuthRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{77} + return file_daemon_proto_rawDescGZIP(), []int{78} } func (x *RequestJWTAuthRequest) GetHint() string { @@ -5311,7 +5412,7 @@ type RequestJWTAuthResponse struct { func (x *RequestJWTAuthResponse) Reset() { *x = RequestJWTAuthResponse{} - mi := &file_daemon_proto_msgTypes[78] + mi := &file_daemon_proto_msgTypes[79] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5323,7 +5424,7 @@ func (x *RequestJWTAuthResponse) String() string { func (*RequestJWTAuthResponse) ProtoMessage() {} func (x *RequestJWTAuthResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[78] + mi := &file_daemon_proto_msgTypes[79] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5336,7 +5437,7 @@ func (x *RequestJWTAuthResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RequestJWTAuthResponse.ProtoReflect.Descriptor instead. func (*RequestJWTAuthResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{78} + return file_daemon_proto_rawDescGZIP(), []int{79} } func (x *RequestJWTAuthResponse) GetVerificationURI() string { @@ -5401,7 +5502,7 @@ type WaitJWTTokenRequest struct { func (x *WaitJWTTokenRequest) Reset() { *x = WaitJWTTokenRequest{} - mi := &file_daemon_proto_msgTypes[79] + mi := &file_daemon_proto_msgTypes[80] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5413,7 +5514,7 @@ func (x *WaitJWTTokenRequest) String() string { func (*WaitJWTTokenRequest) ProtoMessage() {} func (x *WaitJWTTokenRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[79] + mi := &file_daemon_proto_msgTypes[80] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5426,7 +5527,7 @@ func (x *WaitJWTTokenRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use WaitJWTTokenRequest.ProtoReflect.Descriptor instead. func (*WaitJWTTokenRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{79} + return file_daemon_proto_rawDescGZIP(), []int{80} } func (x *WaitJWTTokenRequest) GetDeviceCode() string { @@ -5458,7 +5559,7 @@ type WaitJWTTokenResponse struct { func (x *WaitJWTTokenResponse) Reset() { *x = WaitJWTTokenResponse{} - mi := &file_daemon_proto_msgTypes[80] + mi := &file_daemon_proto_msgTypes[81] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5470,7 +5571,7 @@ func (x *WaitJWTTokenResponse) String() string { func (*WaitJWTTokenResponse) ProtoMessage() {} func (x *WaitJWTTokenResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[80] + mi := &file_daemon_proto_msgTypes[81] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5483,7 +5584,7 @@ func (x *WaitJWTTokenResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use WaitJWTTokenResponse.ProtoReflect.Descriptor instead. func (*WaitJWTTokenResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{80} + return file_daemon_proto_rawDescGZIP(), []int{81} } func (x *WaitJWTTokenResponse) GetToken() string { @@ -5516,7 +5617,7 @@ type StartCPUProfileRequest struct { func (x *StartCPUProfileRequest) Reset() { *x = StartCPUProfileRequest{} - mi := &file_daemon_proto_msgTypes[81] + mi := &file_daemon_proto_msgTypes[82] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5528,7 +5629,7 @@ func (x *StartCPUProfileRequest) String() string { func (*StartCPUProfileRequest) ProtoMessage() {} func (x *StartCPUProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[81] + mi := &file_daemon_proto_msgTypes[82] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5541,7 +5642,7 @@ func (x *StartCPUProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StartCPUProfileRequest.ProtoReflect.Descriptor instead. func (*StartCPUProfileRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{81} + return file_daemon_proto_rawDescGZIP(), []int{82} } // StartCPUProfileResponse confirms CPU profiling has started @@ -5553,7 +5654,7 @@ type StartCPUProfileResponse struct { func (x *StartCPUProfileResponse) Reset() { *x = StartCPUProfileResponse{} - mi := &file_daemon_proto_msgTypes[82] + mi := &file_daemon_proto_msgTypes[83] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5565,7 +5666,7 @@ func (x *StartCPUProfileResponse) String() string { func (*StartCPUProfileResponse) ProtoMessage() {} func (x *StartCPUProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[82] + mi := &file_daemon_proto_msgTypes[83] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5578,7 +5679,7 @@ func (x *StartCPUProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StartCPUProfileResponse.ProtoReflect.Descriptor instead. func (*StartCPUProfileResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{82} + return file_daemon_proto_rawDescGZIP(), []int{83} } // StopCPUProfileRequest for stopping CPU profiling @@ -5590,7 +5691,7 @@ type StopCPUProfileRequest struct { func (x *StopCPUProfileRequest) Reset() { *x = StopCPUProfileRequest{} - mi := &file_daemon_proto_msgTypes[83] + mi := &file_daemon_proto_msgTypes[84] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5602,7 +5703,7 @@ func (x *StopCPUProfileRequest) String() string { func (*StopCPUProfileRequest) ProtoMessage() {} func (x *StopCPUProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[83] + mi := &file_daemon_proto_msgTypes[84] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5615,7 +5716,7 @@ func (x *StopCPUProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StopCPUProfileRequest.ProtoReflect.Descriptor instead. func (*StopCPUProfileRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{83} + return file_daemon_proto_rawDescGZIP(), []int{84} } // StopCPUProfileResponse confirms CPU profiling has stopped @@ -5627,7 +5728,7 @@ type StopCPUProfileResponse struct { func (x *StopCPUProfileResponse) Reset() { *x = StopCPUProfileResponse{} - mi := &file_daemon_proto_msgTypes[84] + mi := &file_daemon_proto_msgTypes[85] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5639,7 +5740,7 @@ func (x *StopCPUProfileResponse) String() string { func (*StopCPUProfileResponse) ProtoMessage() {} func (x *StopCPUProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[84] + mi := &file_daemon_proto_msgTypes[85] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5652,7 +5753,7 @@ func (x *StopCPUProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StopCPUProfileResponse.ProtoReflect.Descriptor instead. func (*StopCPUProfileResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{84} + return file_daemon_proto_rawDescGZIP(), []int{85} } type InstallerResultRequest struct { @@ -5663,7 +5764,7 @@ type InstallerResultRequest struct { func (x *InstallerResultRequest) Reset() { *x = InstallerResultRequest{} - mi := &file_daemon_proto_msgTypes[85] + mi := &file_daemon_proto_msgTypes[86] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5675,7 +5776,7 @@ func (x *InstallerResultRequest) String() string { func (*InstallerResultRequest) ProtoMessage() {} func (x *InstallerResultRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[85] + mi := &file_daemon_proto_msgTypes[86] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5688,7 +5789,7 @@ func (x *InstallerResultRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use InstallerResultRequest.ProtoReflect.Descriptor instead. func (*InstallerResultRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{85} + return file_daemon_proto_rawDescGZIP(), []int{86} } type InstallerResultResponse struct { @@ -5701,7 +5802,7 @@ type InstallerResultResponse struct { func (x *InstallerResultResponse) Reset() { *x = InstallerResultResponse{} - mi := &file_daemon_proto_msgTypes[86] + mi := &file_daemon_proto_msgTypes[87] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5713,7 +5814,7 @@ func (x *InstallerResultResponse) String() string { func (*InstallerResultResponse) ProtoMessage() {} func (x *InstallerResultResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[86] + mi := &file_daemon_proto_msgTypes[87] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5726,7 +5827,7 @@ func (x *InstallerResultResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use InstallerResultResponse.ProtoReflect.Descriptor instead. func (*InstallerResultResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{86} + return file_daemon_proto_rawDescGZIP(), []int{87} } func (x *InstallerResultResponse) GetSuccess() bool { @@ -5759,7 +5860,7 @@ type ExposeServiceRequest struct { func (x *ExposeServiceRequest) Reset() { *x = ExposeServiceRequest{} - mi := &file_daemon_proto_msgTypes[87] + mi := &file_daemon_proto_msgTypes[88] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5771,7 +5872,7 @@ func (x *ExposeServiceRequest) String() string { func (*ExposeServiceRequest) ProtoMessage() {} func (x *ExposeServiceRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[87] + mi := &file_daemon_proto_msgTypes[88] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5784,7 +5885,7 @@ func (x *ExposeServiceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ExposeServiceRequest.ProtoReflect.Descriptor instead. func (*ExposeServiceRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{87} + return file_daemon_proto_rawDescGZIP(), []int{88} } func (x *ExposeServiceRequest) GetPort() uint32 { @@ -5855,7 +5956,7 @@ type ExposeServiceEvent struct { func (x *ExposeServiceEvent) Reset() { *x = ExposeServiceEvent{} - mi := &file_daemon_proto_msgTypes[88] + mi := &file_daemon_proto_msgTypes[89] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5867,7 +5968,7 @@ func (x *ExposeServiceEvent) String() string { func (*ExposeServiceEvent) ProtoMessage() {} func (x *ExposeServiceEvent) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[88] + mi := &file_daemon_proto_msgTypes[89] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5880,7 +5981,7 @@ func (x *ExposeServiceEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use ExposeServiceEvent.ProtoReflect.Descriptor instead. func (*ExposeServiceEvent) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{88} + return file_daemon_proto_rawDescGZIP(), []int{89} } func (x *ExposeServiceEvent) GetEvent() isExposeServiceEvent_Event { @@ -5921,7 +6022,7 @@ type ExposeServiceReady struct { func (x *ExposeServiceReady) Reset() { *x = ExposeServiceReady{} - mi := &file_daemon_proto_msgTypes[89] + mi := &file_daemon_proto_msgTypes[90] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5933,7 +6034,7 @@ func (x *ExposeServiceReady) String() string { func (*ExposeServiceReady) ProtoMessage() {} func (x *ExposeServiceReady) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[89] + mi := &file_daemon_proto_msgTypes[90] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5946,7 +6047,7 @@ func (x *ExposeServiceReady) ProtoReflect() protoreflect.Message { // Deprecated: Use ExposeServiceReady.ProtoReflect.Descriptor instead. func (*ExposeServiceReady) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{89} + return file_daemon_proto_rawDescGZIP(), []int{90} } func (x *ExposeServiceReady) GetServiceName() string { @@ -5987,7 +6088,7 @@ type PortInfo_Range struct { func (x *PortInfo_Range) Reset() { *x = PortInfo_Range{} - mi := &file_daemon_proto_msgTypes[91] + mi := &file_daemon_proto_msgTypes[92] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5999,7 +6100,7 @@ func (x *PortInfo_Range) String() string { func (*PortInfo_Range) ProtoMessage() {} func (x *PortInfo_Range) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[91] + mi := &file_daemon_proto_msgTypes[92] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6012,7 +6113,7 @@ func (x *PortInfo_Range) ProtoReflect() protoreflect.Message { // Deprecated: Use PortInfo_Range.ProtoReflect.Descriptor instead. func (*PortInfo_Range) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{30, 0} + return file_daemon_proto_rawDescGZIP(), []int{31, 0} } func (x *PortInfo_Range) GetStart() uint32 { @@ -6042,7 +6143,7 @@ const file_daemon_proto_rawDesc = "" + "\x05SLEEP\x10\x01\x12\n" + "\n" + "\x06WAKEUP\x10\x02\"\x15\n" + - "\x13OSLifecycleResponse\"\xb6\x12\n" + + "\x13OSLifecycleResponse\"\xbc\x13\n" + "\fLoginRequest\x12\x1a\n" + "\bsetupKey\x18\x01 \x01(\tR\bsetupKey\x12&\n" + "\fpreSharedKey\x18\x02 \x01(\tB\x02\x18\x01R\fpreSharedKey\x12$\n" + @@ -6086,7 +6187,9 @@ const file_daemon_proto_rawDesc = "" + "\x1cenableSSHLocalPortForwarding\x18$ \x01(\bH\x17R\x1cenableSSHLocalPortForwarding\x88\x01\x01\x12I\n" + "\x1denableSSHRemotePortForwarding\x18% \x01(\bH\x18R\x1denableSSHRemotePortForwarding\x88\x01\x01\x12+\n" + "\x0edisableSSHAuth\x18& \x01(\bH\x19R\x0edisableSSHAuth\x88\x01\x01\x12+\n" + - "\x0esshJWTCacheTTL\x18' \x01(\x05H\x1aR\x0esshJWTCacheTTL\x88\x01\x01B\x13\n" + + "\x0esshJWTCacheTTL\x18' \x01(\x05H\x1aR\x0esshJWTCacheTTL\x88\x01\x01\x12/\n" + + "\x10serverVNCAllowed\x18) \x01(\bH\x1bR\x10serverVNCAllowed\x88\x01\x01\x12+\n" + + "\x0edisableVNCAuth\x18* \x01(\bH\x1cR\x0edisableVNCAuth\x88\x01\x01B\x13\n" + "\x11_rosenpassEnabledB\x10\n" + "\x0e_interfaceNameB\x10\n" + "\x0e_wireguardPortB\x17\n" + @@ -6113,7 +6216,9 @@ const file_daemon_proto_rawDesc = "" + "\x1d_enableSSHLocalPortForwardingB \n" + "\x1e_enableSSHRemotePortForwardingB\x11\n" + "\x0f_disableSSHAuthB\x11\n" + - "\x0f_sshJWTCacheTTL\"\xb5\x01\n" + + "\x0f_sshJWTCacheTTLB\x13\n" + + "\x11_serverVNCAllowedB\x11\n" + + "\x0f_disableVNCAuth\"\xb5\x01\n" + "\rLoginResponse\x12$\n" + "\rneedsSSOLogin\x18\x01 \x01(\bR\rneedsSSOLogin\x12\x1a\n" + "\buserCode\x18\x02 \x01(\tR\buserCode\x12(\n" + @@ -6146,7 +6251,7 @@ const file_daemon_proto_rawDesc = "" + "\fDownResponse\"P\n" + "\x10GetConfigRequest\x12 \n" + "\vprofileName\x18\x01 \x01(\tR\vprofileName\x12\x1a\n" + - "\busername\x18\x02 \x01(\tR\busername\"\xdb\b\n" + + "\busername\x18\x02 \x01(\tR\busername\"\xaf\t\n" + "\x11GetConfigResponse\x12$\n" + "\rmanagementUrl\x18\x01 \x01(\tR\rmanagementUrl\x12\x1e\n" + "\n" + @@ -6177,7 +6282,9 @@ const file_daemon_proto_rawDesc = "" + "\x1cenableSSHLocalPortForwarding\x18\x16 \x01(\bR\x1cenableSSHLocalPortForwarding\x12D\n" + "\x1denableSSHRemotePortForwarding\x18\x17 \x01(\bR\x1denableSSHRemotePortForwarding\x12&\n" + "\x0edisableSSHAuth\x18\x19 \x01(\bR\x0edisableSSHAuth\x12&\n" + - "\x0esshJWTCacheTTL\x18\x1a \x01(\x05R\x0esshJWTCacheTTL\"\xfe\x05\n" + + "\x0esshJWTCacheTTL\x18\x1a \x01(\x05R\x0esshJWTCacheTTL\x12*\n" + + "\x10serverVNCAllowed\x18\x1c \x01(\bR\x10serverVNCAllowed\x12&\n" + + "\x0edisableVNCAuth\x18\x1d \x01(\bR\x0edisableVNCAuth\"\xfe\x05\n" + "\tPeerState\x12\x0e\n" + "\x02IP\x18\x01 \x01(\tR\x02IP\x12\x16\n" + "\x06pubKey\x18\x02 \x01(\tR\x06pubKey\x12\x1e\n" + @@ -6236,7 +6343,9 @@ const file_daemon_proto_rawDesc = "" + "\fportForwards\x18\x05 \x03(\tR\fportForwards\"^\n" + "\x0eSSHServerState\x12\x18\n" + "\aenabled\x18\x01 \x01(\bR\aenabled\x122\n" + - "\bsessions\x18\x02 \x03(\v2\x16.daemon.SSHSessionInfoR\bsessions\"\xaf\x04\n" + + "\bsessions\x18\x02 \x03(\v2\x16.daemon.SSHSessionInfoR\bsessions\"*\n" + + "\x0eVNCServerState\x12\x18\n" + + "\aenabled\x18\x01 \x01(\bR\aenabled\"\xef\x04\n" + "\n" + "FullStatus\x12A\n" + "\x0fmanagementState\x18\x01 \x01(\v2\x17.daemon.ManagementStateR\x0fmanagementState\x125\n" + @@ -6250,7 +6359,8 @@ const file_daemon_proto_rawDesc = "" + "\x06events\x18\a \x03(\v2\x13.daemon.SystemEventR\x06events\x124\n" + "\x15lazyConnectionEnabled\x18\t \x01(\bR\x15lazyConnectionEnabled\x12>\n" + "\x0esshServerState\x18\n" + - " \x01(\v2\x16.daemon.SSHServerStateR\x0esshServerState\"\x15\n" + + " \x01(\v2\x16.daemon.SSHServerStateR\x0esshServerState\x12>\n" + + "\x0evncServerState\x18\v \x01(\v2\x16.daemon.VNCServerStateR\x0evncServerState\"\x15\n" + "\x13ListNetworksRequest\"?\n" + "\x14ListNetworksResponse\x12'\n" + "\x06routes\x18\x01 \x03(\v2\x0f.daemon.NetworkR\x06routes\"a\n" + @@ -6390,7 +6500,7 @@ const file_daemon_proto_rawDesc = "" + "\busername\x18\x02 \x01(\tH\x01R\busername\x88\x01\x01B\x0e\n" + "\f_profileNameB\v\n" + "\t_username\"\x17\n" + - "\x15SwitchProfileResponse\"\xdf\x10\n" + + "\x15SwitchProfileResponse\"\xe5\x11\n" + "\x10SetConfigRequest\x12\x1a\n" + "\busername\x18\x01 \x01(\tR\busername\x12 \n" + "\vprofileName\x18\x02 \x01(\tR\vprofileName\x12$\n" + @@ -6429,7 +6539,9 @@ const file_daemon_proto_rawDesc = "" + "\x1cenableSSHLocalPortForwarding\x18\x1f \x01(\bH\x14R\x1cenableSSHLocalPortForwarding\x88\x01\x01\x12I\n" + "\x1denableSSHRemotePortForwarding\x18 \x01(\bH\x15R\x1denableSSHRemotePortForwarding\x88\x01\x01\x12+\n" + "\x0edisableSSHAuth\x18! \x01(\bH\x16R\x0edisableSSHAuth\x88\x01\x01\x12+\n" + - "\x0esshJWTCacheTTL\x18\" \x01(\x05H\x17R\x0esshJWTCacheTTL\x88\x01\x01B\x13\n" + + "\x0esshJWTCacheTTL\x18\" \x01(\x05H\x17R\x0esshJWTCacheTTL\x88\x01\x01\x12/\n" + + "\x10serverVNCAllowed\x18$ \x01(\bH\x18R\x10serverVNCAllowed\x88\x01\x01\x12+\n" + + "\x0edisableVNCAuth\x18% \x01(\bH\x19R\x0edisableVNCAuth\x88\x01\x01B\x13\n" + "\x11_rosenpassEnabledB\x10\n" + "\x0e_interfaceNameB\x10\n" + "\x0e_wireguardPortB\x17\n" + @@ -6453,7 +6565,9 @@ const file_daemon_proto_rawDesc = "" + "\x1d_enableSSHLocalPortForwardingB \n" + "\x1e_enableSSHRemotePortForwardingB\x11\n" + "\x0f_disableSSHAuthB\x11\n" + - "\x0f_sshJWTCacheTTL\"\x13\n" + + "\x0f_sshJWTCacheTTLB\x13\n" + + "\x11_serverVNCAllowedB\x11\n" + + "\x0f_disableVNCAuth\"\x13\n" + "\x11SetConfigResponse\"Q\n" + "\x11AddProfileRequest\x12\x1a\n" + "\busername\x18\x01 \x01(\tR\busername\x12 \n" + @@ -6622,7 +6736,7 @@ func file_daemon_proto_rawDescGZIP() []byte { } var file_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 5) -var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 93) +var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 94) var file_daemon_proto_goTypes = []any{ (LogLevel)(0), // 0: daemon.LogLevel (ExposeProtocol)(0), // 1: daemon.ExposeProtocol @@ -6652,195 +6766,197 @@ var file_daemon_proto_goTypes = []any{ (*NSGroupState)(nil), // 25: daemon.NSGroupState (*SSHSessionInfo)(nil), // 26: daemon.SSHSessionInfo (*SSHServerState)(nil), // 27: daemon.SSHServerState - (*FullStatus)(nil), // 28: daemon.FullStatus - (*ListNetworksRequest)(nil), // 29: daemon.ListNetworksRequest - (*ListNetworksResponse)(nil), // 30: daemon.ListNetworksResponse - (*SelectNetworksRequest)(nil), // 31: daemon.SelectNetworksRequest - (*SelectNetworksResponse)(nil), // 32: daemon.SelectNetworksResponse - (*IPList)(nil), // 33: daemon.IPList - (*Network)(nil), // 34: daemon.Network - (*PortInfo)(nil), // 35: daemon.PortInfo - (*ForwardingRule)(nil), // 36: daemon.ForwardingRule - (*ForwardingRulesResponse)(nil), // 37: daemon.ForwardingRulesResponse - (*DebugBundleRequest)(nil), // 38: daemon.DebugBundleRequest - (*DebugBundleResponse)(nil), // 39: daemon.DebugBundleResponse - (*GetLogLevelRequest)(nil), // 40: daemon.GetLogLevelRequest - (*GetLogLevelResponse)(nil), // 41: daemon.GetLogLevelResponse - (*SetLogLevelRequest)(nil), // 42: daemon.SetLogLevelRequest - (*SetLogLevelResponse)(nil), // 43: daemon.SetLogLevelResponse - (*State)(nil), // 44: daemon.State - (*ListStatesRequest)(nil), // 45: daemon.ListStatesRequest - (*ListStatesResponse)(nil), // 46: daemon.ListStatesResponse - (*CleanStateRequest)(nil), // 47: daemon.CleanStateRequest - (*CleanStateResponse)(nil), // 48: daemon.CleanStateResponse - (*DeleteStateRequest)(nil), // 49: daemon.DeleteStateRequest - (*DeleteStateResponse)(nil), // 50: daemon.DeleteStateResponse - (*SetSyncResponsePersistenceRequest)(nil), // 51: daemon.SetSyncResponsePersistenceRequest - (*SetSyncResponsePersistenceResponse)(nil), // 52: daemon.SetSyncResponsePersistenceResponse - (*TCPFlags)(nil), // 53: daemon.TCPFlags - (*TracePacketRequest)(nil), // 54: daemon.TracePacketRequest - (*TraceStage)(nil), // 55: daemon.TraceStage - (*TracePacketResponse)(nil), // 56: daemon.TracePacketResponse - (*SubscribeRequest)(nil), // 57: daemon.SubscribeRequest - (*SystemEvent)(nil), // 58: daemon.SystemEvent - (*GetEventsRequest)(nil), // 59: daemon.GetEventsRequest - (*GetEventsResponse)(nil), // 60: daemon.GetEventsResponse - (*SwitchProfileRequest)(nil), // 61: daemon.SwitchProfileRequest - (*SwitchProfileResponse)(nil), // 62: daemon.SwitchProfileResponse - (*SetConfigRequest)(nil), // 63: daemon.SetConfigRequest - (*SetConfigResponse)(nil), // 64: daemon.SetConfigResponse - (*AddProfileRequest)(nil), // 65: daemon.AddProfileRequest - (*AddProfileResponse)(nil), // 66: daemon.AddProfileResponse - (*RemoveProfileRequest)(nil), // 67: daemon.RemoveProfileRequest - (*RemoveProfileResponse)(nil), // 68: daemon.RemoveProfileResponse - (*ListProfilesRequest)(nil), // 69: daemon.ListProfilesRequest - (*ListProfilesResponse)(nil), // 70: daemon.ListProfilesResponse - (*Profile)(nil), // 71: daemon.Profile - (*GetActiveProfileRequest)(nil), // 72: daemon.GetActiveProfileRequest - (*GetActiveProfileResponse)(nil), // 73: daemon.GetActiveProfileResponse - (*LogoutRequest)(nil), // 74: daemon.LogoutRequest - (*LogoutResponse)(nil), // 75: daemon.LogoutResponse - (*GetFeaturesRequest)(nil), // 76: daemon.GetFeaturesRequest - (*GetFeaturesResponse)(nil), // 77: daemon.GetFeaturesResponse - (*TriggerUpdateRequest)(nil), // 78: daemon.TriggerUpdateRequest - (*TriggerUpdateResponse)(nil), // 79: daemon.TriggerUpdateResponse - (*GetPeerSSHHostKeyRequest)(nil), // 80: daemon.GetPeerSSHHostKeyRequest - (*GetPeerSSHHostKeyResponse)(nil), // 81: daemon.GetPeerSSHHostKeyResponse - (*RequestJWTAuthRequest)(nil), // 82: daemon.RequestJWTAuthRequest - (*RequestJWTAuthResponse)(nil), // 83: daemon.RequestJWTAuthResponse - (*WaitJWTTokenRequest)(nil), // 84: daemon.WaitJWTTokenRequest - (*WaitJWTTokenResponse)(nil), // 85: daemon.WaitJWTTokenResponse - (*StartCPUProfileRequest)(nil), // 86: daemon.StartCPUProfileRequest - (*StartCPUProfileResponse)(nil), // 87: daemon.StartCPUProfileResponse - (*StopCPUProfileRequest)(nil), // 88: daemon.StopCPUProfileRequest - (*StopCPUProfileResponse)(nil), // 89: daemon.StopCPUProfileResponse - (*InstallerResultRequest)(nil), // 90: daemon.InstallerResultRequest - (*InstallerResultResponse)(nil), // 91: daemon.InstallerResultResponse - (*ExposeServiceRequest)(nil), // 92: daemon.ExposeServiceRequest - (*ExposeServiceEvent)(nil), // 93: daemon.ExposeServiceEvent - (*ExposeServiceReady)(nil), // 94: daemon.ExposeServiceReady - nil, // 95: daemon.Network.ResolvedIPsEntry - (*PortInfo_Range)(nil), // 96: daemon.PortInfo.Range - nil, // 97: daemon.SystemEvent.MetadataEntry - (*durationpb.Duration)(nil), // 98: google.protobuf.Duration - (*timestamppb.Timestamp)(nil), // 99: google.protobuf.Timestamp + (*VNCServerState)(nil), // 28: daemon.VNCServerState + (*FullStatus)(nil), // 29: daemon.FullStatus + (*ListNetworksRequest)(nil), // 30: daemon.ListNetworksRequest + (*ListNetworksResponse)(nil), // 31: daemon.ListNetworksResponse + (*SelectNetworksRequest)(nil), // 32: daemon.SelectNetworksRequest + (*SelectNetworksResponse)(nil), // 33: daemon.SelectNetworksResponse + (*IPList)(nil), // 34: daemon.IPList + (*Network)(nil), // 35: daemon.Network + (*PortInfo)(nil), // 36: daemon.PortInfo + (*ForwardingRule)(nil), // 37: daemon.ForwardingRule + (*ForwardingRulesResponse)(nil), // 38: daemon.ForwardingRulesResponse + (*DebugBundleRequest)(nil), // 39: daemon.DebugBundleRequest + (*DebugBundleResponse)(nil), // 40: daemon.DebugBundleResponse + (*GetLogLevelRequest)(nil), // 41: daemon.GetLogLevelRequest + (*GetLogLevelResponse)(nil), // 42: daemon.GetLogLevelResponse + (*SetLogLevelRequest)(nil), // 43: daemon.SetLogLevelRequest + (*SetLogLevelResponse)(nil), // 44: daemon.SetLogLevelResponse + (*State)(nil), // 45: daemon.State + (*ListStatesRequest)(nil), // 46: daemon.ListStatesRequest + (*ListStatesResponse)(nil), // 47: daemon.ListStatesResponse + (*CleanStateRequest)(nil), // 48: daemon.CleanStateRequest + (*CleanStateResponse)(nil), // 49: daemon.CleanStateResponse + (*DeleteStateRequest)(nil), // 50: daemon.DeleteStateRequest + (*DeleteStateResponse)(nil), // 51: daemon.DeleteStateResponse + (*SetSyncResponsePersistenceRequest)(nil), // 52: daemon.SetSyncResponsePersistenceRequest + (*SetSyncResponsePersistenceResponse)(nil), // 53: daemon.SetSyncResponsePersistenceResponse + (*TCPFlags)(nil), // 54: daemon.TCPFlags + (*TracePacketRequest)(nil), // 55: daemon.TracePacketRequest + (*TraceStage)(nil), // 56: daemon.TraceStage + (*TracePacketResponse)(nil), // 57: daemon.TracePacketResponse + (*SubscribeRequest)(nil), // 58: daemon.SubscribeRequest + (*SystemEvent)(nil), // 59: daemon.SystemEvent + (*GetEventsRequest)(nil), // 60: daemon.GetEventsRequest + (*GetEventsResponse)(nil), // 61: daemon.GetEventsResponse + (*SwitchProfileRequest)(nil), // 62: daemon.SwitchProfileRequest + (*SwitchProfileResponse)(nil), // 63: daemon.SwitchProfileResponse + (*SetConfigRequest)(nil), // 64: daemon.SetConfigRequest + (*SetConfigResponse)(nil), // 65: daemon.SetConfigResponse + (*AddProfileRequest)(nil), // 66: daemon.AddProfileRequest + (*AddProfileResponse)(nil), // 67: daemon.AddProfileResponse + (*RemoveProfileRequest)(nil), // 68: daemon.RemoveProfileRequest + (*RemoveProfileResponse)(nil), // 69: daemon.RemoveProfileResponse + (*ListProfilesRequest)(nil), // 70: daemon.ListProfilesRequest + (*ListProfilesResponse)(nil), // 71: daemon.ListProfilesResponse + (*Profile)(nil), // 72: daemon.Profile + (*GetActiveProfileRequest)(nil), // 73: daemon.GetActiveProfileRequest + (*GetActiveProfileResponse)(nil), // 74: daemon.GetActiveProfileResponse + (*LogoutRequest)(nil), // 75: daemon.LogoutRequest + (*LogoutResponse)(nil), // 76: daemon.LogoutResponse + (*GetFeaturesRequest)(nil), // 77: daemon.GetFeaturesRequest + (*GetFeaturesResponse)(nil), // 78: daemon.GetFeaturesResponse + (*TriggerUpdateRequest)(nil), // 79: daemon.TriggerUpdateRequest + (*TriggerUpdateResponse)(nil), // 80: daemon.TriggerUpdateResponse + (*GetPeerSSHHostKeyRequest)(nil), // 81: daemon.GetPeerSSHHostKeyRequest + (*GetPeerSSHHostKeyResponse)(nil), // 82: daemon.GetPeerSSHHostKeyResponse + (*RequestJWTAuthRequest)(nil), // 83: daemon.RequestJWTAuthRequest + (*RequestJWTAuthResponse)(nil), // 84: daemon.RequestJWTAuthResponse + (*WaitJWTTokenRequest)(nil), // 85: daemon.WaitJWTTokenRequest + (*WaitJWTTokenResponse)(nil), // 86: daemon.WaitJWTTokenResponse + (*StartCPUProfileRequest)(nil), // 87: daemon.StartCPUProfileRequest + (*StartCPUProfileResponse)(nil), // 88: daemon.StartCPUProfileResponse + (*StopCPUProfileRequest)(nil), // 89: daemon.StopCPUProfileRequest + (*StopCPUProfileResponse)(nil), // 90: daemon.StopCPUProfileResponse + (*InstallerResultRequest)(nil), // 91: daemon.InstallerResultRequest + (*InstallerResultResponse)(nil), // 92: daemon.InstallerResultResponse + (*ExposeServiceRequest)(nil), // 93: daemon.ExposeServiceRequest + (*ExposeServiceEvent)(nil), // 94: daemon.ExposeServiceEvent + (*ExposeServiceReady)(nil), // 95: daemon.ExposeServiceReady + nil, // 96: daemon.Network.ResolvedIPsEntry + (*PortInfo_Range)(nil), // 97: daemon.PortInfo.Range + nil, // 98: daemon.SystemEvent.MetadataEntry + (*durationpb.Duration)(nil), // 99: google.protobuf.Duration + (*timestamppb.Timestamp)(nil), // 100: google.protobuf.Timestamp } var file_daemon_proto_depIdxs = []int32{ - 2, // 0: daemon.OSLifecycleRequest.type:type_name -> daemon.OSLifecycleRequest.CycleType - 98, // 1: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration - 28, // 2: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus - 99, // 3: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp - 99, // 4: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp - 98, // 5: daemon.PeerState.latency:type_name -> google.protobuf.Duration - 26, // 6: daemon.SSHServerState.sessions:type_name -> daemon.SSHSessionInfo - 23, // 7: daemon.FullStatus.managementState:type_name -> daemon.ManagementState - 22, // 8: daemon.FullStatus.signalState:type_name -> daemon.SignalState - 21, // 9: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState - 20, // 10: daemon.FullStatus.peers:type_name -> daemon.PeerState - 24, // 11: daemon.FullStatus.relays:type_name -> daemon.RelayState - 25, // 12: daemon.FullStatus.dns_servers:type_name -> daemon.NSGroupState - 58, // 13: daemon.FullStatus.events:type_name -> daemon.SystemEvent - 27, // 14: daemon.FullStatus.sshServerState:type_name -> daemon.SSHServerState - 34, // 15: daemon.ListNetworksResponse.routes:type_name -> daemon.Network - 95, // 16: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry - 96, // 17: daemon.PortInfo.range:type_name -> daemon.PortInfo.Range - 35, // 18: daemon.ForwardingRule.destinationPort:type_name -> daemon.PortInfo - 35, // 19: daemon.ForwardingRule.translatedPort:type_name -> daemon.PortInfo - 36, // 20: daemon.ForwardingRulesResponse.rules:type_name -> daemon.ForwardingRule - 0, // 21: daemon.GetLogLevelResponse.level:type_name -> daemon.LogLevel - 0, // 22: daemon.SetLogLevelRequest.level:type_name -> daemon.LogLevel - 44, // 23: daemon.ListStatesResponse.states:type_name -> daemon.State - 53, // 24: daemon.TracePacketRequest.tcp_flags:type_name -> daemon.TCPFlags - 55, // 25: daemon.TracePacketResponse.stages:type_name -> daemon.TraceStage - 3, // 26: daemon.SystemEvent.severity:type_name -> daemon.SystemEvent.Severity - 4, // 27: daemon.SystemEvent.category:type_name -> daemon.SystemEvent.Category - 99, // 28: daemon.SystemEvent.timestamp:type_name -> google.protobuf.Timestamp - 97, // 29: daemon.SystemEvent.metadata:type_name -> daemon.SystemEvent.MetadataEntry - 58, // 30: daemon.GetEventsResponse.events:type_name -> daemon.SystemEvent - 98, // 31: daemon.SetConfigRequest.dnsRouteInterval:type_name -> google.protobuf.Duration - 71, // 32: daemon.ListProfilesResponse.profiles:type_name -> daemon.Profile - 1, // 33: daemon.ExposeServiceRequest.protocol:type_name -> daemon.ExposeProtocol - 94, // 34: daemon.ExposeServiceEvent.ready:type_name -> daemon.ExposeServiceReady - 33, // 35: daemon.Network.ResolvedIPsEntry.value:type_name -> daemon.IPList - 8, // 36: daemon.DaemonService.Login:input_type -> daemon.LoginRequest - 10, // 37: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest - 12, // 38: daemon.DaemonService.Up:input_type -> daemon.UpRequest - 14, // 39: daemon.DaemonService.Status:input_type -> daemon.StatusRequest - 16, // 40: daemon.DaemonService.Down:input_type -> daemon.DownRequest - 18, // 41: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest - 29, // 42: daemon.DaemonService.ListNetworks:input_type -> daemon.ListNetworksRequest - 31, // 43: daemon.DaemonService.SelectNetworks:input_type -> daemon.SelectNetworksRequest - 31, // 44: daemon.DaemonService.DeselectNetworks:input_type -> daemon.SelectNetworksRequest - 5, // 45: daemon.DaemonService.ForwardingRules:input_type -> daemon.EmptyRequest - 38, // 46: daemon.DaemonService.DebugBundle:input_type -> daemon.DebugBundleRequest - 40, // 47: daemon.DaemonService.GetLogLevel:input_type -> daemon.GetLogLevelRequest - 42, // 48: daemon.DaemonService.SetLogLevel:input_type -> daemon.SetLogLevelRequest - 45, // 49: daemon.DaemonService.ListStates:input_type -> daemon.ListStatesRequest - 47, // 50: daemon.DaemonService.CleanState:input_type -> daemon.CleanStateRequest - 49, // 51: daemon.DaemonService.DeleteState:input_type -> daemon.DeleteStateRequest - 51, // 52: daemon.DaemonService.SetSyncResponsePersistence:input_type -> daemon.SetSyncResponsePersistenceRequest - 54, // 53: daemon.DaemonService.TracePacket:input_type -> daemon.TracePacketRequest - 57, // 54: daemon.DaemonService.SubscribeEvents:input_type -> daemon.SubscribeRequest - 59, // 55: daemon.DaemonService.GetEvents:input_type -> daemon.GetEventsRequest - 61, // 56: daemon.DaemonService.SwitchProfile:input_type -> daemon.SwitchProfileRequest - 63, // 57: daemon.DaemonService.SetConfig:input_type -> daemon.SetConfigRequest - 65, // 58: daemon.DaemonService.AddProfile:input_type -> daemon.AddProfileRequest - 67, // 59: daemon.DaemonService.RemoveProfile:input_type -> daemon.RemoveProfileRequest - 69, // 60: daemon.DaemonService.ListProfiles:input_type -> daemon.ListProfilesRequest - 72, // 61: daemon.DaemonService.GetActiveProfile:input_type -> daemon.GetActiveProfileRequest - 74, // 62: daemon.DaemonService.Logout:input_type -> daemon.LogoutRequest - 76, // 63: daemon.DaemonService.GetFeatures:input_type -> daemon.GetFeaturesRequest - 78, // 64: daemon.DaemonService.TriggerUpdate:input_type -> daemon.TriggerUpdateRequest - 80, // 65: daemon.DaemonService.GetPeerSSHHostKey:input_type -> daemon.GetPeerSSHHostKeyRequest - 82, // 66: daemon.DaemonService.RequestJWTAuth:input_type -> daemon.RequestJWTAuthRequest - 84, // 67: daemon.DaemonService.WaitJWTToken:input_type -> daemon.WaitJWTTokenRequest - 86, // 68: daemon.DaemonService.StartCPUProfile:input_type -> daemon.StartCPUProfileRequest - 88, // 69: daemon.DaemonService.StopCPUProfile:input_type -> daemon.StopCPUProfileRequest - 6, // 70: daemon.DaemonService.NotifyOSLifecycle:input_type -> daemon.OSLifecycleRequest - 90, // 71: daemon.DaemonService.GetInstallerResult:input_type -> daemon.InstallerResultRequest - 92, // 72: daemon.DaemonService.ExposeService:input_type -> daemon.ExposeServiceRequest - 9, // 73: daemon.DaemonService.Login:output_type -> daemon.LoginResponse - 11, // 74: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse - 13, // 75: daemon.DaemonService.Up:output_type -> daemon.UpResponse - 15, // 76: daemon.DaemonService.Status:output_type -> daemon.StatusResponse - 17, // 77: daemon.DaemonService.Down:output_type -> daemon.DownResponse - 19, // 78: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse - 30, // 79: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse - 32, // 80: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse - 32, // 81: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse - 37, // 82: daemon.DaemonService.ForwardingRules:output_type -> daemon.ForwardingRulesResponse - 39, // 83: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse - 41, // 84: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse - 43, // 85: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse - 46, // 86: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse - 48, // 87: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse - 50, // 88: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse - 52, // 89: daemon.DaemonService.SetSyncResponsePersistence:output_type -> daemon.SetSyncResponsePersistenceResponse - 56, // 90: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse - 58, // 91: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent - 60, // 92: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse - 62, // 93: daemon.DaemonService.SwitchProfile:output_type -> daemon.SwitchProfileResponse - 64, // 94: daemon.DaemonService.SetConfig:output_type -> daemon.SetConfigResponse - 66, // 95: daemon.DaemonService.AddProfile:output_type -> daemon.AddProfileResponse - 68, // 96: daemon.DaemonService.RemoveProfile:output_type -> daemon.RemoveProfileResponse - 70, // 97: daemon.DaemonService.ListProfiles:output_type -> daemon.ListProfilesResponse - 73, // 98: daemon.DaemonService.GetActiveProfile:output_type -> daemon.GetActiveProfileResponse - 75, // 99: daemon.DaemonService.Logout:output_type -> daemon.LogoutResponse - 77, // 100: daemon.DaemonService.GetFeatures:output_type -> daemon.GetFeaturesResponse - 79, // 101: daemon.DaemonService.TriggerUpdate:output_type -> daemon.TriggerUpdateResponse - 81, // 102: daemon.DaemonService.GetPeerSSHHostKey:output_type -> daemon.GetPeerSSHHostKeyResponse - 83, // 103: daemon.DaemonService.RequestJWTAuth:output_type -> daemon.RequestJWTAuthResponse - 85, // 104: daemon.DaemonService.WaitJWTToken:output_type -> daemon.WaitJWTTokenResponse - 87, // 105: daemon.DaemonService.StartCPUProfile:output_type -> daemon.StartCPUProfileResponse - 89, // 106: daemon.DaemonService.StopCPUProfile:output_type -> daemon.StopCPUProfileResponse - 7, // 107: daemon.DaemonService.NotifyOSLifecycle:output_type -> daemon.OSLifecycleResponse - 91, // 108: daemon.DaemonService.GetInstallerResult:output_type -> daemon.InstallerResultResponse - 93, // 109: daemon.DaemonService.ExposeService:output_type -> daemon.ExposeServiceEvent - 73, // [73:110] is the sub-list for method output_type - 36, // [36:73] is the sub-list for method input_type - 36, // [36:36] is the sub-list for extension type_name - 36, // [36:36] is the sub-list for extension extendee - 0, // [0:36] is the sub-list for field type_name + 2, // 0: daemon.OSLifecycleRequest.type:type_name -> daemon.OSLifecycleRequest.CycleType + 99, // 1: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration + 29, // 2: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus + 100, // 3: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp + 100, // 4: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp + 99, // 5: daemon.PeerState.latency:type_name -> google.protobuf.Duration + 26, // 6: daemon.SSHServerState.sessions:type_name -> daemon.SSHSessionInfo + 23, // 7: daemon.FullStatus.managementState:type_name -> daemon.ManagementState + 22, // 8: daemon.FullStatus.signalState:type_name -> daemon.SignalState + 21, // 9: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState + 20, // 10: daemon.FullStatus.peers:type_name -> daemon.PeerState + 24, // 11: daemon.FullStatus.relays:type_name -> daemon.RelayState + 25, // 12: daemon.FullStatus.dns_servers:type_name -> daemon.NSGroupState + 59, // 13: daemon.FullStatus.events:type_name -> daemon.SystemEvent + 27, // 14: daemon.FullStatus.sshServerState:type_name -> daemon.SSHServerState + 28, // 15: daemon.FullStatus.vncServerState:type_name -> daemon.VNCServerState + 35, // 16: daemon.ListNetworksResponse.routes:type_name -> daemon.Network + 96, // 17: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry + 97, // 18: daemon.PortInfo.range:type_name -> daemon.PortInfo.Range + 36, // 19: daemon.ForwardingRule.destinationPort:type_name -> daemon.PortInfo + 36, // 20: daemon.ForwardingRule.translatedPort:type_name -> daemon.PortInfo + 37, // 21: daemon.ForwardingRulesResponse.rules:type_name -> daemon.ForwardingRule + 0, // 22: daemon.GetLogLevelResponse.level:type_name -> daemon.LogLevel + 0, // 23: daemon.SetLogLevelRequest.level:type_name -> daemon.LogLevel + 45, // 24: daemon.ListStatesResponse.states:type_name -> daemon.State + 54, // 25: daemon.TracePacketRequest.tcp_flags:type_name -> daemon.TCPFlags + 56, // 26: daemon.TracePacketResponse.stages:type_name -> daemon.TraceStage + 3, // 27: daemon.SystemEvent.severity:type_name -> daemon.SystemEvent.Severity + 4, // 28: daemon.SystemEvent.category:type_name -> daemon.SystemEvent.Category + 100, // 29: daemon.SystemEvent.timestamp:type_name -> google.protobuf.Timestamp + 98, // 30: daemon.SystemEvent.metadata:type_name -> daemon.SystemEvent.MetadataEntry + 59, // 31: daemon.GetEventsResponse.events:type_name -> daemon.SystemEvent + 99, // 32: daemon.SetConfigRequest.dnsRouteInterval:type_name -> google.protobuf.Duration + 72, // 33: daemon.ListProfilesResponse.profiles:type_name -> daemon.Profile + 1, // 34: daemon.ExposeServiceRequest.protocol:type_name -> daemon.ExposeProtocol + 95, // 35: daemon.ExposeServiceEvent.ready:type_name -> daemon.ExposeServiceReady + 34, // 36: daemon.Network.ResolvedIPsEntry.value:type_name -> daemon.IPList + 8, // 37: daemon.DaemonService.Login:input_type -> daemon.LoginRequest + 10, // 38: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest + 12, // 39: daemon.DaemonService.Up:input_type -> daemon.UpRequest + 14, // 40: daemon.DaemonService.Status:input_type -> daemon.StatusRequest + 16, // 41: daemon.DaemonService.Down:input_type -> daemon.DownRequest + 18, // 42: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest + 30, // 43: daemon.DaemonService.ListNetworks:input_type -> daemon.ListNetworksRequest + 32, // 44: daemon.DaemonService.SelectNetworks:input_type -> daemon.SelectNetworksRequest + 32, // 45: daemon.DaemonService.DeselectNetworks:input_type -> daemon.SelectNetworksRequest + 5, // 46: daemon.DaemonService.ForwardingRules:input_type -> daemon.EmptyRequest + 39, // 47: daemon.DaemonService.DebugBundle:input_type -> daemon.DebugBundleRequest + 41, // 48: daemon.DaemonService.GetLogLevel:input_type -> daemon.GetLogLevelRequest + 43, // 49: daemon.DaemonService.SetLogLevel:input_type -> daemon.SetLogLevelRequest + 46, // 50: daemon.DaemonService.ListStates:input_type -> daemon.ListStatesRequest + 48, // 51: daemon.DaemonService.CleanState:input_type -> daemon.CleanStateRequest + 50, // 52: daemon.DaemonService.DeleteState:input_type -> daemon.DeleteStateRequest + 52, // 53: daemon.DaemonService.SetSyncResponsePersistence:input_type -> daemon.SetSyncResponsePersistenceRequest + 55, // 54: daemon.DaemonService.TracePacket:input_type -> daemon.TracePacketRequest + 58, // 55: daemon.DaemonService.SubscribeEvents:input_type -> daemon.SubscribeRequest + 60, // 56: daemon.DaemonService.GetEvents:input_type -> daemon.GetEventsRequest + 62, // 57: daemon.DaemonService.SwitchProfile:input_type -> daemon.SwitchProfileRequest + 64, // 58: daemon.DaemonService.SetConfig:input_type -> daemon.SetConfigRequest + 66, // 59: daemon.DaemonService.AddProfile:input_type -> daemon.AddProfileRequest + 68, // 60: daemon.DaemonService.RemoveProfile:input_type -> daemon.RemoveProfileRequest + 70, // 61: daemon.DaemonService.ListProfiles:input_type -> daemon.ListProfilesRequest + 73, // 62: daemon.DaemonService.GetActiveProfile:input_type -> daemon.GetActiveProfileRequest + 75, // 63: daemon.DaemonService.Logout:input_type -> daemon.LogoutRequest + 77, // 64: daemon.DaemonService.GetFeatures:input_type -> daemon.GetFeaturesRequest + 79, // 65: daemon.DaemonService.TriggerUpdate:input_type -> daemon.TriggerUpdateRequest + 81, // 66: daemon.DaemonService.GetPeerSSHHostKey:input_type -> daemon.GetPeerSSHHostKeyRequest + 83, // 67: daemon.DaemonService.RequestJWTAuth:input_type -> daemon.RequestJWTAuthRequest + 85, // 68: daemon.DaemonService.WaitJWTToken:input_type -> daemon.WaitJWTTokenRequest + 87, // 69: daemon.DaemonService.StartCPUProfile:input_type -> daemon.StartCPUProfileRequest + 89, // 70: daemon.DaemonService.StopCPUProfile:input_type -> daemon.StopCPUProfileRequest + 6, // 71: daemon.DaemonService.NotifyOSLifecycle:input_type -> daemon.OSLifecycleRequest + 91, // 72: daemon.DaemonService.GetInstallerResult:input_type -> daemon.InstallerResultRequest + 93, // 73: daemon.DaemonService.ExposeService:input_type -> daemon.ExposeServiceRequest + 9, // 74: daemon.DaemonService.Login:output_type -> daemon.LoginResponse + 11, // 75: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse + 13, // 76: daemon.DaemonService.Up:output_type -> daemon.UpResponse + 15, // 77: daemon.DaemonService.Status:output_type -> daemon.StatusResponse + 17, // 78: daemon.DaemonService.Down:output_type -> daemon.DownResponse + 19, // 79: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse + 31, // 80: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse + 33, // 81: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse + 33, // 82: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse + 38, // 83: daemon.DaemonService.ForwardingRules:output_type -> daemon.ForwardingRulesResponse + 40, // 84: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse + 42, // 85: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse + 44, // 86: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse + 47, // 87: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse + 49, // 88: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse + 51, // 89: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse + 53, // 90: daemon.DaemonService.SetSyncResponsePersistence:output_type -> daemon.SetSyncResponsePersistenceResponse + 57, // 91: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse + 59, // 92: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent + 61, // 93: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse + 63, // 94: daemon.DaemonService.SwitchProfile:output_type -> daemon.SwitchProfileResponse + 65, // 95: daemon.DaemonService.SetConfig:output_type -> daemon.SetConfigResponse + 67, // 96: daemon.DaemonService.AddProfile:output_type -> daemon.AddProfileResponse + 69, // 97: daemon.DaemonService.RemoveProfile:output_type -> daemon.RemoveProfileResponse + 71, // 98: daemon.DaemonService.ListProfiles:output_type -> daemon.ListProfilesResponse + 74, // 99: daemon.DaemonService.GetActiveProfile:output_type -> daemon.GetActiveProfileResponse + 76, // 100: daemon.DaemonService.Logout:output_type -> daemon.LogoutResponse + 78, // 101: daemon.DaemonService.GetFeatures:output_type -> daemon.GetFeaturesResponse + 80, // 102: daemon.DaemonService.TriggerUpdate:output_type -> daemon.TriggerUpdateResponse + 82, // 103: daemon.DaemonService.GetPeerSSHHostKey:output_type -> daemon.GetPeerSSHHostKeyResponse + 84, // 104: daemon.DaemonService.RequestJWTAuth:output_type -> daemon.RequestJWTAuthResponse + 86, // 105: daemon.DaemonService.WaitJWTToken:output_type -> daemon.WaitJWTTokenResponse + 88, // 106: daemon.DaemonService.StartCPUProfile:output_type -> daemon.StartCPUProfileResponse + 90, // 107: daemon.DaemonService.StopCPUProfile:output_type -> daemon.StopCPUProfileResponse + 7, // 108: daemon.DaemonService.NotifyOSLifecycle:output_type -> daemon.OSLifecycleResponse + 92, // 109: daemon.DaemonService.GetInstallerResult:output_type -> daemon.InstallerResultResponse + 94, // 110: daemon.DaemonService.ExposeService:output_type -> daemon.ExposeServiceEvent + 74, // [74:111] is the sub-list for method output_type + 37, // [37:74] is the sub-list for method input_type + 37, // [37:37] is the sub-list for extension type_name + 37, // [37:37] is the sub-list for extension extendee + 0, // [0:37] is the sub-list for field type_name } func init() { file_daemon_proto_init() } @@ -6851,17 +6967,17 @@ func file_daemon_proto_init() { file_daemon_proto_msgTypes[3].OneofWrappers = []any{} file_daemon_proto_msgTypes[7].OneofWrappers = []any{} file_daemon_proto_msgTypes[9].OneofWrappers = []any{} - file_daemon_proto_msgTypes[30].OneofWrappers = []any{ + file_daemon_proto_msgTypes[31].OneofWrappers = []any{ (*PortInfo_Port)(nil), (*PortInfo_Range_)(nil), } - file_daemon_proto_msgTypes[49].OneofWrappers = []any{} 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[69].OneofWrappers = []any{} - file_daemon_proto_msgTypes[77].OneofWrappers = []any{} - file_daemon_proto_msgTypes[88].OneofWrappers = []any{ + file_daemon_proto_msgTypes[51].OneofWrappers = []any{} + file_daemon_proto_msgTypes[57].OneofWrappers = []any{} + file_daemon_proto_msgTypes[59].OneofWrappers = []any{} + file_daemon_proto_msgTypes[70].OneofWrappers = []any{} + file_daemon_proto_msgTypes[78].OneofWrappers = []any{} + file_daemon_proto_msgTypes[89].OneofWrappers = []any{ (*ExposeServiceEvent_Ready)(nil), } type x struct{} @@ -6870,7 +6986,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: 5, - NumMessages: 93, + NumMessages: 94, NumExtensions: 0, NumServices: 1, }, diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index 19976660c..44968bb2e 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -209,6 +209,9 @@ message LoginRequest { optional bool enableSSHRemotePortForwarding = 37; optional bool disableSSHAuth = 38; optional int32 sshJWTCacheTTL = 39; + + optional bool serverVNCAllowed = 41; + optional bool disableVNCAuth = 42; } message LoginResponse { @@ -316,6 +319,10 @@ message GetConfigResponse { bool disableSSHAuth = 25; int32 sshJWTCacheTTL = 26; + + bool serverVNCAllowed = 28; + + bool disableVNCAuth = 29; } // PeerState contains the latest state of a peer @@ -394,6 +401,11 @@ message SSHServerState { repeated SSHSessionInfo sessions = 2; } +// VNCServerState contains the latest state of the VNC server +message VNCServerState { + bool enabled = 1; +} + // FullStatus contains the full state held by the Status instance message FullStatus { ManagementState managementState = 1; @@ -408,6 +420,7 @@ message FullStatus { bool lazyConnectionEnabled = 9; SSHServerState sshServerState = 10; + VNCServerState vncServerState = 11; } // Networks @@ -677,6 +690,9 @@ message SetConfigRequest { optional bool enableSSHRemotePortForwarding = 32; optional bool disableSSHAuth = 33; optional int32 sshJWTCacheTTL = 34; + + optional bool serverVNCAllowed = 36; + optional bool disableVNCAuth = 37; } message SetConfigResponse{} diff --git a/client/server/server.go b/client/server/server.go index 70e4c342f..a0aa11197 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -369,6 +369,7 @@ func (s *Server) SetConfig(callerCtx context.Context, msg *proto.SetConfigReques config.RosenpassPermissive = msg.RosenpassPermissive config.DisableAutoConnect = msg.DisableAutoConnect config.ServerSSHAllowed = msg.ServerSSHAllowed + config.ServerVNCAllowed = msg.ServerVNCAllowed config.NetworkMonitor = msg.NetworkMonitor config.DisableClientRoutes = msg.DisableClientRoutes config.DisableServerRoutes = msg.DisableServerRoutes @@ -385,6 +386,9 @@ func (s *Server) SetConfig(callerCtx context.Context, msg *proto.SetConfigReques if msg.DisableSSHAuth != nil { config.DisableSSHAuth = msg.DisableSSHAuth } + if msg.DisableVNCAuth != nil { + config.DisableVNCAuth = msg.DisableVNCAuth + } if msg.SshJWTCacheTTL != nil { ttl := int(*msg.SshJWTCacheTTL) config.SSHJWTCacheTTL = &ttl @@ -1123,6 +1127,7 @@ func (s *Server) Status( pbFullStatus := fullStatus.ToProto() pbFullStatus.Events = s.statusRecorder.GetEventHistory() pbFullStatus.SshServerState = s.getSSHServerState() + pbFullStatus.VncServerState = s.getVNCServerState() statusResponse.FullStatus = pbFullStatus } @@ -1162,6 +1167,26 @@ func (s *Server) getSSHServerState() *proto.SSHServerState { return sshServerState } +// getVNCServerState retrieves the current VNC server state. +func (s *Server) getVNCServerState() *proto.VNCServerState { + s.mutex.Lock() + connectClient := s.connectClient + s.mutex.Unlock() + + if connectClient == nil { + return nil + } + + engine := connectClient.Engine() + if engine == nil { + return nil + } + + return &proto.VNCServerState{ + Enabled: engine.GetVNCServerStatus(), + } +} + // GetPeerSSHHostKey retrieves SSH host key for a specific peer func (s *Server) GetPeerSSHHostKey( ctx context.Context, @@ -1503,6 +1528,11 @@ func (s *Server) GetConfig(ctx context.Context, req *proto.GetConfigRequest) (*p disableSSHAuth = *cfg.DisableSSHAuth } + disableVNCAuth := false + if cfg.DisableVNCAuth != nil { + disableVNCAuth = *cfg.DisableVNCAuth + } + sshJWTCacheTTL := int32(0) if cfg.SSHJWTCacheTTL != nil { sshJWTCacheTTL = int32(*cfg.SSHJWTCacheTTL) @@ -1517,6 +1547,7 @@ func (s *Server) GetConfig(ctx context.Context, req *proto.GetConfigRequest) (*p Mtu: int64(cfg.MTU), DisableAutoConnect: cfg.DisableAutoConnect, ServerSSHAllowed: *cfg.ServerSSHAllowed, + ServerVNCAllowed: cfg.ServerVNCAllowed != nil && *cfg.ServerVNCAllowed, RosenpassEnabled: cfg.RosenpassEnabled, RosenpassPermissive: cfg.RosenpassPermissive, LazyConnectionEnabled: cfg.LazyConnectionEnabled, @@ -1532,6 +1563,7 @@ func (s *Server) GetConfig(ctx context.Context, req *proto.GetConfigRequest) (*p EnableSSHLocalPortForwarding: enableSSHLocalPortForwarding, EnableSSHRemotePortForwarding: enableSSHRemotePortForwarding, DisableSSHAuth: disableSSHAuth, + DisableVNCAuth: disableVNCAuth, SshJWTCacheTTL: sshJWTCacheTTL, }, nil } diff --git a/client/server/setconfig_test.go b/client/server/setconfig_test.go index 7f6847c43..85e8f592c 100644 --- a/client/server/setconfig_test.go +++ b/client/server/setconfig_test.go @@ -58,6 +58,8 @@ func TestSetConfig_AllFieldsSaved(t *testing.T) { rosenpassEnabled := true rosenpassPermissive := true serverSSHAllowed := true + serverVNCAllowed := true + disableVNCAuth := true interfaceName := "utun100" wireguardPort := int64(51820) preSharedKey := "test-psk" @@ -82,6 +84,8 @@ func TestSetConfig_AllFieldsSaved(t *testing.T) { RosenpassEnabled: &rosenpassEnabled, RosenpassPermissive: &rosenpassPermissive, ServerSSHAllowed: &serverSSHAllowed, + ServerVNCAllowed: &serverVNCAllowed, + DisableVNCAuth: &disableVNCAuth, InterfaceName: &interfaceName, WireguardPort: &wireguardPort, OptionalPreSharedKey: &preSharedKey, @@ -125,6 +129,10 @@ func TestSetConfig_AllFieldsSaved(t *testing.T) { require.Equal(t, rosenpassPermissive, cfg.RosenpassPermissive) require.NotNil(t, cfg.ServerSSHAllowed) require.Equal(t, serverSSHAllowed, *cfg.ServerSSHAllowed) + require.NotNil(t, cfg.ServerVNCAllowed) + require.Equal(t, serverVNCAllowed, *cfg.ServerVNCAllowed) + require.NotNil(t, cfg.DisableVNCAuth) + require.Equal(t, disableVNCAuth, *cfg.DisableVNCAuth) require.Equal(t, interfaceName, cfg.WgIface) require.Equal(t, int(wireguardPort), cfg.WgPort) require.Equal(t, preSharedKey, cfg.PreSharedKey) @@ -176,6 +184,8 @@ func verifyAllFieldsCovered(t *testing.T, req *proto.SetConfigRequest) { "RosenpassEnabled": true, "RosenpassPermissive": true, "ServerSSHAllowed": true, + "ServerVNCAllowed": true, + "DisableVNCAuth": true, "InterfaceName": true, "WireguardPort": true, "OptionalPreSharedKey": true, @@ -236,6 +246,8 @@ func TestCLIFlags_MappedToSetConfig(t *testing.T) { "enable-rosenpass": "RosenpassEnabled", "rosenpass-permissive": "RosenpassPermissive", "allow-server-ssh": "ServerSSHAllowed", + "allow-server-vnc": "ServerVNCAllowed", + "disable-vnc-auth": "DisableVNCAuth", "interface-name": "InterfaceName", "wireguard-port": "WireguardPort", "preshared-key": "OptionalPreSharedKey", diff --git a/client/ssh/server/executor_windows.go b/client/ssh/server/executor_windows.go index 51c995ec3..4053170d2 100644 --- a/client/ssh/server/executor_windows.go +++ b/client/ssh/server/executor_windows.go @@ -200,8 +200,8 @@ func newLsaString(s string) lsaString { } } -// generateS4UUserToken creates a Windows token using S4U authentication -// This is the exact approach OpenSSH for Windows uses for public key authentication +// generateS4UUserToken creates a Windows token using S4U authentication. +// This is the same approach OpenSSH for Windows uses for public key authentication. func generateS4UUserToken(logger *log.Entry, username, domain string) (windows.Handle, error) { userCpn := buildUserCpn(username, domain) diff --git a/client/ssh/server/server.go b/client/ssh/server/server.go index 82d3b700f..99901c6a1 100644 --- a/client/ssh/server/server.go +++ b/client/ssh/server/server.go @@ -507,27 +507,7 @@ func (s *Server) checkTokenAge(token *gojwt.Token, jwtConfig *JWTConfig) error { maxTokenAge = DefaultJWTMaxTokenAge } - claims, ok := token.Claims.(gojwt.MapClaims) - if !ok { - userID := extractUserID(token) - return fmt.Errorf("token has invalid claims format (user=%s)", userID) - } - - iat, ok := claims["iat"].(float64) - if !ok { - userID := extractUserID(token) - return fmt.Errorf("token missing iat claim (user=%s)", userID) - } - - issuedAt := time.Unix(int64(iat), 0) - tokenAge := time.Since(issuedAt) - maxAge := time.Duration(maxTokenAge) * time.Second - if tokenAge > maxAge { - userID := getUserIDFromClaims(claims) - return fmt.Errorf("token expired for user=%s: age=%v, max=%v", userID, tokenAge, maxAge) - } - - return nil + return jwt.CheckTokenAge(token, time.Duration(maxTokenAge)*time.Second) } func (s *Server) extractAndValidateUser(token *gojwt.Token) (*auth.UserAuth, error) { @@ -558,27 +538,7 @@ func (s *Server) hasSSHAccess(userAuth *auth.UserAuth) bool { } func extractUserID(token *gojwt.Token) string { - if token == nil { - return "unknown" - } - claims, ok := token.Claims.(gojwt.MapClaims) - if !ok { - return "unknown" - } - return getUserIDFromClaims(claims) -} - -func getUserIDFromClaims(claims gojwt.MapClaims) string { - if sub, ok := claims["sub"].(string); ok && sub != "" { - return sub - } - if userID, ok := claims["user_id"].(string); ok && userID != "" { - return userID - } - if email, ok := claims["email"].(string); ok && email != "" { - return email - } - return "unknown" + return jwt.UserIDFromToken(token) } func (s *Server) parseTokenWithoutValidation(tokenString string) (map[string]interface{}, error) { diff --git a/client/status/status.go b/client/status/status.go index 8c932bbab..ec5b41dcb 100644 --- a/client/status/status.go +++ b/client/status/status.go @@ -130,6 +130,10 @@ type SSHServerStateOutput struct { Sessions []SSHSessionOutput `json:"sessions" yaml:"sessions"` } +type VNCServerStateOutput struct { + Enabled bool `json:"enabled" yaml:"enabled"` +} + type OutputOverview struct { Peers PeersStateOutput `json:"peers" yaml:"peers"` CliVersion string `json:"cliVersion" yaml:"cliVersion"` @@ -151,6 +155,7 @@ type OutputOverview struct { LazyConnectionEnabled bool `json:"lazyConnectionEnabled" yaml:"lazyConnectionEnabled"` ProfileName string `json:"profileName" yaml:"profileName"` SSHServerState SSHServerStateOutput `json:"sshServer" yaml:"sshServer"` + VNCServerState VNCServerStateOutput `json:"vncServer" yaml:"vncServer"` } // ConvertToStatusOutputOverview converts protobuf status to the output overview. @@ -171,6 +176,9 @@ func ConvertToStatusOutputOverview(pbFullStatus *proto.FullStatus, opts ConvertO relayOverview := mapRelays(pbFullStatus.GetRelays()) sshServerOverview := mapSSHServer(pbFullStatus.GetSshServerState()) + vncServerOverview := VNCServerStateOutput{ + Enabled: pbFullStatus.GetVncServerState().GetEnabled(), + } peersOverview := mapPeers(pbFullStatus.GetPeers(), opts.StatusFilter, opts.PrefixNamesFilter, opts.PrefixNamesFilterMap, opts.IPsFilter, opts.ConnectionTypeFilter) overview := OutputOverview{ @@ -194,6 +202,7 @@ func ConvertToStatusOutputOverview(pbFullStatus *proto.FullStatus, opts ConvertO LazyConnectionEnabled: pbFullStatus.GetLazyConnectionEnabled(), ProfileName: opts.ProfileName, SSHServerState: sshServerOverview, + VNCServerState: vncServerOverview, } if opts.Anonymize { @@ -524,6 +533,11 @@ func (o *OutputOverview) GeneralSummary(showURL bool, showRelays bool, showNameS } } + vncServerStatus := "Disabled" + if o.VNCServerState.Enabled { + vncServerStatus = "Enabled" + } + peersCountString := fmt.Sprintf("%d/%d Connected", o.Peers.Connected, o.Peers.Total) var forwardingRulesString string @@ -553,6 +567,7 @@ func (o *OutputOverview) GeneralSummary(showURL bool, showRelays bool, showNameS "Quantum resistance: %s\n"+ "Lazy connection: %s\n"+ "SSH Server: %s\n"+ + "VNC Server: %s\n"+ "Networks: %s\n"+ "%s"+ "Peers count: %s\n", @@ -570,6 +585,7 @@ func (o *OutputOverview) GeneralSummary(showURL bool, showRelays bool, showNameS rosenpassEnabledStatus, lazyConnectionEnabledStatus, sshServerStatus, + vncServerStatus, networks, forwardingRulesString, peersCountString, diff --git a/client/status/status_test.go b/client/status/status_test.go index 7754eebae..7babc5733 100644 --- a/client/status/status_test.go +++ b/client/status/status_test.go @@ -398,6 +398,9 @@ func TestParsingToJSON(t *testing.T) { "sshServer":{ "enabled":false, "sessions":[] + }, + "vncServer":{ + "enabled":false } }` // @formatter:on @@ -505,6 +508,8 @@ profileName: "" sshServer: enabled: false sessions: [] +vncServer: + enabled: false ` assert.Equal(t, expectedYAML, yaml) @@ -572,6 +577,7 @@ Interface type: Kernel Quantum resistance: false Lazy connection: false SSH Server: Disabled +VNC Server: Disabled Networks: 10.10.0.0/24 Peers count: 2/2 Connected `, lastConnectionUpdate1, lastHandshake1, lastConnectionUpdate2, lastHandshake2, runtime.GOOS, runtime.GOARCH, overview.CliVersion) @@ -596,6 +602,7 @@ Interface type: Kernel Quantum resistance: false Lazy connection: false SSH Server: Disabled +VNC Server: Disabled Networks: 10.10.0.0/24 Peers count: 2/2 Connected ` diff --git a/client/system/info.go b/client/system/info.go index 175d1f07f..807053cfd 100644 --- a/client/system/info.go +++ b/client/system/info.go @@ -62,6 +62,7 @@ type Info struct { RosenpassEnabled bool RosenpassPermissive bool ServerSSHAllowed bool + ServerVNCAllowed bool DisableClientRoutes bool DisableServerRoutes bool @@ -77,21 +78,27 @@ type Info struct { EnableSSHLocalPortForwarding bool EnableSSHRemotePortForwarding bool DisableSSHAuth bool + DisableVNCAuth bool } func (i *Info) SetFlags( rosenpassEnabled, rosenpassPermissive bool, serverSSHAllowed *bool, + serverVNCAllowed *bool, disableClientRoutes, disableServerRoutes, disableDNS, disableFirewall, blockLANAccess, blockInbound, lazyConnectionEnabled bool, enableSSHRoot, enableSSHSFTP, enableSSHLocalPortForwarding, enableSSHRemotePortForwarding *bool, disableSSHAuth *bool, + disableVNCAuth *bool, ) { i.RosenpassEnabled = rosenpassEnabled i.RosenpassPermissive = rosenpassPermissive if serverSSHAllowed != nil { i.ServerSSHAllowed = *serverSSHAllowed } + if serverVNCAllowed != nil { + i.ServerVNCAllowed = *serverVNCAllowed + } i.DisableClientRoutes = disableClientRoutes i.DisableServerRoutes = disableServerRoutes @@ -117,6 +124,9 @@ func (i *Info) SetFlags( if disableSSHAuth != nil { i.DisableSSHAuth = *disableSSHAuth } + if disableVNCAuth != nil { + i.DisableVNCAuth = *disableVNCAuth + } } // extractUserAgent extracts Netbird's agent (client) name and version from the outgoing context diff --git a/client/vnc/server/agent_windows.go b/client/vnc/server/agent_windows.go new file mode 100644 index 000000000..41450f214 --- /dev/null +++ b/client/vnc/server/agent_windows.go @@ -0,0 +1,474 @@ +//go:build windows + +package server + +import ( + crand "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net" + "os" + "sync" + "time" + "unsafe" + + log "github.com/sirupsen/logrus" + "golang.org/x/sys/windows" +) + +const ( + agentPort = "15900" + + // agentTokenLen is the length of the random authentication token + // used to verify that connections to the agent come from the service. + agentTokenLen = 32 + + stillActive = 259 + + tokenPrimary = 1 + securityImpersonation = 2 + tokenSessionID = 12 + + createUnicodeEnvironment = 0x00000400 + createNoWindow = 0x08000000 +) + +var ( + kernel32 = windows.NewLazySystemDLL("kernel32.dll") + advapi32 = windows.NewLazySystemDLL("advapi32.dll") + userenv = windows.NewLazySystemDLL("userenv.dll") + + procWTSGetActiveConsoleSessionId = kernel32.NewProc("WTSGetActiveConsoleSessionId") + procSetTokenInformation = advapi32.NewProc("SetTokenInformation") + procCreateEnvironmentBlock = userenv.NewProc("CreateEnvironmentBlock") + procDestroyEnvironmentBlock = userenv.NewProc("DestroyEnvironmentBlock") + + wtsapi32 = windows.NewLazySystemDLL("wtsapi32.dll") + procWTSEnumerateSessionsW = wtsapi32.NewProc("WTSEnumerateSessionsW") + procWTSFreeMemory = wtsapi32.NewProc("WTSFreeMemory") +) + +// GetCurrentSessionID returns the session ID of the current process. +func GetCurrentSessionID() uint32 { + var token windows.Token + if err := windows.OpenProcessToken(windows.CurrentProcess(), + windows.TOKEN_QUERY, &token); err != nil { + return 0 + } + defer token.Close() + var id uint32 + var ret uint32 + _ = windows.GetTokenInformation(token, windows.TokenSessionId, + (*byte)(unsafe.Pointer(&id)), 4, &ret) + return id +} + +func getConsoleSessionID() uint32 { + r, _, _ := procWTSGetActiveConsoleSessionId.Call() + return uint32(r) +} + +const ( + wtsActive = 0 + wtsConnected = 1 + wtsDisconnected = 4 +) + +type wtsSessionInfo struct { + SessionID uint32 + WinStationName [66]byte // actually *uint16, but we just need the struct size + State uint32 +} + +// getActiveSessionID returns the session ID of the best session to attach to. +// Prefers an active (logged-in, interactive) session over the console session. +// This avoids kicking out an RDP user when the console is at the login screen. +func getActiveSessionID() uint32 { + var sessionInfo uintptr + var count uint32 + + r, _, _ := procWTSEnumerateSessionsW.Call( + 0, // WTS_CURRENT_SERVER_HANDLE + 0, // reserved + 1, // version + uintptr(unsafe.Pointer(&sessionInfo)), + uintptr(unsafe.Pointer(&count)), + ) + if r == 0 || count == 0 { + return getConsoleSessionID() + } + defer procWTSFreeMemory.Call(sessionInfo) + + type wtsSession struct { + SessionID uint32 + Station *uint16 + State uint32 + } + sessions := unsafe.Slice((*wtsSession)(unsafe.Pointer(sessionInfo)), count) + + // Find the first active session (not session 0, which is the services session). + var bestID uint32 + found := false + for _, s := range sessions { + if s.SessionID == 0 { + continue + } + if s.State == wtsActive { + bestID = s.SessionID + found = true + break + } + } + + if !found { + return getConsoleSessionID() + } + return bestID +} + +// getSystemTokenForSession duplicates the current SYSTEM token and sets its +// session ID so the spawned process runs in the target session. Using a SYSTEM +// token gives access to both Default and Winlogon desktops plus UIPI bypass. +func getSystemTokenForSession(sessionID uint32) (windows.Token, error) { + var cur windows.Token + if err := windows.OpenProcessToken(windows.CurrentProcess(), + windows.MAXIMUM_ALLOWED, &cur); err != nil { + return 0, fmt.Errorf("OpenProcessToken: %w", err) + } + defer cur.Close() + + var dup windows.Token + if err := windows.DuplicateTokenEx(cur, windows.MAXIMUM_ALLOWED, nil, + securityImpersonation, tokenPrimary, &dup); err != nil { + return 0, fmt.Errorf("DuplicateTokenEx: %w", err) + } + + sid := sessionID + r, _, err := procSetTokenInformation.Call( + uintptr(dup), + uintptr(tokenSessionID), + uintptr(unsafe.Pointer(&sid)), + unsafe.Sizeof(sid), + ) + if r == 0 { + dup.Close() + return 0, fmt.Errorf("SetTokenInformation(SessionId=%d): %w", sessionID, err) + } + return dup, nil +} + +const agentTokenEnvVar = "NB_VNC_AGENT_TOKEN" + +// injectEnvVar appends a KEY=VALUE entry to a Unicode environment block. +// The block is a sequence of null-terminated UTF-16 strings, terminated by +// an extra null. Returns a new block pointer with the entry added. +func injectEnvVar(envBlock uintptr, key, value string) uintptr { + entry := key + "=" + value + + // Walk the existing block to find its total length. + ptr := (*uint16)(unsafe.Pointer(envBlock)) + var totalChars int + for { + ch := *(*uint16)(unsafe.Pointer(uintptr(unsafe.Pointer(ptr)) + uintptr(totalChars)*2)) + if ch == 0 { + // Check for double-null terminator. + next := *(*uint16)(unsafe.Pointer(uintptr(unsafe.Pointer(ptr)) + uintptr(totalChars+1)*2)) + totalChars++ + if next == 0 { + // End of block (don't count the final null yet, we'll rebuild). + break + } + } else { + totalChars++ + } + } + + entryUTF16, _ := windows.UTF16FromString(entry) + // New block: existing entries + new entry (null-terminated) + final null. + newLen := totalChars + len(entryUTF16) + 1 + newBlock := make([]uint16, newLen) + // Copy existing entries (up to but not including the final null). + for i := range totalChars { + newBlock[i] = *(*uint16)(unsafe.Pointer(uintptr(unsafe.Pointer(ptr)) + uintptr(i)*2)) + } + copy(newBlock[totalChars:], entryUTF16) + newBlock[newLen-1] = 0 // final null terminator + + return uintptr(unsafe.Pointer(&newBlock[0])) +} + +func spawnAgentInSession(sessionID uint32, port string, authToken string) (windows.Handle, error) { + token, err := getSystemTokenForSession(sessionID) + if err != nil { + return 0, fmt.Errorf("get SYSTEM token for session %d: %w", sessionID, err) + } + defer token.Close() + + var envBlock uintptr + r, _, _ := procCreateEnvironmentBlock.Call( + uintptr(unsafe.Pointer(&envBlock)), + uintptr(token), + 0, + ) + if r != 0 { + defer procDestroyEnvironmentBlock.Call(envBlock) + } + + // Inject the auth token into the environment block so it doesn't appear + // in the process command line (visible via tasklist/wmic). + if r != 0 { + envBlock = injectEnvVar(envBlock, agentTokenEnvVar, authToken) + } + + exePath, err := os.Executable() + if err != nil { + return 0, fmt.Errorf("get executable path: %w", err) + } + + cmdLine := fmt.Sprintf(`"%s" vnc-agent --port %s`, exePath, port) + cmdLineW, err := windows.UTF16PtrFromString(cmdLine) + if err != nil { + return 0, fmt.Errorf("UTF16 cmdline: %w", err) + } + + // Create an inheritable pipe for the agent's stderr so we can relog + // its output in the service process. + var sa windows.SecurityAttributes + sa.Length = uint32(unsafe.Sizeof(sa)) + sa.InheritHandle = 1 + + var stderrRead, stderrWrite windows.Handle + if err := windows.CreatePipe(&stderrRead, &stderrWrite, &sa, 0); err != nil { + return 0, fmt.Errorf("create stderr pipe: %w", err) + } + // The read end must NOT be inherited by the child. + windows.SetHandleInformation(stderrRead, windows.HANDLE_FLAG_INHERIT, 0) + + desktop, _ := windows.UTF16PtrFromString(`WinSta0\Default`) + si := windows.StartupInfo{ + Cb: uint32(unsafe.Sizeof(windows.StartupInfo{})), + Desktop: desktop, + Flags: windows.STARTF_USESHOWWINDOW | windows.STARTF_USESTDHANDLES, + ShowWindow: 0, + StdErr: stderrWrite, + StdOutput: stderrWrite, + } + var pi windows.ProcessInformation + + var envPtr *uint16 + if envBlock != 0 { + envPtr = (*uint16)(unsafe.Pointer(envBlock)) + } + + err = windows.CreateProcessAsUser( + token, nil, cmdLineW, + nil, nil, true, // inheritHandles=true for the pipe + createUnicodeEnvironment|createNoWindow, + envPtr, nil, &si, &pi, + ) + // Close the write end in the parent so reads will get EOF when the child exits. + windows.CloseHandle(stderrWrite) + if err != nil { + windows.CloseHandle(stderrRead) + return 0, fmt.Errorf("CreateProcessAsUser: %w", err) + } + windows.CloseHandle(pi.Thread) + + // Relog agent output in the service with a [vnc-agent] prefix. + go relogAgentOutput(stderrRead) + + log.Infof("spawned agent PID=%d in session %d on port %s", pi.ProcessId, sessionID, port) + return pi.Process, nil +} + +// sessionManager monitors the active console session and ensures a VNC agent +// process is running in it. When the session changes (e.g., user switch, RDP +// connect/disconnect), it kills the old agent and spawns a new one. +type sessionManager struct { + port string + mu sync.Mutex + agentProc windows.Handle + sessionID uint32 + authToken string + done chan struct{} +} + +func newSessionManager(port string) *sessionManager { + return &sessionManager{port: port, sessionID: ^uint32(0), done: make(chan struct{})} +} + +// generateAuthToken creates a new random hex token for agent authentication. +func generateAuthToken() string { + b := make([]byte, agentTokenLen) + if _, err := crand.Read(b); err != nil { + log.Warnf("generate agent auth token: %v", err) + return "" + } + return hex.EncodeToString(b) +} + +// AuthToken returns the current agent authentication token. +func (m *sessionManager) AuthToken() string { + m.mu.Lock() + defer m.mu.Unlock() + return m.authToken +} + +// Stop signals the session manager to exit its polling loop. +func (m *sessionManager) Stop() { + select { + case <-m.done: + default: + close(m.done) + } +} + +func (m *sessionManager) run() { + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + for { + sid := getActiveSessionID() + + m.mu.Lock() + if sid != m.sessionID { + log.Infof("active session changed: %d -> %d", m.sessionID, sid) + m.killAgent() + m.sessionID = sid + } + + if m.agentProc != 0 { + var code uint32 + _ = windows.GetExitCodeProcess(m.agentProc, &code) + if code != stillActive { + log.Infof("agent exited (code=%d), respawning", code) + windows.CloseHandle(m.agentProc) + m.agentProc = 0 + } + } + + if m.agentProc == 0 && sid != 0xFFFFFFFF { + m.authToken = generateAuthToken() + h, err := spawnAgentInSession(sid, m.port, m.authToken) + if err != nil { + log.Warnf("spawn agent in session %d: %v", sid, err) + m.authToken = "" + } else { + m.agentProc = h + } + } + m.mu.Unlock() + + select { + case <-m.done: + m.mu.Lock() + m.killAgent() + m.mu.Unlock() + return + case <-ticker.C: + } + } +} + +func (m *sessionManager) killAgent() { + if m.agentProc != 0 { + _ = windows.TerminateProcess(m.agentProc, 0) + windows.CloseHandle(m.agentProc) + m.agentProc = 0 + log.Info("killed old agent") + } +} + +// relogAgentOutput reads JSON log lines from the agent's stderr pipe and +// relogs them at the correct level with the service's formatter. +func relogAgentOutput(pipe windows.Handle) { + defer windows.CloseHandle(pipe) + f := os.NewFile(uintptr(pipe), "vnc-agent-stderr") + defer f.Close() + + entry := log.WithField("component", "vnc-agent") + dec := json.NewDecoder(f) + for dec.More() { + var m map[string]any + if err := dec.Decode(&m); err != nil { + break + } + msg, _ := m["msg"].(string) + if msg == "" { + continue + } + + // Forward extra fields from the agent (skip standard logrus fields). + // Remap "caller" to "source" so it doesn't conflict with logrus internals + // but still shows the original file/line from the agent process. + fields := make(log.Fields) + for k, v := range m { + switch k { + case "msg", "level", "time", "func": + continue + case "caller": + fields["source"] = v + default: + fields[k] = v + } + } + e := entry.WithFields(fields) + + switch m["level"] { + case "error": + e.Error(msg) + case "warning": + e.Warn(msg) + case "debug": + e.Debug(msg) + case "trace": + e.Trace(msg) + default: + e.Info(msg) + } + } +} + +// proxyToAgent connects to the agent, sends the auth token, then proxies +// the VNC client connection bidirectionally. +func proxyToAgent(client net.Conn, port string, authToken string) { + defer client.Close() + + addr := "127.0.0.1:" + port + var agentConn net.Conn + var err error + for range 50 { + agentConn, err = net.DialTimeout("tcp", addr, time.Second) + if err == nil { + break + } + time.Sleep(200 * time.Millisecond) + } + if err != nil { + log.Warnf("proxy cannot reach agent at %s: %v", addr, err) + return + } + defer agentConn.Close() + + // Send the auth token so the agent can verify this connection + // comes from the trusted service process. + tokenBytes, _ := hex.DecodeString(authToken) + if _, err := agentConn.Write(tokenBytes); err != nil { + log.Warnf("send auth token to agent: %v", err) + return + } + + log.Debugf("proxy connected to agent, starting bidirectional copy") + + done := make(chan struct{}, 2) + cp := func(label string, dst, src net.Conn) { + n, err := io.Copy(dst, src) + log.Debugf("proxy %s: %d bytes, err=%v", label, n, err) + done <- struct{}{} + } + go cp("client→agent", agentConn, client) + go cp("agent→client", client, agentConn) + <-done +} diff --git a/client/vnc/server/capture_darwin.go b/client/vnc/server/capture_darwin.go new file mode 100644 index 000000000..fc20fdb6c --- /dev/null +++ b/client/vnc/server/capture_darwin.go @@ -0,0 +1,486 @@ +//go:build darwin && !ios + +package server + +import ( + "errors" + "fmt" + "hash/maphash" + "image" + "os" + "runtime" + "strconv" + "sync" + "time" + "unsafe" + + "github.com/ebitengine/purego" + log "github.com/sirupsen/logrus" +) + + +var darwinCaptureOnce sync.Once + +var ( + cgMainDisplayID func() uint32 + cgDisplayPixelsWide func(uint32) uintptr + cgDisplayPixelsHigh func(uint32) uintptr + cgDisplayCreateImage func(uint32) uintptr + cgImageGetWidth func(uintptr) uintptr + cgImageGetHeight func(uintptr) uintptr + cgImageGetBytesPerRow func(uintptr) uintptr + cgImageGetBitsPerPixel func(uintptr) uintptr + cgImageGetDataProvider func(uintptr) uintptr + cgDataProviderCopyData func(uintptr) uintptr + cgImageRelease func(uintptr) + cfDataGetLength func(uintptr) int64 + cfDataGetBytePtr func(uintptr) uintptr + cfRelease func(uintptr) + cgPreflightScreenCaptureAccess func() bool + cgRequestScreenCaptureAccess func() bool + darwinCaptureReady bool +) + +func initDarwinCapture() { + darwinCaptureOnce.Do(func() { + cg, err := purego.Dlopen("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics", purego.RTLD_NOW|purego.RTLD_GLOBAL) + if err != nil { + log.Debugf("load CoreGraphics: %v", err) + return + } + cf, err := purego.Dlopen("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation", purego.RTLD_NOW|purego.RTLD_GLOBAL) + if err != nil { + log.Debugf("load CoreFoundation: %v", err) + return + } + + purego.RegisterLibFunc(&cgMainDisplayID, cg, "CGMainDisplayID") + purego.RegisterLibFunc(&cgDisplayPixelsWide, cg, "CGDisplayPixelsWide") + purego.RegisterLibFunc(&cgDisplayPixelsHigh, cg, "CGDisplayPixelsHigh") + purego.RegisterLibFunc(&cgDisplayCreateImage, cg, "CGDisplayCreateImage") + purego.RegisterLibFunc(&cgImageGetWidth, cg, "CGImageGetWidth") + purego.RegisterLibFunc(&cgImageGetHeight, cg, "CGImageGetHeight") + purego.RegisterLibFunc(&cgImageGetBytesPerRow, cg, "CGImageGetBytesPerRow") + purego.RegisterLibFunc(&cgImageGetBitsPerPixel, cg, "CGImageGetBitsPerPixel") + purego.RegisterLibFunc(&cgImageGetDataProvider, cg, "CGImageGetDataProvider") + purego.RegisterLibFunc(&cgDataProviderCopyData, cg, "CGDataProviderCopyData") + purego.RegisterLibFunc(&cgImageRelease, cg, "CGImageRelease") + purego.RegisterLibFunc(&cfDataGetLength, cf, "CFDataGetLength") + purego.RegisterLibFunc(&cfDataGetBytePtr, cf, "CFDataGetBytePtr") + purego.RegisterLibFunc(&cfRelease, cf, "CFRelease") + + // Screen capture permission APIs (macOS 11+). Might not exist on older versions. + if sym, err := purego.Dlsym(cg, "CGPreflightScreenCaptureAccess"); err == nil { + purego.RegisterFunc(&cgPreflightScreenCaptureAccess, sym) + } + if sym, err := purego.Dlsym(cg, "CGRequestScreenCaptureAccess"); err == nil { + purego.RegisterFunc(&cgRequestScreenCaptureAccess, sym) + } + + darwinCaptureReady = true + }) +} + +// errFrameUnchanged signals that the raw capture bytes matched the previous +// frame, so the caller can skip the expensive BGRA to RGBA conversion. +var errFrameUnchanged = errors.New("frame unchanged") + +// CGCapturer captures the macOS main display using Core Graphics. +type CGCapturer struct { + displayID uint32 + w, h int + // downscale is 1 for pixel-perfect, 2 for Retina 2:1 box-filter downscale. + downscale int + hashSeed maphash.Seed + lastHash uint64 + hasHash bool +} + +// NewCGCapturer creates a screen capturer for the main display. +func NewCGCapturer() (*CGCapturer, error) { + initDarwinCapture() + if !darwinCaptureReady { + return nil, fmt.Errorf("CoreGraphics not available") + } + + // Request Screen Recording permission (shows system dialog on macOS 11+). + if cgPreflightScreenCaptureAccess != nil && !cgPreflightScreenCaptureAccess() { + if cgRequestScreenCaptureAccess != nil { + cgRequestScreenCaptureAccess() + } + openPrivacyPane("Privacy_ScreenCapture") + log.Warn("Screen Recording permission not granted. " + + "Opened System Settings > Privacy & Security > Screen Recording; enable netbird and restart.") + } + + displayID := cgMainDisplayID() + c := &CGCapturer{displayID: displayID, downscale: 1, hashSeed: maphash.MakeSeed()} + + // Probe actual pixel dimensions via a test capture. CGDisplayPixelsWide/High + // returns logical points on Retina, but CGDisplayCreateImage produces native + // pixels (often 2x), so probing the image is the only reliable source. + img, err := c.Capture() + if err != nil { + return nil, fmt.Errorf("probe capture: %w", err) + } + nativeW := img.Rect.Dx() + nativeH := img.Rect.Dy() + c.hasHash = false + if nativeW == 0 || nativeH == 0 { + return nil, errors.New("display dimensions are zero") + } + + logicalW := int(cgDisplayPixelsWide(displayID)) + logicalH := int(cgDisplayPixelsHigh(displayID)) + + // Enable 2:1 downscale on Retina unless explicitly disabled. Cuts pixel + // count 4x, shrinking convert, diff, and wire data proportionally. + if !retinaDownscaleDisabled() && nativeW >= 2*logicalW && nativeH >= 2*logicalH && nativeW%2 == 0 && nativeH%2 == 0 { + c.downscale = 2 + } + c.w = nativeW / c.downscale + c.h = nativeH / c.downscale + + log.Infof("macOS capturer ready: %dx%d (native %dx%d, logical %dx%d, downscale=%d, display=%d)", + c.w, c.h, nativeW, nativeH, logicalW, logicalH, c.downscale, displayID) + return c, nil +} + +func retinaDownscaleDisabled() bool { + v := os.Getenv(EnvVNCDisableDownscale) + if v == "" { + return false + } + disabled, err := strconv.ParseBool(v) + if err != nil { + log.Warnf("parse %s: %v", EnvVNCDisableDownscale, err) + return false + } + return disabled +} + +// Width returns the screen width. +func (c *CGCapturer) Width() int { return c.w } + +// Height returns the screen height. +func (c *CGCapturer) Height() int { return c.h } + +// Capture returns the current screen as an RGBA image. +func (c *CGCapturer) Capture() (*image.RGBA, error) { + cgImage := cgDisplayCreateImage(c.displayID) + if cgImage == 0 { + return nil, fmt.Errorf("CGDisplayCreateImage returned nil (screen recording permission?)") + } + defer cgImageRelease(cgImage) + + w := int(cgImageGetWidth(cgImage)) + h := int(cgImageGetHeight(cgImage)) + bytesPerRow := int(cgImageGetBytesPerRow(cgImage)) + bpp := int(cgImageGetBitsPerPixel(cgImage)) + + provider := cgImageGetDataProvider(cgImage) + if provider == 0 { + return nil, fmt.Errorf("CGImageGetDataProvider returned nil") + } + + cfData := cgDataProviderCopyData(provider) + if cfData == 0 { + return nil, fmt.Errorf("CGDataProviderCopyData returned nil") + } + defer cfRelease(cfData) + + dataLen := int(cfDataGetLength(cfData)) + dataPtr := cfDataGetBytePtr(cfData) + if dataPtr == 0 || dataLen == 0 { + return nil, fmt.Errorf("empty image data") + } + + src := unsafe.Slice((*byte)(unsafe.Pointer(dataPtr)), dataLen) + + hash := maphash.Bytes(c.hashSeed, src) + if c.hasHash && hash == c.lastHash { + return nil, errFrameUnchanged + } + c.lastHash = hash + c.hasHash = true + + ds := c.downscale + if ds < 1 { + ds = 1 + } + outW := w / ds + outH := h / ds + img := image.NewRGBA(image.Rect(0, 0, outW, outH)) + + bytesPerPixel := bpp / 8 + if bytesPerPixel == 4 && ds == 1 { + convertBGRAToRGBA(img.Pix, img.Stride, src, bytesPerRow, w, h) + } else if bytesPerPixel == 4 && ds == 2 { + convertBGRAToRGBADownscale2(img.Pix, img.Stride, src, bytesPerRow, outW, outH) + } else { + for row := 0; row < outH; row++ { + srcOff := row * ds * bytesPerRow + dstOff := row * img.Stride + for col := 0; col < outW; col++ { + si := srcOff + col*ds*bytesPerPixel + di := dstOff + col*4 + img.Pix[di+0] = src[si+2] + img.Pix[di+1] = src[si+1] + img.Pix[di+2] = src[si+0] + img.Pix[di+3] = 0xff + } + } + } + + return img, nil +} + +// convertBGRAToRGBADownscale2 averages every 2x2 BGRA block into one RGBA +// output pixel, parallelised across GOMAXPROCS cores. outW and outH are the +// destination dimensions (source is 2*outW by 2*outH). +func convertBGRAToRGBADownscale2(dst []byte, dstStride int, src []byte, srcStride, outW, outH int) { + workers := runtime.GOMAXPROCS(0) + if workers > outH { + workers = outH + } + if workers < 1 || outH < 32 { + workers = 1 + } + + convertRows := func(y0, y1 int) { + for row := y0; row < y1; row++ { + srcRow0 := 2 * row * srcStride + srcRow1 := srcRow0 + srcStride + dstOff := row * dstStride + for col := 0; col < outW; col++ { + s0 := srcRow0 + col*8 + s1 := srcRow1 + col*8 + b := (uint32(src[s0]) + uint32(src[s0+4]) + uint32(src[s1]) + uint32(src[s1+4])) >> 2 + g := (uint32(src[s0+1]) + uint32(src[s0+5]) + uint32(src[s1+1]) + uint32(src[s1+5])) >> 2 + r := (uint32(src[s0+2]) + uint32(src[s0+6]) + uint32(src[s1+2]) + uint32(src[s1+6])) >> 2 + di := dstOff + col*4 + dst[di+0] = byte(r) + dst[di+1] = byte(g) + dst[di+2] = byte(b) + dst[di+3] = 0xff + } + } + } + + if workers == 1 { + convertRows(0, outH) + return + } + + var wg sync.WaitGroup + chunk := (outH + workers - 1) / workers + for i := 0; i < workers; i++ { + y0 := i * chunk + y1 := y0 + chunk + if y1 > outH { + y1 = outH + } + if y0 >= y1 { + break + } + wg.Add(1) + go func(y0, y1 int) { + defer wg.Done() + convertRows(y0, y1) + }(y0, y1) + } + wg.Wait() +} + +// convertBGRAToRGBA swaps R/B channels using uint32 word operations, and +// parallelises across GOMAXPROCS cores for large images. +func convertBGRAToRGBA(dst []byte, dstStride int, src []byte, srcStride, w, h int) { + workers := runtime.GOMAXPROCS(0) + if workers > h { + workers = h + } + if workers < 1 || h < 64 { + workers = 1 + } + + convertRows := func(y0, y1 int) { + rowBytes := w * 4 + for row := y0; row < y1; row++ { + dstRow := dst[row*dstStride : row*dstStride+rowBytes] + srcRow := src[row*srcStride : row*srcStride+rowBytes] + dstU := unsafe.Slice((*uint32)(unsafe.Pointer(&dstRow[0])), w) + srcU := unsafe.Slice((*uint32)(unsafe.Pointer(&srcRow[0])), w) + for i, p := range srcU { + dstU[i] = (p & 0xff00ff00) | ((p & 0x000000ff) << 16) | ((p & 0x00ff0000) >> 16) | 0xff000000 + } + } + } + + if workers == 1 { + convertRows(0, h) + return + } + + var wg sync.WaitGroup + chunk := (h + workers - 1) / workers + for i := 0; i < workers; i++ { + y0 := i * chunk + y1 := y0 + chunk + if y1 > h { + y1 = h + } + if y0 >= y1 { + break + } + wg.Add(1) + go func(y0, y1 int) { + defer wg.Done() + convertRows(y0, y1) + }(y0, y1) + } + wg.Wait() +} + +// MacPoller wraps CGCapturer in a continuous capture loop. +type MacPoller struct { + mu sync.Mutex + frame *image.RGBA + w, h int + done chan struct{} + // wake shortens the init-retry backoff when a client is trying to connect, + // so granting Screen Recording mid-session takes effect immediately. + wake chan struct{} +} + +// NewMacPoller creates a capturer that continuously grabs the macOS display. +func NewMacPoller() *MacPoller { + p := &MacPoller{ + done: make(chan struct{}), + wake: make(chan struct{}, 1), + } + go p.loop() + return p +} + +// Wake pokes the init-retry loop so it doesn't wait out the full backoff +// before trying again. Safe to call from any goroutine; extra calls while a +// wake is pending are dropped. +func (p *MacPoller) Wake() { + select { + case p.wake <- struct{}{}: + default: + } +} + +// Close stops the capture loop. +func (p *MacPoller) Close() { + select { + case <-p.done: + default: + close(p.done) + } +} + +// Width returns the screen width. +func (p *MacPoller) Width() int { + p.mu.Lock() + defer p.mu.Unlock() + return p.w +} + +// Height returns the screen height. +func (p *MacPoller) Height() int { + p.mu.Lock() + defer p.mu.Unlock() + return p.h +} + +// Capture returns the most recent frame. +func (p *MacPoller) Capture() (*image.RGBA, error) { + p.mu.Lock() + img := p.frame + p.mu.Unlock() + if img != nil { + return img, nil + } + return nil, fmt.Errorf("no frame available yet") +} + +func (p *MacPoller) loop() { + var capturer *CGCapturer + var initFails int + + for { + select { + case <-p.done: + return + default: + } + + if capturer == nil { + var err error + capturer, err = NewCGCapturer() + if err != nil { + initFails++ + // Retry forever with backoff: the user may grant Screen + // Recording after the server started, and we need to pick it + // up whenever that happens. + delay := 2 * time.Second + if initFails > 15 { + delay = 30 * time.Second + } else if initFails > 5 { + delay = 10 * time.Second + } + if initFails == 1 || initFails%10 == 0 { + log.Warnf("macOS capturer: %v (attempt %d, retrying every %s)", err, initFails, delay) + } else { + log.Debugf("macOS capturer: %v (attempt %d)", err, initFails) + } + select { + case <-p.done: + return + case <-p.wake: + // Client is trying to connect, retry now. + case <-time.After(delay): + } + continue + } + initFails = 0 + p.mu.Lock() + p.w, p.h = capturer.Width(), capturer.Height() + p.mu.Unlock() + } + + img, err := capturer.Capture() + if errors.Is(err, errFrameUnchanged) { + select { + case <-p.done: + return + case <-time.After(33 * time.Millisecond): + } + continue + } + if err != nil { + log.Debugf("macOS capture: %v", err) + capturer = nil + select { + case <-p.done: + return + case <-time.After(500 * time.Millisecond): + } + continue + } + + p.mu.Lock() + p.frame = img + p.mu.Unlock() + + select { + case <-p.done: + return + case <-time.After(33 * time.Millisecond): // ~30 fps + } + } +} + +var _ ScreenCapturer = (*MacPoller)(nil) diff --git a/client/vnc/server/capture_dxgi_windows.go b/client/vnc/server/capture_dxgi_windows.go new file mode 100644 index 000000000..f64ec7158 --- /dev/null +++ b/client/vnc/server/capture_dxgi_windows.go @@ -0,0 +1,99 @@ +//go:build windows + +package server + +import ( + "errors" + "fmt" + "image" + + "github.com/kirides/go-d3d/d3d11" + "github.com/kirides/go-d3d/outputduplication" +) + +// dxgiCapturer captures the desktop using DXGI Desktop Duplication. +// Provides GPU-accelerated capture with native dirty rect tracking. +// Only works from the interactive user session, not Session 0. +// +// Uses a double-buffer: DXGI writes into img, then we copy to the current +// output buffer and hand it out. Alternating between two output buffers +// avoids allocating a new image.RGBA per frame (~8MB at 1080p, 30fps). +type dxgiCapturer struct { + dup *outputduplication.OutputDuplicator + device *d3d11.ID3D11Device + ctx *d3d11.ID3D11DeviceContext + img *image.RGBA + out [2]*image.RGBA + outIdx int + width int + height int +} + +func newDXGICapturer() (*dxgiCapturer, error) { + device, deviceCtx, err := d3d11.NewD3D11Device() + if err != nil { + return nil, fmt.Errorf("create D3D11 device: %w", err) + } + + dup, err := outputduplication.NewIDXGIOutputDuplication(device, deviceCtx, 0) + if err != nil { + device.Release() + deviceCtx.Release() + return nil, fmt.Errorf("create output duplication: %w", err) + } + + w, h := screenSize() + if w == 0 || h == 0 { + dup.Release() + device.Release() + deviceCtx.Release() + return nil, fmt.Errorf("screen dimensions are zero") + } + + rect := image.Rect(0, 0, w, h) + c := &dxgiCapturer{ + dup: dup, + device: device, + ctx: deviceCtx, + img: image.NewRGBA(rect), + out: [2]*image.RGBA{image.NewRGBA(rect), image.NewRGBA(rect)}, + width: w, + height: h, + } + + // Grab the initial frame with a longer timeout to ensure we have + // a valid image before returning. + _ = dup.GetImage(c.img, 2000) + + return c, nil +} + +func (c *dxgiCapturer) capture() (*image.RGBA, error) { + err := c.dup.GetImage(c.img, 100) + if err != nil && !errors.Is(err, outputduplication.ErrNoImageYet) { + return nil, err + } + + // Copy into the next output buffer. The DesktopCapturer hands out the + // returned pointer to VNC sessions that read pixels concurrently, so we + // alternate between two pre-allocated buffers instead of allocating per frame. + out := c.out[c.outIdx] + c.outIdx ^= 1 + copy(out.Pix, c.img.Pix) + return out, nil +} + +func (c *dxgiCapturer) close() { + if c.dup != nil { + c.dup.Release() + c.dup = nil + } + if c.ctx != nil { + c.ctx.Release() + c.ctx = nil + } + if c.device != nil { + c.device.Release() + c.device = nil + } +} diff --git a/client/vnc/server/capture_windows.go b/client/vnc/server/capture_windows.go new file mode 100644 index 000000000..8f8e95d4f --- /dev/null +++ b/client/vnc/server/capture_windows.go @@ -0,0 +1,461 @@ +//go:build windows + +package server + +import ( + "fmt" + "image" + "runtime" + "sync" + "sync/atomic" + "time" + "unsafe" + + log "github.com/sirupsen/logrus" + "golang.org/x/sys/windows" +) + +var ( + gdi32 = windows.NewLazySystemDLL("gdi32.dll") + user32 = windows.NewLazySystemDLL("user32.dll") + + procGetDC = user32.NewProc("GetDC") + procReleaseDC = user32.NewProc("ReleaseDC") + procCreateCompatDC = gdi32.NewProc("CreateCompatibleDC") + procCreateDIBSection = gdi32.NewProc("CreateDIBSection") + procSelectObject = gdi32.NewProc("SelectObject") + procDeleteObject = gdi32.NewProc("DeleteObject") + procDeleteDC = gdi32.NewProc("DeleteDC") + procBitBlt = gdi32.NewProc("BitBlt") + procGetSystemMetrics = user32.NewProc("GetSystemMetrics") + + // Desktop switching for service/Session 0 capture. + procOpenInputDesktop = user32.NewProc("OpenInputDesktop") + procSetThreadDesktop = user32.NewProc("SetThreadDesktop") + procCloseDesktop = user32.NewProc("CloseDesktop") + procOpenWindowStation = user32.NewProc("OpenWindowStationW") + procSetProcessWindowStation = user32.NewProc("SetProcessWindowStation") + procCloseWindowStation = user32.NewProc("CloseWindowStation") + procGetUserObjectInformationW = user32.NewProc("GetUserObjectInformationW") +) + +const uoiName = 2 + +const ( + smCxScreen = 0 + smCyScreen = 1 + srccopy = 0x00CC0020 + dibRgbColors = 0 +) + +type bitmapInfoHeader struct { + Size uint32 + Width int32 + Height int32 + Planes uint16 + BitCount uint16 + Compression uint32 + SizeImage uint32 + XPelsPerMeter int32 + YPelsPerMeter int32 + ClrUsed uint32 + ClrImportant uint32 +} + +type bitmapInfo struct { + Header bitmapInfoHeader +} + +// setupInteractiveWindowStation associates the current process with WinSta0, +// the interactive window station. This is required for a SYSTEM service in +// Session 0 to call OpenInputDesktop for screen capture and input injection. +func setupInteractiveWindowStation() error { + name, err := windows.UTF16PtrFromString("WinSta0") + if err != nil { + return fmt.Errorf("UTF16 WinSta0: %w", err) + } + hWinSta, _, err := procOpenWindowStation.Call( + uintptr(unsafe.Pointer(name)), + 0, + uintptr(windows.MAXIMUM_ALLOWED), + ) + if hWinSta == 0 { + return fmt.Errorf("OpenWindowStation(WinSta0): %w", err) + } + r, _, err := procSetProcessWindowStation.Call(hWinSta) + if r == 0 { + procCloseWindowStation.Call(hWinSta) + return fmt.Errorf("SetProcessWindowStation: %w", err) + } + log.Info("process window station set to WinSta0 (interactive)") + return nil +} + +func screenSize() (int, int) { + w, _, _ := procGetSystemMetrics.Call(uintptr(smCxScreen)) + h, _, _ := procGetSystemMetrics.Call(uintptr(smCyScreen)) + return int(w), int(h) +} + +func getDesktopName(hDesk uintptr) string { + var buf [256]uint16 + var needed uint32 + procGetUserObjectInformationW.Call(hDesk, uoiName, + uintptr(unsafe.Pointer(&buf[0])), 512, + uintptr(unsafe.Pointer(&needed))) + return windows.UTF16ToString(buf[:]) +} + +// switchToInputDesktop opens the desktop currently receiving user input +// and sets it as the calling OS thread's desktop. Must be called from a +// goroutine locked to its OS thread via runtime.LockOSThread(). +func switchToInputDesktop() (bool, string) { + hDesk, _, _ := procOpenInputDesktop.Call(0, 0, uintptr(windows.MAXIMUM_ALLOWED)) + if hDesk == 0 { + return false, "" + } + name := getDesktopName(hDesk) + ret, _, _ := procSetThreadDesktop.Call(hDesk) + procCloseDesktop.Call(hDesk) + return ret != 0, name +} + +// gdiCapturer captures the desktop screen using GDI BitBlt. +// GDI objects (DC, DIBSection) are allocated once and reused across frames. +type gdiCapturer struct { + mu sync.Mutex + width int + height int + + // Pre-allocated GDI resources, reused across captures. + memDC uintptr + bmp uintptr + bits uintptr +} + +func newGDICapturer() (*gdiCapturer, error) { + w, h := screenSize() + if w == 0 || h == 0 { + return nil, fmt.Errorf("screen dimensions are zero") + } + c := &gdiCapturer{width: w, height: h} + if err := c.allocGDI(); err != nil { + return nil, err + } + return c, nil +} + +// allocGDI pre-allocates the compatible DC and DIB section for reuse. +func (c *gdiCapturer) allocGDI() error { + screenDC, _, _ := procGetDC.Call(0) + if screenDC == 0 { + return fmt.Errorf("GetDC returned 0") + } + defer procReleaseDC.Call(0, screenDC) + + memDC, _, _ := procCreateCompatDC.Call(screenDC) + if memDC == 0 { + return fmt.Errorf("CreateCompatibleDC returned 0") + } + + bi := bitmapInfo{ + Header: bitmapInfoHeader{ + Size: uint32(unsafe.Sizeof(bitmapInfoHeader{})), + Width: int32(c.width), + Height: -int32(c.height), // negative = top-down DIB + Planes: 1, + BitCount: 32, + }, + } + + var bits uintptr + bmp, _, _ := procCreateDIBSection.Call( + screenDC, + uintptr(unsafe.Pointer(&bi)), + dibRgbColors, + uintptr(unsafe.Pointer(&bits)), + 0, 0, + ) + if bmp == 0 || bits == 0 { + procDeleteDC.Call(memDC) + return fmt.Errorf("CreateDIBSection returned 0") + } + + procSelectObject.Call(memDC, bmp) + + c.memDC = memDC + c.bmp = bmp + c.bits = bits + return nil +} + +func (c *gdiCapturer) close() { c.freeGDI() } + +// freeGDI releases pre-allocated GDI resources. +func (c *gdiCapturer) freeGDI() { + if c.bmp != 0 { + procDeleteObject.Call(c.bmp) + c.bmp = 0 + } + if c.memDC != 0 { + procDeleteDC.Call(c.memDC) + c.memDC = 0 + } + c.bits = 0 +} + +func (c *gdiCapturer) capture() (*image.RGBA, error) { + c.mu.Lock() + defer c.mu.Unlock() + + if c.memDC == 0 { + return nil, fmt.Errorf("GDI resources not allocated") + } + + screenDC, _, _ := procGetDC.Call(0) + if screenDC == 0 { + return nil, fmt.Errorf("GetDC returned 0") + } + defer procReleaseDC.Call(0, screenDC) + + ret, _, _ := procBitBlt.Call(c.memDC, 0, 0, uintptr(c.width), uintptr(c.height), + screenDC, 0, 0, srccopy) + if ret == 0 { + return nil, fmt.Errorf("BitBlt returned 0") + } + + n := c.width * c.height * 4 + raw := unsafe.Slice((*byte)(unsafe.Pointer(c.bits)), n) + + // GDI gives BGRA, the RFB encoder expects RGBA (img.Pix layout). + // Swap R and B in bulk using uint32 operations (one load + mask + shift + // per pixel instead of three separate byte assignments). + img := image.NewRGBA(image.Rect(0, 0, c.width, c.height)) + pix := img.Pix + copy(pix, raw) + swizzleBGRAtoRGBA(pix) + return img, nil +} + +// DesktopCapturer captures the interactive desktop, handling desktop transitions +// (login screen, UAC prompts). A dedicated OS-locked goroutine continuously +// captures frames, which are retrieved by the VNC session on demand. +// Capture pauses automatically when no clients are connected. +type DesktopCapturer struct { + mu sync.Mutex + frame *image.RGBA + w, h int + + // clients tracks the number of active VNC sessions. When zero, the + // capture loop idles instead of grabbing frames. + clients atomic.Int32 + + // wake is signaled when a client connects and the loop should resume. + wake chan struct{} + // done is closed when Close is called, terminating the capture loop. + done chan struct{} +} + +// NewDesktopCapturer creates a capturer that continuously grabs the active desktop. +func NewDesktopCapturer() *DesktopCapturer { + c := &DesktopCapturer{ + wake: make(chan struct{}, 1), + done: make(chan struct{}), + } + go c.loop() + return c +} + +// ClientConnect increments the active client count, resuming capture if needed. +func (c *DesktopCapturer) ClientConnect() { + c.clients.Add(1) + select { + case c.wake <- struct{}{}: + default: + } +} + +// ClientDisconnect decrements the active client count. +func (c *DesktopCapturer) ClientDisconnect() { + c.clients.Add(-1) +} + +// Close stops the capture loop and releases resources. +func (c *DesktopCapturer) Close() { + select { + case <-c.done: + default: + close(c.done) + } +} + +// Width returns the current screen width. +func (c *DesktopCapturer) Width() int { + c.mu.Lock() + defer c.mu.Unlock() + return c.w +} + +// Height returns the current screen height. +func (c *DesktopCapturer) Height() int { + c.mu.Lock() + defer c.mu.Unlock() + return c.h +} + +// Capture returns the most recent desktop frame. +func (c *DesktopCapturer) Capture() (*image.RGBA, error) { + c.mu.Lock() + img := c.frame + c.mu.Unlock() + if img != nil { + return img, nil + } + return nil, fmt.Errorf("no frame available yet") +} + +// waitForClient blocks until a client connects or the capturer is closed. +func (c *DesktopCapturer) waitForClient() bool { + if c.clients.Load() > 0 { + return true + } + select { + case <-c.wake: + return true + case <-c.done: + return false + } +} + +func (c *DesktopCapturer) loop() { + runtime.LockOSThread() + + // When running as a Windows service (Session 0), we need to attach to the + // interactive window station before OpenInputDesktop will succeed. + if err := setupInteractiveWindowStation(); err != nil { + log.Warnf("attach to interactive window station: %v", err) + } + + frameTicker := time.NewTicker(33 * time.Millisecond) // ~30 fps + defer frameTicker.Stop() + + retryTimer := time.NewTimer(0) + retryTimer.Stop() + defer retryTimer.Stop() + + type frameCapturer interface { + capture() (*image.RGBA, error) + close() + } + + var cap frameCapturer + var desktopFails int + var lastDesktop string + + createCapturer := func() (frameCapturer, error) { + dc, err := newDXGICapturer() + if err == nil { + log.Info("using DXGI Desktop Duplication for capture") + return dc, nil + } + log.Debugf("DXGI unavailable (%v), falling back to GDI", err) + gc, err := newGDICapturer() + if err != nil { + return nil, err + } + log.Info("using GDI BitBlt for capture") + return gc, nil + } + + for { + if !c.waitForClient() { + if cap != nil { + cap.close() + } + return + } + + // No clients: release the capturer and wait. + if c.clients.Load() <= 0 { + if cap != nil { + cap.close() + cap = nil + } + continue + } + + ok, desk := switchToInputDesktop() + if !ok { + desktopFails++ + if desktopFails == 1 || desktopFails%100 == 0 { + log.Warnf("switchToInputDesktop failed (count=%d), no interactive desktop session?", desktopFails) + } + retryTimer.Reset(100 * time.Millisecond) + select { + case <-retryTimer.C: + case <-c.done: + return + } + continue + } + if desktopFails > 0 { + log.Infof("switchToInputDesktop recovered after %d failures, desktop=%q", desktopFails, desk) + desktopFails = 0 + } + if desk != lastDesktop { + log.Infof("desktop changed: %q -> %q", lastDesktop, desk) + lastDesktop = desk + if cap != nil { + cap.close() + } + cap = nil + } + + if cap == nil { + fc, err := createCapturer() + if err != nil { + log.Warnf("create capturer: %v", err) + retryTimer.Reset(500 * time.Millisecond) + select { + case <-retryTimer.C: + case <-c.done: + return + } + continue + } + cap = fc + w, h := screenSize() + c.mu.Lock() + c.w, c.h = w, h + c.mu.Unlock() + log.Infof("screen capturer ready: %dx%d", w, h) + } + + img, err := cap.capture() + if err != nil { + log.Debugf("capture: %v", err) + cap.close() + cap = nil + retryTimer.Reset(100 * time.Millisecond) + select { + case <-retryTimer.C: + case <-c.done: + return + } + continue + } + + c.mu.Lock() + c.frame = img + c.mu.Unlock() + + select { + case <-frameTicker.C: + case <-c.done: + if cap != nil { + cap.close() + } + return + } + } +} diff --git a/client/vnc/server/capture_x11.go b/client/vnc/server/capture_x11.go new file mode 100644 index 000000000..f08b435cf --- /dev/null +++ b/client/vnc/server/capture_x11.go @@ -0,0 +1,385 @@ +//go:build (linux && !android) || freebsd + +package server + +import ( + "fmt" + "image" + "os" + "os/exec" + "strings" + "sync" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/jezek/xgb" + "github.com/jezek/xgb/xproto" +) + +// X11Capturer captures the screen from an X11 display using the MIT-SHM extension. +type X11Capturer struct { + mu sync.Mutex + conn *xgb.Conn + screen *xproto.ScreenInfo + w, h int + shmID int + shmAddr []byte + shmSeg uint32 // shm.Seg + useSHM bool +} + +// detectX11Display finds the active X11 display and sets DISPLAY/XAUTHORITY +// environment variables if needed. This is required when running as a system +// service where these vars aren't set. +func detectX11Display() { + if os.Getenv("DISPLAY") != "" { + return + } + + // Try /proc first (Linux), then ps fallback (FreeBSD and others). + if detectX11FromProc() { + return + } + if detectX11FromSockets() { + return + } +} + +// detectX11FromProc scans /proc/*/cmdline for Xorg (Linux). +func detectX11FromProc() bool { + entries, err := os.ReadDir("/proc") + if err != nil { + return false + } + for _, e := range entries { + if !e.IsDir() { + continue + } + cmdline, err := os.ReadFile("/proc/" + e.Name() + "/cmdline") + if err != nil { + continue + } + if display, auth := parseXorgArgs(splitCmdline(cmdline)); display != "" { + setDisplayEnv(display, auth) + return true + } + } + return false +} + +// detectX11FromSockets checks /tmp/.X11-unix/ for X sockets and uses ps +// to find the auth file. Works on FreeBSD and other systems without /proc. +func detectX11FromSockets() bool { + entries, err := os.ReadDir("/tmp/.X11-unix") + if err != nil { + return false + } + + // Find the lowest display number. + for _, e := range entries { + name := e.Name() + if len(name) < 2 || name[0] != 'X' { + continue + } + display := ":" + name[1:] + os.Setenv("DISPLAY", display) + log.Infof("auto-detected DISPLAY=%s (from socket)", display) + + // Try to find -auth from ps output. + if auth := findXorgAuthFromPS(); auth != "" { + os.Setenv("XAUTHORITY", auth) + log.Infof("auto-detected XAUTHORITY=%s (from ps)", auth) + } + return true + } + return false +} + +// findXorgAuthFromPS runs ps to find Xorg and extract its -auth argument. +func findXorgAuthFromPS() string { + out, err := exec.Command("ps", "auxww").Output() + if err != nil { + return "" + } + for _, line := range strings.Split(string(out), "\n") { + if !strings.Contains(line, "Xorg") && !strings.Contains(line, "/X ") { + continue + } + fields := strings.Fields(line) + for i, f := range fields { + if f == "-auth" && i+1 < len(fields) { + return fields[i+1] + } + } + } + return "" +} + +func parseXorgArgs(args []string) (display, auth string) { + if len(args) == 0 { + return "", "" + } + base := args[0] + if !(base == "Xorg" || base == "X" || len(base) > 0 && base[len(base)-1] == 'X' || + strings.Contains(base, "/Xorg") || strings.Contains(base, "/X")) { + return "", "" + } + for i, arg := range args[1:] { + if len(arg) > 0 && arg[0] == ':' { + display = arg + } + if arg == "-auth" && i+2 < len(args) { + auth = args[i+2] + } + } + return display, auth +} + +func setDisplayEnv(display, auth string) { + os.Setenv("DISPLAY", display) + log.Infof("auto-detected DISPLAY=%s", display) + if auth != "" { + os.Setenv("XAUTHORITY", auth) + log.Infof("auto-detected XAUTHORITY=%s", auth) + } +} + +func splitCmdline(data []byte) []string { + var args []string + for _, b := range splitNull(data) { + if len(b) > 0 { + args = append(args, string(b)) + } + } + return args +} + +func splitNull(data []byte) [][]byte { + var parts [][]byte + start := 0 + for i, b := range data { + if b == 0 { + parts = append(parts, data[start:i]) + start = i + 1 + } + } + if start < len(data) { + parts = append(parts, data[start:]) + } + return parts +} + +// NewX11Capturer connects to the X11 display and sets up shared memory capture. +func NewX11Capturer(display string) (*X11Capturer, error) { + detectX11Display() + + if display == "" { + display = os.Getenv("DISPLAY") + } + if display == "" { + return nil, fmt.Errorf("DISPLAY not set and no Xorg process found") + } + + conn, err := xgb.NewConnDisplay(display) + if err != nil { + return nil, fmt.Errorf("connect to X11 display %s: %w", display, err) + } + + setup := xproto.Setup(conn) + if len(setup.Roots) == 0 { + conn.Close() + return nil, fmt.Errorf("no X11 screens") + } + screen := setup.Roots[0] + + c := &X11Capturer{ + conn: conn, + screen: &screen, + w: int(screen.WidthInPixels), + h: int(screen.HeightInPixels), + } + + if err := c.initSHM(); err != nil { + log.Debugf("X11 SHM not available, using slow GetImage: %v", err) + } + + log.Infof("X11 capturer ready: %dx%d (display=%s, shm=%v)", c.w, c.h, display, c.useSHM) + return c, nil +} + +// initSHM is implemented in capture_x11_shm_linux.go (requires SysV SHM). +// On platforms without SysV SHM (FreeBSD), a stub returns an error and +// the capturer falls back to GetImage. + +// Width returns the screen width. +func (c *X11Capturer) Width() int { return c.w } + +// Height returns the screen height. +func (c *X11Capturer) Height() int { return c.h } + +// Capture returns the current screen as an RGBA image. +func (c *X11Capturer) Capture() (*image.RGBA, error) { + c.mu.Lock() + defer c.mu.Unlock() + + if c.useSHM { + return c.captureSHM() + } + return c.captureGetImage() +} + +// captureSHM is implemented in capture_x11_shm_linux.go. + +func (c *X11Capturer) captureGetImage() (*image.RGBA, error) { + cookie := xproto.GetImage(c.conn, xproto.ImageFormatZPixmap, + xproto.Drawable(c.screen.Root), + 0, 0, uint16(c.w), uint16(c.h), 0xFFFFFFFF) + + reply, err := cookie.Reply() + if err != nil { + return nil, fmt.Errorf("GetImage: %w", err) + } + + img := image.NewRGBA(image.Rect(0, 0, c.w, c.h)) + data := reply.Data + n := c.w * c.h * 4 + if len(data) < n { + return nil, fmt.Errorf("GetImage returned %d bytes, expected %d", len(data), n) + } + + for i := 0; i < n; i += 4 { + img.Pix[i+0] = data[i+2] // R + img.Pix[i+1] = data[i+1] // G + img.Pix[i+2] = data[i+0] // B + img.Pix[i+3] = 0xff + } + return img, nil +} + +// Close releases X11 resources. +func (c *X11Capturer) Close() { + c.closeSHM() + c.conn.Close() +} + +// closeSHM is implemented in capture_x11_shm_linux.go. + +// X11Poller wraps X11Capturer in a continuous capture loop, matching the +// DesktopCapturer pattern from Windows. +type X11Poller struct { + mu sync.Mutex + frame *image.RGBA + w, h int + display string + done chan struct{} +} + +// NewX11Poller creates a capturer that continuously grabs the X11 display. +func NewX11Poller(display string) *X11Poller { + p := &X11Poller{ + display: display, + done: make(chan struct{}), + } + go p.loop() + return p +} + +// Close stops the capture loop. +func (p *X11Poller) Close() { + select { + case <-p.done: + default: + close(p.done) + } +} + +// Width returns the screen width. +func (p *X11Poller) Width() int { + p.mu.Lock() + defer p.mu.Unlock() + return p.w +} + +// Height returns the screen height. +func (p *X11Poller) Height() int { + p.mu.Lock() + defer p.mu.Unlock() + return p.h +} + +// Capture returns the most recent frame. +func (p *X11Poller) Capture() (*image.RGBA, error) { + p.mu.Lock() + img := p.frame + p.mu.Unlock() + if img != nil { + return img, nil + } + return nil, fmt.Errorf("no frame available yet") +} + +func (p *X11Poller) loop() { + var capturer *X11Capturer + var initFails int + + defer func() { + if capturer != nil { + capturer.Close() + } + }() + + for { + select { + case <-p.done: + return + default: + } + + if capturer == nil { + var err error + capturer, err = NewX11Capturer(p.display) + if err != nil { + initFails++ + if initFails <= maxCapturerRetries { + log.Debugf("X11 capturer: %v (attempt %d/%d)", err, initFails, maxCapturerRetries) + select { + case <-p.done: + return + case <-time.After(2 * time.Second): + } + continue + } + log.Warnf("X11 capturer unavailable after %d attempts, stopping poller", maxCapturerRetries) + return + } + initFails = 0 + p.mu.Lock() + p.w, p.h = capturer.Width(), capturer.Height() + p.mu.Unlock() + } + + img, err := capturer.Capture() + if err != nil { + log.Debugf("X11 capture: %v", err) + capturer.Close() + capturer = nil + select { + case <-p.done: + return + case <-time.After(500 * time.Millisecond): + } + continue + } + + p.mu.Lock() + p.frame = img + p.mu.Unlock() + + select { + case <-p.done: + return + case <-time.After(33 * time.Millisecond): // ~30 fps + } + } +} diff --git a/client/vnc/server/capture_x11_shm_linux.go b/client/vnc/server/capture_x11_shm_linux.go new file mode 100644 index 000000000..4bf4366fc --- /dev/null +++ b/client/vnc/server/capture_x11_shm_linux.go @@ -0,0 +1,78 @@ +//go:build linux && !android + +package server + +import ( + "fmt" + "image" + + "github.com/jezek/xgb/shm" + "github.com/jezek/xgb/xproto" + "golang.org/x/sys/unix" +) + +func (c *X11Capturer) initSHM() error { + if err := shm.Init(c.conn); err != nil { + return fmt.Errorf("init SHM extension: %w", err) + } + + size := c.w * c.h * 4 + id, err := unix.SysvShmGet(unix.IPC_PRIVATE, size, unix.IPC_CREAT|0600) + if err != nil { + return fmt.Errorf("shmget: %w", err) + } + + addr, err := unix.SysvShmAttach(id, 0, 0) + if err != nil { + unix.SysvShmCtl(id, unix.IPC_RMID, nil) + return fmt.Errorf("shmat: %w", err) + } + + unix.SysvShmCtl(id, unix.IPC_RMID, nil) + + seg, err := shm.NewSegId(c.conn) + if err != nil { + unix.SysvShmDetach(addr) + return fmt.Errorf("new SHM seg: %w", err) + } + + if err := shm.AttachChecked(c.conn, seg, uint32(id), false).Check(); err != nil { + unix.SysvShmDetach(addr) + return fmt.Errorf("SHM attach to X: %w", err) + } + + c.shmID = id + c.shmAddr = addr + c.shmSeg = uint32(seg) + c.useSHM = true + return nil +} + +func (c *X11Capturer) captureSHM() (*image.RGBA, error) { + cookie := shm.GetImage(c.conn, xproto.Drawable(c.screen.Root), + 0, 0, uint16(c.w), uint16(c.h), 0xFFFFFFFF, + xproto.ImageFormatZPixmap, shm.Seg(c.shmSeg), 0) + + _, err := cookie.Reply() + if err != nil { + return nil, fmt.Errorf("SHM GetImage: %w", err) + } + + img := image.NewRGBA(image.Rect(0, 0, c.w, c.h)) + n := c.w * c.h * 4 + + for i := 0; i < n; i += 4 { + img.Pix[i+0] = c.shmAddr[i+2] // R + img.Pix[i+1] = c.shmAddr[i+1] // G + img.Pix[i+2] = c.shmAddr[i+0] // B + img.Pix[i+3] = 0xff + } + return img, nil +} + +func (c *X11Capturer) closeSHM() { + if c.useSHM { + shm.Detach(c.conn, shm.Seg(c.shmSeg)) + unix.SysvShmDetach(c.shmAddr) + } +} diff --git a/client/vnc/server/capture_x11_shm_stub.go b/client/vnc/server/capture_x11_shm_stub.go new file mode 100644 index 000000000..59abb0bc8 --- /dev/null +++ b/client/vnc/server/capture_x11_shm_stub.go @@ -0,0 +1,18 @@ +//go:build freebsd + +package server + +import ( + "fmt" + "image" +) + +func (c *X11Capturer) initSHM() error { + return fmt.Errorf("SysV SHM not available on this platform") +} + +func (c *X11Capturer) captureSHM() (*image.RGBA, error) { + return nil, fmt.Errorf("SHM capture not available on this platform") +} + +func (c *X11Capturer) closeSHM() {} diff --git a/client/vnc/server/crypto.go b/client/vnc/server/crypto.go new file mode 100644 index 000000000..e60a3d55f --- /dev/null +++ b/client/vnc/server/crypto.go @@ -0,0 +1,151 @@ +package server + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/ecdh" + "crypto/rand" + "encoding/base64" + "encoding/binary" + "fmt" + "io" + + "crypto/sha256" + + "golang.org/x/crypto/hkdf" +) + +const ( + aesKeySize = 32 // AES-256 + gcmNonceSize = 12 +) + +// recCrypto holds per-session encryption state. +type recCrypto struct { + gcm cipher.AEAD + frameCounter uint64 + // ephemeralPub is stored in the recording header so the admin can derive the same key. + ephemeralPub []byte +} + +// newRecCrypto sets up encryption for a new recording session. +// adminPubKeyB64 is the base64-encoded X25519 public key from management settings. +func newRecCrypto(adminPubKeyB64 string) (*recCrypto, error) { + adminPubBytes, err := base64.StdEncoding.DecodeString(adminPubKeyB64) + if err != nil { + return nil, fmt.Errorf("decode admin public key: %w", err) + } + + adminPub, err := ecdh.X25519().NewPublicKey(adminPubBytes) + if err != nil { + return nil, fmt.Errorf("parse admin X25519 public key: %w", err) + } + + // Generate ephemeral keypair + ephemeral, err := ecdh.X25519().GenerateKey(rand.Reader) + if err != nil { + return nil, fmt.Errorf("generate ephemeral key: %w", err) + } + + // ECDH shared secret + shared, err := ephemeral.ECDH(adminPub) + if err != nil { + return nil, fmt.Errorf("ECDH: %w", err) + } + + // Derive AES-256 key via HKDF + aesKey, err := deriveKey(shared, ephemeral.PublicKey().Bytes()) + if err != nil { + return nil, fmt.Errorf("derive key: %w", err) + } + + block, err := aes.NewCipher(aesKey) + if err != nil { + return nil, fmt.Errorf("create AES cipher: %w", err) + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("create GCM: %w", err) + } + + return &recCrypto{ + gcm: gcm, + ephemeralPub: ephemeral.PublicKey().Bytes(), + }, nil +} + +// encrypt encrypts plaintext using a counter-based nonce. Each call increments the counter. +func (c *recCrypto) encrypt(plaintext []byte) []byte { + nonce := make([]byte, gcmNonceSize) + binary.LittleEndian.PutUint64(nonce, c.frameCounter) + c.frameCounter++ + return c.gcm.Seal(nil, nonce, plaintext, nil) +} + +// DecryptRecording creates a decryptor from the admin's private key and the ephemeral public key from the header. +func DecryptRecording(adminPrivKeyB64 string, ephemeralPubB64 string) (*recDecryptor, error) { + adminPrivBytes, err := base64.StdEncoding.DecodeString(adminPrivKeyB64) + if err != nil { + return nil, fmt.Errorf("decode admin private key: %w", err) + } + + adminPriv, err := ecdh.X25519().NewPrivateKey(adminPrivBytes) + if err != nil { + return nil, fmt.Errorf("parse admin X25519 private key: %w", err) + } + + ephPubBytes, err := base64.StdEncoding.DecodeString(ephemeralPubB64) + if err != nil { + return nil, fmt.Errorf("decode ephemeral public key: %w", err) + } + + ephPub, err := ecdh.X25519().NewPublicKey(ephPubBytes) + if err != nil { + return nil, fmt.Errorf("parse ephemeral public key: %w", err) + } + + shared, err := adminPriv.ECDH(ephPub) + if err != nil { + return nil, fmt.Errorf("ECDH: %w", err) + } + + aesKey, err := deriveKey(shared, ephPubBytes) + if err != nil { + return nil, fmt.Errorf("derive key: %w", err) + } + + block, err := aes.NewCipher(aesKey) + if err != nil { + return nil, fmt.Errorf("create AES cipher: %w", err) + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("create GCM: %w", err) + } + + return &recDecryptor{gcm: gcm}, nil +} + +type recDecryptor struct { + gcm cipher.AEAD + frameCounter uint64 +} + +// Decrypt decrypts a frame. Must be called in the same order as encryption. +func (d *recDecryptor) Decrypt(ciphertext []byte) ([]byte, error) { + nonce := make([]byte, gcmNonceSize) + binary.LittleEndian.PutUint64(nonce, d.frameCounter) + d.frameCounter++ + return d.gcm.Open(nil, nonce, ciphertext, nil) +} + +func deriveKey(shared, ephemeralPub []byte) ([]byte, error) { + hkdfReader := hkdf.New(sha256.New, shared, ephemeralPub, []byte("netbird-recording")) + key := make([]byte, aesKeySize) + if _, err := io.ReadFull(hkdfReader, key); err != nil { + return nil, err + } + return key, nil +} diff --git a/client/vnc/server/crypto_test.go b/client/vnc/server/crypto_test.go new file mode 100644 index 000000000..058f7d106 --- /dev/null +++ b/client/vnc/server/crypto_test.go @@ -0,0 +1,129 @@ +package server + +import ( + "crypto/ecdh" + "crypto/rand" + "encoding/base64" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCryptoRoundtrip(t *testing.T) { + // Generate admin keypair + adminPriv, err := ecdh.X25519().GenerateKey(rand.Reader) + require.NoError(t, err) + + adminPubB64 := base64.StdEncoding.EncodeToString(adminPriv.PublicKey().Bytes()) + adminPrivB64 := base64.StdEncoding.EncodeToString(adminPriv.Bytes()) + + // Create encryptor (recording side) + enc, err := newRecCrypto(adminPubB64) + require.NoError(t, err) + assert.Len(t, enc.ephemeralPub, 32) + + ephPubB64 := base64.StdEncoding.EncodeToString(enc.ephemeralPub) + + // Encrypt some frames + plaintext1 := []byte("frame data one - PNG bytes would go here") + plaintext2 := []byte("frame data two - different content") + plaintext3 := make([]byte, 1024*100) // 100KB frame + rand.Read(plaintext3) + + ct1 := enc.encrypt(plaintext1) + ct2 := enc.encrypt(plaintext2) + ct3 := enc.encrypt(plaintext3) + + // Ciphertext should differ from plaintext + assert.NotEqual(t, plaintext1, ct1) + // Ciphertext is larger (GCM tag overhead) + assert.Greater(t, len(ct1), len(plaintext1)) + + // Create decryptor (playback side) + dec, err := DecryptRecording(adminPrivB64, ephPubB64) + require.NoError(t, err) + + // Decrypt in same order + got1, err := dec.Decrypt(ct1) + require.NoError(t, err) + assert.Equal(t, plaintext1, got1) + + got2, err := dec.Decrypt(ct2) + require.NoError(t, err) + assert.Equal(t, plaintext2, got2) + + got3, err := dec.Decrypt(ct3) + require.NoError(t, err) + assert.Equal(t, plaintext3, got3) +} + +func TestCryptoWrongKey(t *testing.T) { + // Admin key + adminPriv, err := ecdh.X25519().GenerateKey(rand.Reader) + require.NoError(t, err) + adminPubB64 := base64.StdEncoding.EncodeToString(adminPriv.PublicKey().Bytes()) + + // Encrypt with admin's public key + enc, err := newRecCrypto(adminPubB64) + require.NoError(t, err) + ephPubB64 := base64.StdEncoding.EncodeToString(enc.ephemeralPub) + + ct := enc.encrypt([]byte("secret frame data")) + + // Try to decrypt with a different private key + wrongPriv, err := ecdh.X25519().GenerateKey(rand.Reader) + require.NoError(t, err) + wrongPrivB64 := base64.StdEncoding.EncodeToString(wrongPriv.Bytes()) + + dec, err := DecryptRecording(wrongPrivB64, ephPubB64) + require.NoError(t, err) + + _, err = dec.Decrypt(ct) + assert.Error(t, err, "decryption with wrong key should fail") +} + +func TestCryptoInvalidKey(t *testing.T) { + _, err := newRecCrypto("") + assert.Error(t, err, "empty key should fail") + + _, err = newRecCrypto("not-base64!!!") + assert.Error(t, err, "invalid base64 should fail") + + _, err = newRecCrypto(base64.StdEncoding.EncodeToString([]byte("too-short"))) + assert.Error(t, err, "wrong-length key should fail") + + _, err = DecryptRecording("", "validbutirrelevant") + assert.Error(t, err, "empty private key should fail") + + _, err = DecryptRecording("not-base64!!!", base64.StdEncoding.EncodeToString(make([]byte, 32))) + assert.Error(t, err, "invalid base64 private key should fail") +} + +func TestCryptoOutOfOrderFails(t *testing.T) { + adminPriv, err := ecdh.X25519().GenerateKey(rand.Reader) + require.NoError(t, err) + adminPubB64 := base64.StdEncoding.EncodeToString(adminPriv.PublicKey().Bytes()) + adminPrivB64 := base64.StdEncoding.EncodeToString(adminPriv.Bytes()) + + enc, err := newRecCrypto(adminPubB64) + require.NoError(t, err) + ephPubB64 := base64.StdEncoding.EncodeToString(enc.ephemeralPub) + + ct0 := enc.encrypt([]byte("frame 0")) + ct1 := enc.encrypt([]byte("frame 1")) + + dec, err := DecryptRecording(adminPrivB64, ephPubB64) + require.NoError(t, err) + + // Skip frame 0, try to decrypt frame 1 first (wrong nonce) + _, err = dec.Decrypt(ct1) + assert.Error(t, err, "out-of-order decryption should fail due to nonce mismatch") + + // But frame 0 with a fresh decryptor should work + dec2, err := DecryptRecording(adminPrivB64, ephPubB64) + require.NoError(t, err) + got, err := dec2.Decrypt(ct0) + require.NoError(t, err) + assert.Equal(t, []byte("frame 0"), got) +} diff --git a/client/vnc/server/input_darwin.go b/client/vnc/server/input_darwin.go new file mode 100644 index 000000000..dd392406c --- /dev/null +++ b/client/vnc/server/input_darwin.go @@ -0,0 +1,540 @@ +//go:build darwin && !ios + +package server + +import ( + "fmt" + "os/exec" + "strings" + "sync" + + "github.com/ebitengine/purego" + log "github.com/sirupsen/logrus" +) + +// Core Graphics event constants. +const ( + kCGEventSourceStateCombinedSessionState int32 = 0 + + kCGEventLeftMouseDown int32 = 1 + kCGEventLeftMouseUp int32 = 2 + kCGEventRightMouseDown int32 = 3 + kCGEventRightMouseUp int32 = 4 + kCGEventMouseMoved int32 = 5 + kCGEventLeftMouseDragged int32 = 6 + kCGEventRightMouseDragged int32 = 7 + kCGEventKeyDown int32 = 10 + kCGEventKeyUp int32 = 11 + kCGEventOtherMouseDown int32 = 25 + kCGEventOtherMouseUp int32 = 26 + + kCGMouseButtonLeft int32 = 0 + kCGMouseButtonRight int32 = 1 + kCGMouseButtonCenter int32 = 2 + + kCGHIDEventTap int32 = 0 + + // IOKit power management constants. + kIOPMUserActiveLocal int32 = 0 + kIOPMAssertionLevelOn uint32 = 255 + kCFStringEncodingUTF8 uint32 = 0x08000100 +) + +var darwinInputOnce sync.Once + +var ( + cgEventSourceCreate func(int32) uintptr + cgEventCreateKeyboardEvent func(uintptr, uint16, bool) uintptr + // CGEventCreateMouseEvent takes CGPoint as two separate float64 args. + // purego can't handle array/struct types but individual float64s work. + cgEventCreateMouseEvent func(uintptr, int32, float64, float64, int32) uintptr + cgEventPost func(int32, uintptr) + + // CGEventCreateScrollWheelEvent is variadic, call via SyscallN. + cgEventCreateScrollWheelEventAddr uintptr + + axIsProcessTrusted func() bool + + // IOKit power-management bindings used to wake the display and inhibit + // idle sleep while a VNC client is driving input. + iopmAssertionDeclareUserActivity func(uintptr, int32, *uint32) int32 + iopmAssertionCreateWithName func(uintptr, uint32, uintptr, *uint32) int32 + iopmAssertionRelease func(uint32) int32 + cfStringCreateWithCString func(uintptr, string, uint32) uintptr + + // Cached CFStrings for assertion name and idle-sleep type. + pmAssertionNameCFStr uintptr + pmPreventIdleDisplayCFStr uintptr + + // Assertion IDs. userActivityID is reused across input events so repeated + // calls refresh the same assertion rather than create new ones. + pmMu sync.Mutex + userActivityID uint32 + preventSleepID uint32 + preventSleepHeld bool + + darwinInputReady bool + darwinEventSource uintptr +) + +func initDarwinInput() { + darwinInputOnce.Do(func() { + cg, err := purego.Dlopen("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics", purego.RTLD_NOW|purego.RTLD_GLOBAL) + if err != nil { + log.Debugf("load CoreGraphics for input: %v", err) + return + } + + purego.RegisterLibFunc(&cgEventSourceCreate, cg, "CGEventSourceCreate") + purego.RegisterLibFunc(&cgEventCreateKeyboardEvent, cg, "CGEventCreateKeyboardEvent") + purego.RegisterLibFunc(&cgEventCreateMouseEvent, cg, "CGEventCreateMouseEvent") + purego.RegisterLibFunc(&cgEventPost, cg, "CGEventPost") + + sym, err := purego.Dlsym(cg, "CGEventCreateScrollWheelEvent") + if err == nil { + cgEventCreateScrollWheelEventAddr = sym + } + + if ax, err := purego.Dlopen("/System/Library/Frameworks/ApplicationServices.framework/ApplicationServices", purego.RTLD_NOW|purego.RTLD_GLOBAL); err == nil { + if sym, err := purego.Dlsym(ax, "AXIsProcessTrusted"); err == nil { + purego.RegisterFunc(&axIsProcessTrusted, sym) + } + } + + initPowerAssertions() + + darwinInputReady = true + }) +} + +func initPowerAssertions() { + iokit, err := purego.Dlopen("/System/Library/Frameworks/IOKit.framework/IOKit", purego.RTLD_NOW|purego.RTLD_GLOBAL) + if err != nil { + log.Debugf("load IOKit: %v", err) + return + } + cf, err := purego.Dlopen("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation", purego.RTLD_NOW|purego.RTLD_GLOBAL) + if err != nil { + log.Debugf("load CoreFoundation for power assertions: %v", err) + return + } + + purego.RegisterLibFunc(&cfStringCreateWithCString, cf, "CFStringCreateWithCString") + purego.RegisterLibFunc(&iopmAssertionDeclareUserActivity, iokit, "IOPMAssertionDeclareUserActivity") + purego.RegisterLibFunc(&iopmAssertionCreateWithName, iokit, "IOPMAssertionCreateWithName") + purego.RegisterLibFunc(&iopmAssertionRelease, iokit, "IOPMAssertionRelease") + + pmAssertionNameCFStr = cfStringCreateWithCString(0, "NetBird VNC input", kCFStringEncodingUTF8) + pmPreventIdleDisplayCFStr = cfStringCreateWithCString(0, "PreventUserIdleDisplaySleep", kCFStringEncodingUTF8) +} + +// wakeDisplay declares user activity so macOS treats the synthesized input as +// real HID activity, waking the display if it is asleep. Called on every key +// and pointer event; the kernel coalesces repeated calls cheaply. +func wakeDisplay() { + if iopmAssertionDeclareUserActivity == nil || pmAssertionNameCFStr == 0 { + return + } + pmMu.Lock() + id := userActivityID + pmMu.Unlock() + r := iopmAssertionDeclareUserActivity(pmAssertionNameCFStr, kIOPMUserActiveLocal, &id) + if r != 0 { + log.Tracef("IOPMAssertionDeclareUserActivity returned %d", r) + return + } + pmMu.Lock() + userActivityID = id + pmMu.Unlock() +} + +// holdPreventIdleSleep creates an assertion that keeps the display from going +// idle-to-sleep while a VNC session is active. Safe to call repeatedly. +func holdPreventIdleSleep() { + if iopmAssertionCreateWithName == nil || pmPreventIdleDisplayCFStr == 0 || pmAssertionNameCFStr == 0 { + return + } + pmMu.Lock() + defer pmMu.Unlock() + if preventSleepHeld { + return + } + var id uint32 + r := iopmAssertionCreateWithName(pmPreventIdleDisplayCFStr, kIOPMAssertionLevelOn, pmAssertionNameCFStr, &id) + if r != 0 { + log.Debugf("IOPMAssertionCreateWithName returned %d", r) + return + } + preventSleepID = id + preventSleepHeld = true +} + +// releasePreventIdleSleep drops the idle-sleep assertion. +func releasePreventIdleSleep() { + if iopmAssertionRelease == nil { + return + } + pmMu.Lock() + defer pmMu.Unlock() + if !preventSleepHeld { + return + } + if r := iopmAssertionRelease(preventSleepID); r != 0 { + log.Debugf("IOPMAssertionRelease returned %d", r) + } + preventSleepHeld = false + preventSleepID = 0 +} + +func ensureEventSource() uintptr { + if darwinEventSource != 0 { + return darwinEventSource + } + darwinEventSource = cgEventSourceCreate(kCGEventSourceStateCombinedSessionState) + return darwinEventSource +} + +// MacInputInjector injects keyboard and mouse events via Core Graphics. +type MacInputInjector struct { + lastButtons uint8 + pbcopyPath string + pbpastePath string +} + +// NewMacInputInjector creates a macOS input injector. +func NewMacInputInjector() (*MacInputInjector, error) { + initDarwinInput() + if !darwinInputReady { + return nil, fmt.Errorf("CoreGraphics not available for input injection") + } + checkMacPermissions() + + m := &MacInputInjector{} + if path, err := exec.LookPath("pbcopy"); err == nil { + m.pbcopyPath = path + } + if path, err := exec.LookPath("pbpaste"); err == nil { + m.pbpastePath = path + } + if m.pbcopyPath == "" || m.pbpastePath == "" { + log.Debugf("clipboard tools not found (pbcopy=%q, pbpaste=%q)", m.pbcopyPath, m.pbpastePath) + } + + holdPreventIdleSleep() + + log.Info("macOS input injector ready") + return m, nil +} + +// checkMacPermissions warns and opens the Privacy pane if Accessibility is +// missing. Uses AXIsProcessTrusted which returns immediately; the previous +// osascript probe blocked for 120s (AppleEvent timeout) when access was +// denied, which delayed VNC server startup past client deadlines. +func checkMacPermissions() { + if axIsProcessTrusted != nil && !axIsProcessTrusted() { + openPrivacyPane("Privacy_Accessibility") + log.Warn("Accessibility permission not granted. Input injection will not work. " + + "Opened System Settings > Privacy & Security > Accessibility; enable netbird.") + } + + log.Info("Screen Recording permission is required for screen capture. " + + "If the screen appears black, grant in System Settings > Privacy & Security > Screen Recording.") +} + +// openPrivacyPane opens the given Privacy pane in System Settings so the user +// can toggle the permission without navigating manually. +func openPrivacyPane(pane string) { + url := "x-apple.systempreferences:com.apple.preference.security?" + pane + if err := exec.Command("open", url).Start(); err != nil { + log.Debugf("open privacy pane %s: %v", pane, err) + } +} + +// InjectKey simulates a key press or release. +func (m *MacInputInjector) InjectKey(keysym uint32, down bool) { + wakeDisplay() + src := ensureEventSource() + if src == 0 { + return + } + keycode := keysymToMacKeycode(keysym) + if keycode == 0xFFFF { + return + } + event := cgEventCreateKeyboardEvent(src, keycode, down) + if event == 0 { + return + } + cgEventPost(kCGHIDEventTap, event) + cfRelease(event) +} + +// InjectPointer simulates mouse movement and button events. +func (m *MacInputInjector) InjectPointer(buttonMask uint8, px, py, serverW, serverH int) { + wakeDisplay() + if serverW == 0 || serverH == 0 { + return + } + src := ensureEventSource() + if src == 0 { + return + } + + // Framebuffer is in physical pixels (Retina). CGEventCreateMouseEvent + // expects logical points, so scale down by the display's pixel/point ratio. + x := float64(px) + y := float64(py) + if cgDisplayPixelsWide != nil && cgMainDisplayID != nil { + displayID := cgMainDisplayID() + logicalW := int(cgDisplayPixelsWide(displayID)) + logicalH := int(cgDisplayPixelsHigh(displayID)) + if logicalW > 0 && logicalH > 0 { + x = float64(px) * float64(logicalW) / float64(serverW) + y = float64(py) * float64(logicalH) / float64(serverH) + } + } + leftDown := buttonMask&0x01 != 0 + rightDown := buttonMask&0x04 != 0 + middleDown := buttonMask&0x02 != 0 + scrollUp := buttonMask&0x08 != 0 + scrollDown := buttonMask&0x10 != 0 + + wasLeft := m.lastButtons&0x01 != 0 + wasRight := m.lastButtons&0x04 != 0 + wasMiddle := m.lastButtons&0x02 != 0 + + if leftDown { + m.postMouse(src, kCGEventLeftMouseDragged, x, y, kCGMouseButtonLeft) + } else if rightDown { + m.postMouse(src, kCGEventRightMouseDragged, x, y, kCGMouseButtonRight) + } else { + m.postMouse(src, kCGEventMouseMoved, x, y, kCGMouseButtonLeft) + } + + if leftDown && !wasLeft { + m.postMouse(src, kCGEventLeftMouseDown, x, y, kCGMouseButtonLeft) + } else if !leftDown && wasLeft { + m.postMouse(src, kCGEventLeftMouseUp, x, y, kCGMouseButtonLeft) + } + if rightDown && !wasRight { + m.postMouse(src, kCGEventRightMouseDown, x, y, kCGMouseButtonRight) + } else if !rightDown && wasRight { + m.postMouse(src, kCGEventRightMouseUp, x, y, kCGMouseButtonRight) + } + if middleDown && !wasMiddle { + m.postMouse(src, kCGEventOtherMouseDown, x, y, kCGMouseButtonCenter) + } else if !middleDown && wasMiddle { + m.postMouse(src, kCGEventOtherMouseUp, x, y, kCGMouseButtonCenter) + } + + if scrollUp { + m.postScroll(src, 3) + } + if scrollDown { + m.postScroll(src, -3) + } + + m.lastButtons = buttonMask +} + +func (m *MacInputInjector) postMouse(src uintptr, eventType int32, x, y float64, button int32) { + if cgEventCreateMouseEvent == nil { + return + } + event := cgEventCreateMouseEvent(src, eventType, x, y, button) + if event == 0 { + return + } + cgEventPost(kCGHIDEventTap, event) + cfRelease(event) +} + +func (m *MacInputInjector) postScroll(src uintptr, deltaY int32) { + if cgEventCreateScrollWheelEventAddr == 0 { + return + } + // CGEventCreateScrollWheelEvent(source, units, wheelCount, wheel1delta) + // units=0 (pixel), wheelCount=1, wheel1delta=deltaY + // Variadic C function: pass args as uintptr via SyscallN. + r1, _, _ := purego.SyscallN(cgEventCreateScrollWheelEventAddr, + src, 0, 1, uintptr(uint32(deltaY))) + if r1 == 0 { + return + } + cgEventPost(kCGHIDEventTap, r1) + cfRelease(r1) +} + +// SetClipboard sets the macOS clipboard using pbcopy. +func (m *MacInputInjector) SetClipboard(text string) { + if m.pbcopyPath == "" { + return + } + cmd := exec.Command(m.pbcopyPath) + cmd.Stdin = strings.NewReader(text) + if err := cmd.Run(); err != nil { + log.Tracef("set clipboard via pbcopy: %v", err) + } +} + +// GetClipboard reads the macOS clipboard using pbpaste. +func (m *MacInputInjector) GetClipboard() string { + if m.pbpastePath == "" { + return "" + } + out, err := exec.Command(m.pbpastePath).Output() + if err != nil { + log.Tracef("get clipboard via pbpaste: %v", err) + return "" + } + return string(out) +} + +// Close releases the idle-sleep assertion held for the injector's lifetime. +func (m *MacInputInjector) Close() { + releasePreventIdleSleep() +} + +func keysymToMacKeycode(keysym uint32) uint16 { + if keysym >= 0x61 && keysym <= 0x7a { + return asciiToMacKey[keysym-0x61] + } + if keysym >= 0x41 && keysym <= 0x5a { + return asciiToMacKey[keysym-0x41] + } + if keysym >= 0x30 && keysym <= 0x39 { + return digitToMacKey[keysym-0x30] + } + if code, ok := specialKeyMap[keysym]; ok { + return code + } + return 0xFFFF +} + +var asciiToMacKey = [26]uint16{ + 0x00, 0x0B, 0x08, 0x02, 0x0E, 0x03, 0x05, 0x04, + 0x22, 0x26, 0x28, 0x25, 0x2E, 0x2D, 0x1F, 0x23, + 0x0C, 0x0F, 0x01, 0x11, 0x20, 0x09, 0x0D, 0x07, + 0x10, 0x06, +} + +var digitToMacKey = [10]uint16{ + 0x1D, 0x12, 0x13, 0x14, 0x15, 0x17, 0x16, 0x1A, 0x1C, 0x19, +} + +var specialKeyMap = map[uint32]uint16{ + // Whitespace and editing + 0x0020: 0x31, // space + 0xff08: 0x33, // BackSpace + 0xff09: 0x30, // Tab + 0xff0d: 0x24, // Return + 0xff1b: 0x35, // Escape + 0xffff: 0x75, // Delete (forward) + + // Navigation + 0xff50: 0x73, // Home + 0xff51: 0x7B, // Left + 0xff52: 0x7E, // Up + 0xff53: 0x7C, // Right + 0xff54: 0x7D, // Down + 0xff55: 0x74, // Page_Up + 0xff56: 0x79, // Page_Down + 0xff57: 0x77, // End + 0xff63: 0x72, // Insert (Help on Mac) + + // Modifiers + 0xffe1: 0x38, // Shift_L + 0xffe2: 0x3C, // Shift_R + 0xffe3: 0x3B, // Control_L + 0xffe4: 0x3E, // Control_R + 0xffe5: 0x39, // Caps_Lock + 0xffe9: 0x3A, // Alt_L (Option) + 0xffea: 0x3D, // Alt_R (Option) + 0xffe7: 0x37, // Meta_L (Command) + 0xffe8: 0x36, // Meta_R (Command) + 0xffeb: 0x37, // Super_L (Command) - noVNC sends this + 0xffec: 0x36, // Super_R (Command) + + // Mode_switch / ISO_Level3_Shift (sent by noVNC for macOS Option remap) + 0xff7e: 0x3A, // Mode_switch -> Option + 0xfe03: 0x3D, // ISO_Level3_Shift -> Right Option + + // Function keys + 0xffbe: 0x7A, // F1 + 0xffbf: 0x78, // F2 + 0xffc0: 0x63, // F3 + 0xffc1: 0x76, // F4 + 0xffc2: 0x60, // F5 + 0xffc3: 0x61, // F6 + 0xffc4: 0x62, // F7 + 0xffc5: 0x64, // F8 + 0xffc6: 0x65, // F9 + 0xffc7: 0x6D, // F10 + 0xffc8: 0x67, // F11 + 0xffc9: 0x6F, // F12 + 0xffca: 0x69, // F13 + 0xffcb: 0x6B, // F14 + 0xffcc: 0x71, // F15 + 0xffcd: 0x6A, // F16 + 0xffce: 0x40, // F17 + 0xffcf: 0x4F, // F18 + 0xffd0: 0x50, // F19 + 0xffd1: 0x5A, // F20 + + // Punctuation (US keyboard layout, keysym = ASCII code) + 0x002d: 0x1B, // minus - + 0x003d: 0x18, // equal = + 0x005b: 0x21, // bracketleft [ + 0x005d: 0x1E, // bracketright ] + 0x005c: 0x2A, // backslash + 0x003b: 0x29, // semicolon ; + 0x0027: 0x27, // apostrophe ' + 0x0060: 0x32, // grave ` + 0x002c: 0x2B, // comma , + 0x002e: 0x2F, // period . + 0x002f: 0x2C, // slash / + + // Shifted punctuation (noVNC sends these as separate keysyms) + 0x005f: 0x1B, // underscore _ (shift+minus) + 0x002b: 0x18, // plus + (shift+equal) + 0x007b: 0x21, // braceleft { (shift+[) + 0x007d: 0x1E, // braceright } (shift+]) + 0x007c: 0x2A, // bar | (shift+\) + 0x003a: 0x29, // colon : (shift+;) + 0x0022: 0x27, // quotedbl " (shift+') + 0x007e: 0x32, // tilde ~ (shift+`) + 0x003c: 0x2B, // less < (shift+,) + 0x003e: 0x2F, // greater > (shift+.) + 0x003f: 0x2C, // question ? (shift+/) + 0x0021: 0x12, // exclam ! (shift+1) + 0x0040: 0x13, // at @ (shift+2) + 0x0023: 0x14, // numbersign # (shift+3) + 0x0024: 0x15, // dollar $ (shift+4) + 0x0025: 0x17, // percent % (shift+5) + 0x005e: 0x16, // asciicircum ^ (shift+6) + 0x0026: 0x1A, // ampersand & (shift+7) + 0x002a: 0x1C, // asterisk * (shift+8) + 0x0028: 0x19, // parenleft ( (shift+9) + 0x0029: 0x1D, // parenright ) (shift+0) + + // Numpad + 0xffb0: 0x52, // KP_0 + 0xffb1: 0x53, // KP_1 + 0xffb2: 0x54, // KP_2 + 0xffb3: 0x55, // KP_3 + 0xffb4: 0x56, // KP_4 + 0xffb5: 0x57, // KP_5 + 0xffb6: 0x58, // KP_6 + 0xffb7: 0x59, // KP_7 + 0xffb8: 0x5B, // KP_8 + 0xffb9: 0x5C, // KP_9 + 0xffae: 0x41, // KP_Decimal + 0xffaa: 0x43, // KP_Multiply + 0xffab: 0x45, // KP_Add + 0xffad: 0x4E, // KP_Subtract + 0xffaf: 0x4B, // KP_Divide + 0xff8d: 0x4C, // KP_Enter + 0xffbd: 0x51, // KP_Equal +} + +var _ InputInjector = (*MacInputInjector)(nil) diff --git a/client/vnc/server/input_windows.go b/client/vnc/server/input_windows.go new file mode 100644 index 000000000..4057ee0ec --- /dev/null +++ b/client/vnc/server/input_windows.go @@ -0,0 +1,398 @@ +//go:build windows + +package server + +import ( + "runtime" + "unsafe" + + log "github.com/sirupsen/logrus" + "golang.org/x/sys/windows" +) + +var ( + procOpenEventW = kernel32.NewProc("OpenEventW") + procSendInput = user32.NewProc("SendInput") + procVkKeyScanA = user32.NewProc("VkKeyScanA") +) + +const eventModifyState = 0x0002 + +const ( + inputMouse = 0 + inputKeyboard = 1 + + mouseeventfMove = 0x0001 + mouseeventfLeftDown = 0x0002 + mouseeventfLeftUp = 0x0004 + mouseeventfRightDown = 0x0008 + mouseeventfRightUp = 0x0010 + mouseeventfMiddleDown = 0x0020 + mouseeventfMiddleUp = 0x0040 + mouseeventfWheel = 0x0800 + mouseeventfAbsolute = 0x8000 + + wheelDelta = 120 + + keyeventfKeyUp = 0x0002 + keyeventfScanCode = 0x0008 +) + +type mouseInput struct { + Dx int32 + Dy int32 + MouseData uint32 + DwFlags uint32 + Time uint32 + DwExtraInfo uintptr +} + +type keybdInput struct { + WVk uint16 + WScan uint16 + DwFlags uint32 + Time uint32 + DwExtraInfo uintptr + _ [8]byte +} + +type inputUnion [32]byte + +type winInput struct { + Type uint32 + _ [4]byte + Data inputUnion +} + +func sendMouseInput(flags uint32, dx, dy int32, mouseData uint32) { + mi := mouseInput{ + Dx: dx, + Dy: dy, + MouseData: mouseData, + DwFlags: flags, + } + inp := winInput{Type: inputMouse} + copy(inp.Data[:], (*[unsafe.Sizeof(mi)]byte)(unsafe.Pointer(&mi))[:]) + r, _, err := procSendInput.Call(1, uintptr(unsafe.Pointer(&inp)), unsafe.Sizeof(inp)) + if r == 0 { + log.Tracef("SendInput(mouse flags=0x%x): %v", flags, err) + } +} + +func sendKeyInput(vk uint16, scanCode uint16, flags uint32) { + ki := keybdInput{ + WVk: vk, + WScan: scanCode, + DwFlags: flags, + } + inp := winInput{Type: inputKeyboard} + copy(inp.Data[:], (*[unsafe.Sizeof(ki)]byte)(unsafe.Pointer(&ki))[:]) + r, _, err := procSendInput.Call(1, uintptr(unsafe.Pointer(&inp)), unsafe.Sizeof(inp)) + if r == 0 { + log.Tracef("SendInput(key vk=0x%x): %v", vk, err) + } +} + +const sasEventName = `Global\NetBirdVNC_SAS` + +type inputCmd struct { + isKey bool + keysym uint32 + down bool + buttonMask uint8 + x, y int + serverW int + serverH int +} + +// WindowsInputInjector delivers input events from a dedicated OS thread that +// calls switchToInputDesktop before each injection. SendInput targets the +// calling thread's desktop, so the injection thread must be on the same +// desktop the user sees. +type WindowsInputInjector struct { + ch chan inputCmd + prevButtonMask uint8 + ctrlDown bool + altDown bool +} + +// NewWindowsInputInjector creates a desktop-aware input injector. +func NewWindowsInputInjector() *WindowsInputInjector { + w := &WindowsInputInjector{ch: make(chan inputCmd, 64)} + go w.loop() + return w +} + +func (w *WindowsInputInjector) loop() { + runtime.LockOSThread() + + for cmd := range w.ch { + // Switch to the current input desktop so SendInput reaches the right target. + switchToInputDesktop() + + if cmd.isKey { + w.doInjectKey(cmd.keysym, cmd.down) + } else { + w.doInjectPointer(cmd.buttonMask, cmd.x, cmd.y, cmd.serverW, cmd.serverH) + } + } +} + +// InjectKey queues a key event for injection on the input desktop thread. +func (w *WindowsInputInjector) InjectKey(keysym uint32, down bool) { + w.ch <- inputCmd{isKey: true, keysym: keysym, down: down} +} + +// InjectPointer queues a pointer event for injection on the input desktop thread. +func (w *WindowsInputInjector) InjectPointer(buttonMask uint8, x, y, serverW, serverH int) { + w.ch <- inputCmd{buttonMask: buttonMask, x: x, y: y, serverW: serverW, serverH: serverH} +} + +func (w *WindowsInputInjector) doInjectKey(keysym uint32, down bool) { + switch keysym { + case 0xffe3, 0xffe4: + w.ctrlDown = down + case 0xffe9, 0xffea: + w.altDown = down + } + + if (keysym == 0xff9f || keysym == 0xffff) && w.ctrlDown && w.altDown && down { + signalSAS() + return + } + + vk, _, extended := keysym2VK(keysym) + if vk == 0 { + return + } + var flags uint32 + if !down { + flags |= keyeventfKeyUp + } + if extended { + flags |= keyeventfScanCode + } + sendKeyInput(vk, 0, flags) +} + +// signalSAS signals the SAS named event. A listener in Session 0 +// (startSASListener) calls SendSAS to trigger the Secure Attention Sequence. +func signalSAS() { + namePtr, err := windows.UTF16PtrFromString(sasEventName) + if err != nil { + log.Warnf("SAS UTF16: %v", err) + return + } + h, _, lerr := procOpenEventW.Call( + uintptr(eventModifyState), + 0, + uintptr(unsafe.Pointer(namePtr)), + ) + if h == 0 { + log.Warnf("OpenEvent(%s): %v", sasEventName, lerr) + return + } + ev := windows.Handle(h) + defer windows.CloseHandle(ev) + if err := windows.SetEvent(ev); err != nil { + log.Warnf("SetEvent SAS: %v", err) + } else { + log.Info("SAS event signaled") + } +} + +func (w *WindowsInputInjector) doInjectPointer(buttonMask uint8, x, y, serverW, serverH int) { + if serverW == 0 || serverH == 0 { + return + } + + absX := int32(x * 65535 / serverW) + absY := int32(y * 65535 / serverH) + + sendMouseInput(mouseeventfMove|mouseeventfAbsolute, absX, absY, 0) + + changed := buttonMask ^ w.prevButtonMask + w.prevButtonMask = buttonMask + + type btnMap struct { + bit uint8 + down uint32 + up uint32 + } + buttons := [...]btnMap{ + {0x01, mouseeventfLeftDown, mouseeventfLeftUp}, + {0x02, mouseeventfMiddleDown, mouseeventfMiddleUp}, + {0x04, mouseeventfRightDown, mouseeventfRightUp}, + } + for _, b := range buttons { + if changed&b.bit == 0 { + continue + } + var flags uint32 + if buttonMask&b.bit != 0 { + flags = b.down + } else { + flags = b.up + } + sendMouseInput(flags|mouseeventfAbsolute, absX, absY, 0) + } + + negWheelDelta := ^uint32(wheelDelta - 1) + if changed&0x08 != 0 && buttonMask&0x08 != 0 { + sendMouseInput(mouseeventfWheel|mouseeventfAbsolute, absX, absY, wheelDelta) + } + if changed&0x10 != 0 && buttonMask&0x10 != 0 { + sendMouseInput(mouseeventfWheel|mouseeventfAbsolute, absX, absY, negWheelDelta) + } +} + +// keysym2VK converts an X11 keysym to a Windows virtual key code. +func keysym2VK(keysym uint32) (vk uint16, scan uint16, extended bool) { + if keysym >= 0x20 && keysym <= 0x7e { + r, _, _ := procVkKeyScanA.Call(uintptr(keysym)) + vk = uint16(r & 0xff) + return + } + + if keysym >= 0xffbe && keysym <= 0xffc9 { + vk = uint16(0x70 + keysym - 0xffbe) + return + } + + switch keysym { + case 0xff08: + vk = 0x08 // Backspace + case 0xff09: + vk = 0x09 // Tab + case 0xff0d: + vk = 0x0d // Return + case 0xff1b: + vk = 0x1b // Escape + case 0xff63: + vk, extended = 0x2d, true // Insert + case 0xff9f, 0xffff: + vk, extended = 0x2e, true // Delete + case 0xff50: + vk, extended = 0x24, true // Home + case 0xff57: + vk, extended = 0x23, true // End + case 0xff55: + vk, extended = 0x21, true // PageUp + case 0xff56: + vk, extended = 0x22, true // PageDown + case 0xff51: + vk, extended = 0x25, true // Left + case 0xff52: + vk, extended = 0x26, true // Up + case 0xff53: + vk, extended = 0x27, true // Right + case 0xff54: + vk, extended = 0x28, true // Down + case 0xffe1, 0xffe2: + vk = 0x10 // Shift + case 0xffe3, 0xffe4: + vk = 0x11 // Control + case 0xffe9, 0xffea: + vk = 0x12 // Alt + case 0xffe5: + vk = 0x14 // CapsLock + case 0xffe7, 0xffeb: + vk, extended = 0x5B, true // Meta_L / Super_L -> Left Windows + case 0xffe8, 0xffec: + vk, extended = 0x5C, true // Meta_R / Super_R -> Right Windows + case 0xff61: + vk = 0x2c // PrintScreen + case 0xff13: + vk = 0x13 // Pause + case 0xff14: + vk = 0x91 // ScrollLock + } + return +} + +var ( + procOpenClipboard = user32.NewProc("OpenClipboard") + procCloseClipboard = user32.NewProc("CloseClipboard") + procEmptyClipboard = user32.NewProc("EmptyClipboard") + procSetClipboardData = user32.NewProc("SetClipboardData") + procGetClipboardData = user32.NewProc("GetClipboardData") + procIsClipboardFormatAvailable = user32.NewProc("IsClipboardFormatAvailable") + + procGlobalAlloc = kernel32.NewProc("GlobalAlloc") + procGlobalLock = kernel32.NewProc("GlobalLock") + procGlobalUnlock = kernel32.NewProc("GlobalUnlock") +) + +const ( + cfUnicodeText = 13 + gmemMoveable = 0x0002 +) + +// SetClipboard sets the Windows clipboard to the given UTF-8 text. +func (w *WindowsInputInjector) SetClipboard(text string) { + utf16, err := windows.UTF16FromString(text) + if err != nil { + log.Tracef("clipboard UTF16 encode: %v", err) + return + } + + size := uintptr(len(utf16) * 2) + hMem, _, _ := procGlobalAlloc.Call(gmemMoveable, size) + if hMem == 0 { + log.Tracef("GlobalAlloc for clipboard: allocation returned nil") + return + } + + ptr, _, _ := procGlobalLock.Call(hMem) + if ptr == 0 { + log.Tracef("GlobalLock for clipboard: lock returned nil") + return + } + copy(unsafe.Slice((*uint16)(unsafe.Pointer(ptr)), len(utf16)), utf16) + procGlobalUnlock.Call(hMem) + + r, _, lerr := procOpenClipboard.Call(0) + if r == 0 { + log.Tracef("OpenClipboard: %v", lerr) + return + } + defer procCloseClipboard.Call() + + procEmptyClipboard.Call() + r, _, lerr = procSetClipboardData.Call(cfUnicodeText, hMem) + if r == 0 { + log.Tracef("SetClipboardData: %v", lerr) + } +} + +// GetClipboard reads the Windows clipboard as UTF-8 text. +func (w *WindowsInputInjector) GetClipboard() string { + r, _, _ := procIsClipboardFormatAvailable.Call(cfUnicodeText) + if r == 0 { + return "" + } + + r, _, lerr := procOpenClipboard.Call(0) + if r == 0 { + log.Tracef("OpenClipboard for read: %v", lerr) + return "" + } + defer procCloseClipboard.Call() + + hData, _, _ := procGetClipboardData.Call(cfUnicodeText) + if hData == 0 { + return "" + } + + ptr, _, _ := procGlobalLock.Call(hData) + if ptr == 0 { + return "" + } + defer procGlobalUnlock.Call(hData) + + return windows.UTF16PtrToString((*uint16)(unsafe.Pointer(ptr))) +} + +var _ InputInjector = (*WindowsInputInjector)(nil) + +var _ ScreenCapturer = (*DesktopCapturer)(nil) diff --git a/client/vnc/server/input_x11.go b/client/vnc/server/input_x11.go new file mode 100644 index 000000000..9d80532d5 --- /dev/null +++ b/client/vnc/server/input_x11.go @@ -0,0 +1,242 @@ +//go:build (linux && !android) || freebsd + +package server + +import ( + "fmt" + "os" + "os/exec" + "strings" + + log "github.com/sirupsen/logrus" + + "github.com/jezek/xgb" + "github.com/jezek/xgb/xproto" + "github.com/jezek/xgb/xtest" +) + +// X11InputInjector injects keyboard and mouse events via the XTest extension. +type X11InputInjector struct { + conn *xgb.Conn + root xproto.Window + screen *xproto.ScreenInfo + display string + keysymMap map[uint32]byte + lastButtons uint8 + clipboardTool string + clipboardToolName string +} + +// NewX11InputInjector connects to the X11 display and initializes XTest. +func NewX11InputInjector(display string) (*X11InputInjector, error) { + detectX11Display() + + if display == "" { + display = os.Getenv("DISPLAY") + } + if display == "" { + return nil, fmt.Errorf("DISPLAY not set and no Xorg process found") + } + + conn, err := xgb.NewConnDisplay(display) + if err != nil { + return nil, fmt.Errorf("connect to X11 display %s: %w", display, err) + } + + if err := xtest.Init(conn); err != nil { + conn.Close() + return nil, fmt.Errorf("init XTest extension: %w", err) + } + + setup := xproto.Setup(conn) + if len(setup.Roots) == 0 { + conn.Close() + return nil, fmt.Errorf("no X11 screens") + } + screen := setup.Roots[0] + + inj := &X11InputInjector{ + conn: conn, + root: screen.Root, + screen: &screen, + display: display, + } + inj.cacheKeyboardMapping() + inj.resolveClipboardTool() + + log.Infof("X11 input injector ready (display=%s)", display) + return inj, nil +} + +// InjectKey simulates a key press or release. keysym is an X11 KeySym. +func (x *X11InputInjector) InjectKey(keysym uint32, down bool) { + keycode := x.keysymToKeycode(keysym) + if keycode == 0 { + return + } + + var eventType byte + if down { + eventType = xproto.KeyPress + } else { + eventType = xproto.KeyRelease + } + + xtest.FakeInput(x.conn, eventType, keycode, 0, x.root, 0, 0, 0) +} + +// InjectPointer simulates mouse movement and button events. +func (x *X11InputInjector) InjectPointer(buttonMask uint8, px, py, serverW, serverH int) { + if serverW == 0 || serverH == 0 { + return + } + + // Scale to actual screen coordinates. + screenW := int(x.screen.WidthInPixels) + screenH := int(x.screen.HeightInPixels) + absX := px * screenW / serverW + absY := py * screenH / serverH + + // Move pointer. + xtest.FakeInput(x.conn, xproto.MotionNotify, 0, 0, x.root, int16(absX), int16(absY), 0) + + // Handle button events. RFB button mask: bit0=left, bit1=middle, bit2=right, + // bit3=scrollUp, bit4=scrollDown. X11 buttons: 1=left, 2=middle, 3=right, + // 4=scrollUp, 5=scrollDown. + type btnMap struct { + rfbBit uint8 + x11Btn byte + } + buttons := [...]btnMap{ + {0x01, 1}, // left + {0x02, 2}, // middle + {0x04, 3}, // right + {0x08, 4}, // scroll up + {0x10, 5}, // scroll down + } + + for _, b := range buttons { + pressed := buttonMask&b.rfbBit != 0 + wasPressed := x.lastButtons&b.rfbBit != 0 + if b.x11Btn >= 4 { + // Scroll: send press+release on each scroll event. + if pressed { + xtest.FakeInput(x.conn, xproto.ButtonPress, b.x11Btn, 0, x.root, 0, 0, 0) + xtest.FakeInput(x.conn, xproto.ButtonRelease, b.x11Btn, 0, x.root, 0, 0, 0) + } + } else { + if pressed && !wasPressed { + xtest.FakeInput(x.conn, xproto.ButtonPress, b.x11Btn, 0, x.root, 0, 0, 0) + } else if !pressed && wasPressed { + xtest.FakeInput(x.conn, xproto.ButtonRelease, b.x11Btn, 0, x.root, 0, 0, 0) + } + } + } + x.lastButtons = buttonMask +} + +// cacheKeyboardMapping fetches the X11 keyboard mapping once and stores it +// as a keysym-to-keycode map, avoiding a round-trip per keystroke. +func (x *X11InputInjector) cacheKeyboardMapping() { + setup := xproto.Setup(x.conn) + minKeycode := setup.MinKeycode + maxKeycode := setup.MaxKeycode + + reply, err := xproto.GetKeyboardMapping(x.conn, minKeycode, + byte(maxKeycode-minKeycode+1)).Reply() + if err != nil { + log.Debugf("cache keyboard mapping: %v", err) + x.keysymMap = make(map[uint32]byte) + return + } + + m := make(map[uint32]byte, int(maxKeycode-minKeycode+1)*int(reply.KeysymsPerKeycode)) + keysymsPerKeycode := int(reply.KeysymsPerKeycode) + for i := int(minKeycode); i <= int(maxKeycode); i++ { + offset := (i - int(minKeycode)) * keysymsPerKeycode + for j := 0; j < keysymsPerKeycode; j++ { + ks := uint32(reply.Keysyms[offset+j]) + if ks != 0 { + if _, exists := m[ks]; !exists { + m[ks] = byte(i) + } + } + } + } + x.keysymMap = m +} + +// keysymToKeycode looks up a cached keysym-to-keycode mapping. +// Returns 0 if the keysym is not mapped. +func (x *X11InputInjector) keysymToKeycode(keysym uint32) byte { + return x.keysymMap[keysym] +} + +// SetClipboard sets the X11 clipboard using xclip or xsel. +func (x *X11InputInjector) SetClipboard(text string) { + if x.clipboardTool == "" { + return + } + + var cmd *exec.Cmd + if x.clipboardToolName == "xclip" { + cmd = exec.Command(x.clipboardTool, "-selection", "clipboard") + } else { + cmd = exec.Command(x.clipboardTool, "--clipboard", "--input") + } + cmd.Env = x.clipboardEnv() + cmd.Stdin = strings.NewReader(text) + if err := cmd.Run(); err != nil { + log.Debugf("set clipboard via %s: %v", x.clipboardToolName, err) + } +} + +func (x *X11InputInjector) resolveClipboardTool() { + for _, name := range []string{"xclip", "xsel"} { + path, err := exec.LookPath(name) + if err == nil { + x.clipboardTool = path + x.clipboardToolName = name + log.Debugf("clipboard tool resolved to %s", path) + return + } + } + log.Debugf("no clipboard tool (xclip/xsel) found, clipboard sync disabled") +} + +// GetClipboard reads the X11 clipboard using xclip or xsel. +func (x *X11InputInjector) GetClipboard() string { + if x.clipboardTool == "" { + return "" + } + + var cmd *exec.Cmd + if x.clipboardToolName == "xclip" { + cmd = exec.Command(x.clipboardTool, "-selection", "clipboard", "-o") + } else { + cmd = exec.Command(x.clipboardTool, "--clipboard", "--output") + } + cmd.Env = x.clipboardEnv() + out, err := cmd.Output() + if err != nil { + log.Tracef("get clipboard via %s: %v", x.clipboardToolName, err) + return "" + } + return string(out) +} + +func (x *X11InputInjector) clipboardEnv() []string { + env := []string{"DISPLAY=" + x.display} + if auth := os.Getenv("XAUTHORITY"); auth != "" { + env = append(env, "XAUTHORITY="+auth) + } + return env +} + +// Close releases X11 resources. +func (x *X11InputInjector) Close() { + x.conn.Close() +} + +var _ InputInjector = (*X11InputInjector)(nil) +var _ ScreenCapturer = (*X11Poller)(nil) diff --git a/client/vnc/server/recorder.go b/client/vnc/server/recorder.go new file mode 100644 index 000000000..4b71813c6 --- /dev/null +++ b/client/vnc/server/recorder.go @@ -0,0 +1,175 @@ +package server + +import ( + "bytes" + "encoding/base64" + "encoding/binary" + "encoding/json" + "fmt" + "image" + "image/png" + "os" + "path/filepath" + "sync" + "time" + + log "github.com/sirupsen/logrus" +) + +// Recording file format: +// +// Header: magic(6) + width(2) + height(2) + startTime(8) + metaLen(4) + metaJSON +// Frames: offsetMs(4) + pngLen(4) + PNG image data +// +// Each frame is a PNG-encoded screenshot. Only changed frames are stored. +const recMagic = "NBVNC\x01" + +// RecordingMeta holds metadata written to the recording file header. +type RecordingMeta struct { + User string `json:"user,omitempty"` + RemoteAddr string `json:"remote_addr"` + JWTUser string `json:"jwt_user,omitempty"` + Mode string `json:"mode,omitempty"` + Encrypted bool `json:"encrypted,omitempty"` + EphemeralKey string `json:"ephemeral_key,omitempty"` +} + +// vncRecorder writes VNC session frames to a recording file. +type vncRecorder struct { + mu sync.Mutex + file *os.File + startTime time.Time + closed bool + log *log.Entry + prevFrame *image.RGBA + pngEnc *png.Encoder + pngBuf bytes.Buffer + crypto *recCrypto +} + +func newVNCRecorder(dir string, width, height int, meta *RecordingMeta, encryptionKey string, logger *log.Entry) (*vncRecorder, error) { + if err := os.MkdirAll(dir, 0700); err != nil { + return nil, fmt.Errorf("create recording dir: %w", err) + } + + now := time.Now().UTC() + filename := fmt.Sprintf("%s_vnc.rec", now.Format("20060102-150405")) + filePath := filepath.Join(dir, filename) + + f, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0600) + if err != nil { + return nil, fmt.Errorf("create recording file: %w", err) + } + + var crypto *recCrypto + if encryptionKey != "" { + var cryptoErr error + crypto, cryptoErr = newRecCrypto(encryptionKey) + if cryptoErr != nil { + f.Close() + os.Remove(filePath) + return nil, fmt.Errorf("init encryption: %w", cryptoErr) + } + meta.Encrypted = true + meta.EphemeralKey = base64.StdEncoding.EncodeToString(crypto.ephemeralPub) + } + + metaJSON, err := json.Marshal(meta) + if err != nil { + f.Close() + os.Remove(filePath) + return nil, fmt.Errorf("marshal meta: %w", err) + } + + var hdr [6 + 2 + 2 + 8 + 4]byte + copy(hdr[:6], recMagic) + binary.BigEndian.PutUint16(hdr[6:8], uint16(width)) + binary.BigEndian.PutUint16(hdr[8:10], uint16(height)) + binary.BigEndian.PutUint64(hdr[10:18], uint64(now.UnixMilli())) + binary.BigEndian.PutUint32(hdr[18:22], uint32(len(metaJSON))) + + if _, err := f.Write(hdr[:]); err != nil { + f.Close() + os.Remove(filePath) + return nil, fmt.Errorf("write header: %w", err) + } + if _, err := f.Write(metaJSON); err != nil { + f.Close() + os.Remove(filePath) + return nil, fmt.Errorf("write meta: %w", err) + } + + r := &vncRecorder{ + file: f, + startTime: now, + log: logger.WithField("recording", filepath.Base(filePath)), + pngEnc: &png.Encoder{CompressionLevel: png.BestSpeed}, + crypto: crypto, + } + if crypto != nil { + r.log.Infof("VNC recording started (encrypted): %s", filePath) + } else { + r.log.Infof("VNC recording started: %s", filePath) + } + return r, nil +} + +// writeFrame records a screen frame. Only writes if the frame differs from the previous one. +func (r *vncRecorder) writeFrame(img *image.RGBA) { + r.mu.Lock() + defer r.mu.Unlock() + + if r.closed { + return + } + + if r.prevFrame != nil && bytes.Equal(r.prevFrame.Pix, img.Pix) { + return + } + + offsetMs := uint32(time.Since(r.startTime).Milliseconds()) + + r.pngBuf.Reset() + if err := r.pngEnc.Encode(&r.pngBuf, img); err != nil { + r.log.Debugf("encode PNG frame: %v", err) + return + } + + frameData := r.pngBuf.Bytes() + if r.crypto != nil { + frameData = r.crypto.encrypt(frameData) + } + + var frameHdr [8]byte + binary.BigEndian.PutUint32(frameHdr[0:4], offsetMs) + binary.BigEndian.PutUint32(frameHdr[4:8], uint32(len(frameData))) + + if _, err := r.file.Write(frameHdr[:]); err != nil { + r.log.Debugf("write frame header: %v", err) + return + } + if _, err := r.file.Write(frameData); err != nil { + r.log.Debugf("write frame data: %v", err) + return + } + + if r.prevFrame == nil { + r.prevFrame = image.NewRGBA(img.Rect) + } + copy(r.prevFrame.Pix, img.Pix) +} + +func (r *vncRecorder) close() { + r.mu.Lock() + defer r.mu.Unlock() + + if r.closed { + return + } + r.closed = true + + duration := time.Since(r.startTime) + r.log.Infof("VNC recording stopped after %v", duration.Round(time.Millisecond)) + r.file.Close() +} + diff --git a/client/vnc/server/recorder_test.go b/client/vnc/server/recorder_test.go new file mode 100644 index 000000000..67e21dfd4 --- /dev/null +++ b/client/vnc/server/recorder_test.go @@ -0,0 +1,202 @@ +package server + +import ( + "crypto/ecdh" + "crypto/rand" + "encoding/base64" + "image" + "image/color" + "os" + "path/filepath" + "testing" + + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func makeTestImage(w, h int, c color.RGBA) *image.RGBA { + img := image.NewRGBA(image.Rect(0, 0, w, h)) + for i := 0; i < len(img.Pix); i += 4 { + img.Pix[i] = c.R + img.Pix[i+1] = c.G + img.Pix[i+2] = c.B + img.Pix[i+3] = c.A + } + return img +} + +func TestRecorderWriteAndReadHeader(t *testing.T) { + dir := t.TempDir() + logger := log.WithField("test", t.Name()) + + meta := &RecordingMeta{ + User: "alice", + RemoteAddr: "100.0.1.5:12345", + JWTUser: "google|123", + Mode: "session", + } + + rec, err := newVNCRecorder(dir, 800, 600, meta, "", logger) + require.NoError(t, err) + + // Write some frames + red := makeTestImage(800, 600, color.RGBA{255, 0, 0, 255}) + blue := makeTestImage(800, 600, color.RGBA{0, 0, 255, 255}) + + rec.writeFrame(red) + rec.writeFrame(red) // duplicate, should be skipped + rec.writeFrame(blue) + rec.close() + + // Read back the header + files, err := os.ReadDir(dir) + require.NoError(t, err) + require.Len(t, files, 1) + + filePath := filepath.Join(dir, files[0].Name()) + header, err := ReadRecordingHeader(filePath) + require.NoError(t, err) + + assert.Equal(t, 800, header.Width) + assert.Equal(t, 600, header.Height) + assert.Equal(t, "alice", header.Meta.User) + assert.Equal(t, "100.0.1.5:12345", header.Meta.RemoteAddr) + assert.Equal(t, "google|123", header.Meta.JWTUser) + assert.Equal(t, "session", header.Meta.Mode) + assert.False(t, header.Meta.Encrypted) + + // Verify file is valid by checking size is reasonable + fi, err := os.Stat(filePath) + require.NoError(t, err) + assert.Greater(t, fi.Size(), int64(100), "recording should have content") +} + +func TestRecorderDuplicateFrameSkip(t *testing.T) { + dir := t.TempDir() + logger := log.WithField("test", t.Name()) + + rec, err := newVNCRecorder(dir, 100, 100, &RecordingMeta{RemoteAddr: "test"}, "", logger) + require.NoError(t, err) + + img := makeTestImage(100, 100, color.RGBA{128, 128, 128, 255}) + + rec.writeFrame(img) + rec.writeFrame(img) // duplicate + rec.writeFrame(img) // duplicate + rec.close() + + files, _ := os.ReadDir(dir) + filePath := filepath.Join(dir, files[0].Name()) + + // Count frames by parsing + f, err := os.Open(filePath) + require.NoError(t, err) + defer f.Close() + + _, err = parseRecHeader(f) + require.NoError(t, err) + + frameCount := 0 + var hdr [8]byte + for { + if _, err := f.Read(hdr[:]); err != nil { + break + } + pngLen := int64(hdr[4])<<24 | int64(hdr[5])<<16 | int64(hdr[6])<<8 | int64(hdr[7]) + f.Seek(pngLen, 1) + frameCount++ + } + + assert.Equal(t, 1, frameCount, "duplicate frames should be skipped") +} + +func TestRecorderEncrypted(t *testing.T) { + dir := t.TempDir() + logger := log.WithField("test", t.Name()) + + // Generate admin keypair + adminPriv, err := ecdh.X25519().GenerateKey(rand.Reader) + require.NoError(t, err) + adminPubB64 := base64.StdEncoding.EncodeToString(adminPriv.PublicKey().Bytes()) + + meta := &RecordingMeta{ + RemoteAddr: "100.0.1.5:12345", + Mode: "attach", + } + + rec, err := newVNCRecorder(dir, 200, 150, meta, adminPubB64, logger) + require.NoError(t, err) + + img := makeTestImage(200, 150, color.RGBA{255, 0, 0, 255}) + rec.writeFrame(img) + rec.close() + + // Read header and verify encryption metadata + files, _ := os.ReadDir(dir) + filePath := filepath.Join(dir, files[0].Name()) + + header, err := ReadRecordingHeader(filePath) + require.NoError(t, err) + + assert.True(t, header.Meta.Encrypted) + assert.NotEmpty(t, header.Meta.EphemeralKey) + assert.Equal(t, 200, header.Width) + assert.Equal(t, 150, header.Height) +} + +func TestRecorderEncryptedDecryptRoundtrip(t *testing.T) { + dir := t.TempDir() + logger := log.WithField("test", t.Name()) + + adminPriv, err := ecdh.X25519().GenerateKey(rand.Reader) + require.NoError(t, err) + adminPubB64 := base64.StdEncoding.EncodeToString(adminPriv.PublicKey().Bytes()) + adminPrivB64 := base64.StdEncoding.EncodeToString(adminPriv.Bytes()) + + rec, err := newVNCRecorder(dir, 100, 100, &RecordingMeta{RemoteAddr: "test"}, adminPubB64, logger) + require.NoError(t, err) + + red := makeTestImage(100, 100, color.RGBA{255, 0, 0, 255}) + green := makeTestImage(100, 100, color.RGBA{0, 255, 0, 255}) + + rec.writeFrame(red) + rec.writeFrame(green) + rec.close() + + // Read back and decrypt + files, _ := os.ReadDir(dir) + filePath := filepath.Join(dir, files[0].Name()) + + header, err := ReadRecordingHeader(filePath) + require.NoError(t, err) + require.True(t, header.Meta.Encrypted) + + dec, err := DecryptRecording(adminPrivB64, header.Meta.EphemeralKey) + require.NoError(t, err) + + // Read raw frames and decrypt + f, err := os.Open(filePath) + require.NoError(t, err) + defer f.Close() + + _, err = parseRecHeader(f) + require.NoError(t, err) + + decryptedFrames := 0 + var hdr [8]byte + for { + if _, readErr := f.Read(hdr[:]); readErr != nil { + break + } + frameLen := int(hdr[4])<<24 | int(hdr[5])<<16 | int(hdr[6])<<8 | int(hdr[7]) + ct := make([]byte, frameLen) + f.Read(ct) + + _, err := dec.Decrypt(ct) + require.NoError(t, err, "frame %d decrypt should succeed", decryptedFrames) + decryptedFrames++ + } + + assert.Equal(t, 2, decryptedFrames) +} diff --git a/client/vnc/server/replay.go b/client/vnc/server/replay.go new file mode 100644 index 000000000..82fded0fe --- /dev/null +++ b/client/vnc/server/replay.go @@ -0,0 +1,64 @@ +package server + +import ( + "encoding/binary" + "encoding/json" + "fmt" + "io" + "os" + "time" +) + +// RecordingHeader holds parsed header data from a VNC recording file. +type RecordingHeader struct { + Width int + Height int + StartTime time.Time + Meta RecordingMeta +} + +// ReadRecordingHeader parses and returns the recording header without loading frames. +func ReadRecordingHeader(filePath string) (*RecordingHeader, error) { + f, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer f.Close() + return parseRecHeader(f) +} + +func parseRecHeader(r io.Reader) (*RecordingHeader, error) { + var hdr [22]byte + if _, err := io.ReadFull(r, hdr[:]); err != nil { + return nil, fmt.Errorf("read header: %w", err) + } + if string(hdr[:6]) != recMagic { + return nil, fmt.Errorf("invalid magic: %x", hdr[:6]) + } + + width := int(binary.BigEndian.Uint16(hdr[6:8])) + height := int(binary.BigEndian.Uint16(hdr[8:10])) + startMs := int64(binary.BigEndian.Uint64(hdr[10:18])) + metaLen := binary.BigEndian.Uint32(hdr[18:22]) + + if metaLen > 1<<20 { + return nil, fmt.Errorf("meta too large: %d bytes", metaLen) + } + + metaJSON := make([]byte, metaLen) + if _, err := io.ReadFull(r, metaJSON); err != nil { + return nil, fmt.Errorf("read meta: %w", err) + } + + var meta RecordingMeta + if err := json.Unmarshal(metaJSON, &meta); err != nil { + return nil, fmt.Errorf("parse meta: %w", err) + } + + return &RecordingHeader{ + Width: width, + Height: height, + StartTime: time.UnixMilli(startMs), + Meta: meta, + }, nil +} diff --git a/client/vnc/server/rfb.go b/client/vnc/server/rfb.go new file mode 100644 index 000000000..173e89c60 --- /dev/null +++ b/client/vnc/server/rfb.go @@ -0,0 +1,264 @@ +package server + +import ( + "bytes" + "compress/zlib" + "crypto/des" + "encoding/binary" + "image" +) + +const ( + rfbProtocolVersion = "RFB 003.008\n" + + secNone = 1 + secVNCAuth = 2 + + // Client message types. + clientSetPixelFormat = 0 + clientSetEncodings = 2 + clientFramebufferUpdateRequest = 3 + clientKeyEvent = 4 + clientPointerEvent = 5 + clientCutText = 6 + + // Server message types. + serverFramebufferUpdate = 0 + serverCutText = 3 + + // Encoding types. + encRaw = 0 + encZlib = 6 +) + +// serverPixelFormat is the default pixel format advertised by the server: +// 32bpp RGBA, big-endian, true-colour, 8 bits per channel. +var serverPixelFormat = [16]byte{ + 32, // bits-per-pixel + 24, // depth + 1, // big-endian-flag + 1, // true-colour-flag + 0, 255, // red-max + 0, 255, // green-max + 0, 255, // blue-max + 16, // red-shift + 8, // green-shift + 0, // blue-shift + 0, 0, 0, // padding +} + +// clientPixelFormat holds the negotiated pixel format from the client. +type clientPixelFormat struct { + bpp uint8 + bigEndian uint8 + rMax uint16 + gMax uint16 + bMax uint16 + rShift uint8 + gShift uint8 + bShift uint8 +} + +func defaultClientPixelFormat() clientPixelFormat { + return clientPixelFormat{ + bpp: serverPixelFormat[0], + bigEndian: serverPixelFormat[2], + rMax: binary.BigEndian.Uint16(serverPixelFormat[4:6]), + gMax: binary.BigEndian.Uint16(serverPixelFormat[6:8]), + bMax: binary.BigEndian.Uint16(serverPixelFormat[8:10]), + rShift: serverPixelFormat[10], + gShift: serverPixelFormat[11], + bShift: serverPixelFormat[12], + } +} + +func parsePixelFormat(pf []byte) clientPixelFormat { + return clientPixelFormat{ + bpp: pf[0], + bigEndian: pf[2], + rMax: binary.BigEndian.Uint16(pf[4:6]), + gMax: binary.BigEndian.Uint16(pf[6:8]), + bMax: binary.BigEndian.Uint16(pf[8:10]), + rShift: pf[10], + gShift: pf[11], + bShift: pf[12], + } +} + +// encodeRawRect encodes a framebuffer region as a raw RFB rectangle. +// The returned buffer includes the FramebufferUpdate header (1 rectangle). +func encodeRawRect(img *image.RGBA, pf clientPixelFormat, x, y, w, h int) []byte { + bytesPerPixel := max(int(pf.bpp)/8, 1) + + pixelBytes := w * h * bytesPerPixel + buf := make([]byte, 4+12+pixelBytes) + + // FramebufferUpdate header. + buf[0] = serverFramebufferUpdate + buf[1] = 0 // padding + binary.BigEndian.PutUint16(buf[2:4], 1) + + // Rectangle header. + binary.BigEndian.PutUint16(buf[4:6], uint16(x)) + binary.BigEndian.PutUint16(buf[6:8], uint16(y)) + binary.BigEndian.PutUint16(buf[8:10], uint16(w)) + binary.BigEndian.PutUint16(buf[10:12], uint16(h)) + binary.BigEndian.PutUint32(buf[12:16], uint32(encRaw)) + + off := 16 + stride := img.Stride + for row := y; row < y+h; row++ { + for col := x; col < x+w; col++ { + p := row*stride + col*4 + r, g, b := img.Pix[p], img.Pix[p+1], img.Pix[p+2] + + rv := uint32(r) * uint32(pf.rMax) / 255 + gv := uint32(g) * uint32(pf.gMax) / 255 + bv := uint32(b) * uint32(pf.bMax) / 255 + pixel := (rv << pf.rShift) | (gv << pf.gShift) | (bv << pf.bShift) + + if pf.bigEndian != 0 { + for i := range bytesPerPixel { + buf[off+i] = byte(pixel >> uint((bytesPerPixel-1-i)*8)) + } + } else { + for i := range bytesPerPixel { + buf[off+i] = byte(pixel >> uint(i*8)) + } + } + off += bytesPerPixel + } + } + + return buf +} + +// vncAuthEncrypt encrypts a 16-byte challenge using the VNC DES scheme. +func vncAuthEncrypt(challenge []byte, password string) []byte { + key := make([]byte, 8) + for i, c := range []byte(password) { + if i >= 8 { + break + } + key[i] = reverseBits(c) + } + block, _ := des.NewCipher(key) + out := make([]byte, 16) + block.Encrypt(out[:8], challenge[:8]) + block.Encrypt(out[8:], challenge[8:]) + return out +} + +func reverseBits(b byte) byte { + var r byte + for range 8 { + r = (r << 1) | (b & 1) + b >>= 1 + } + return r +} + +// encodeZlibRect encodes a framebuffer region using Zlib compression. +// The zlib stream is continuous for the entire VNC session: noVNC creates +// one inflate context at startup and reuses it for all zlib-encoded rects. +// We must NOT reset the zlib writer between calls. +func encodeZlibRect(img *image.RGBA, pf clientPixelFormat, x, y, w, h int, zw *zlib.Writer, zbuf *bytes.Buffer) []byte { + bytesPerPixel := max(int(pf.bpp)/8, 1) + + // Clear the output buffer but keep the deflate dictionary intact. + zbuf.Reset() + + stride := img.Stride + pixel := make([]byte, bytesPerPixel) + for row := y; row < y+h; row++ { + for col := x; col < x+w; col++ { + p := row*stride + col*4 + r, g, b := img.Pix[p], img.Pix[p+1], img.Pix[p+2] + + rv := uint32(r) * uint32(pf.rMax) / 255 + gv := uint32(g) * uint32(pf.gMax) / 255 + bv := uint32(b) * uint32(pf.bMax) / 255 + val := (rv << pf.rShift) | (gv << pf.gShift) | (bv << pf.bShift) + + if pf.bigEndian != 0 { + for i := range bytesPerPixel { + pixel[i] = byte(val >> uint((bytesPerPixel-1-i)*8)) + } + } else { + for i := range bytesPerPixel { + pixel[i] = byte(val >> uint(i*8)) + } + } + zw.Write(pixel) + } + } + zw.Flush() + + compressed := zbuf.Bytes() + + // Build the FramebufferUpdate message. + buf := make([]byte, 4+12+4+len(compressed)) + buf[0] = serverFramebufferUpdate + buf[1] = 0 + binary.BigEndian.PutUint16(buf[2:4], 1) // 1 rectangle + + binary.BigEndian.PutUint16(buf[4:6], uint16(x)) + binary.BigEndian.PutUint16(buf[6:8], uint16(y)) + binary.BigEndian.PutUint16(buf[8:10], uint16(w)) + binary.BigEndian.PutUint16(buf[10:12], uint16(h)) + binary.BigEndian.PutUint32(buf[12:16], uint32(encZlib)) + binary.BigEndian.PutUint32(buf[16:20], uint32(len(compressed))) + copy(buf[20:], compressed) + + return buf +} + +// diffRects compares two RGBA images and returns a list of dirty rectangles. +// Divides the screen into tiles and checks each for changes. +func diffRects(prev, cur *image.RGBA, w, h, tileSize int) [][4]int { + if prev == nil { + return [][4]int{{0, 0, w, h}} + } + + var rects [][4]int + for ty := 0; ty < h; ty += tileSize { + th := min(tileSize, h-ty) + for tx := 0; tx < w; tx += tileSize { + tw := min(tileSize, w-tx) + if tileChanged(prev, cur, tx, ty, tw, th) { + rects = append(rects, [4]int{tx, ty, tw, th}) + } + } + } + return rects +} + +func tileChanged(prev, cur *image.RGBA, x, y, w, h int) bool { + stride := prev.Stride + for row := y; row < y+h; row++ { + off := row*stride + x*4 + end := off + w*4 + prevRow := prev.Pix[off:end] + curRow := cur.Pix[off:end] + if !bytes.Equal(prevRow, curRow) { + return true + } + } + return false +} + +// zlibState holds the persistent zlib writer and buffer for a session. +type zlibState struct { + buf *bytes.Buffer + w *zlib.Writer +} + +func newZlibState() *zlibState { + buf := &bytes.Buffer{} + w, _ := zlib.NewWriterLevel(buf, zlib.BestSpeed) + return &zlibState{buf: buf, w: w} +} + +func (z *zlibState) Close() error { + return z.w.Close() +} diff --git a/client/vnc/server/server.go b/client/vnc/server/server.go new file mode 100644 index 000000000..bb8c11ad1 --- /dev/null +++ b/client/vnc/server/server.go @@ -0,0 +1,690 @@ +package server + +import ( + "context" + "crypto/subtle" + "encoding/binary" + "encoding/hex" + "errors" + "fmt" + "image" + "io" + "net" + "net/netip" + "strings" + "sync" + "time" + + gojwt "github.com/golang-jwt/jwt/v5" + log "github.com/sirupsen/logrus" + "golang.zx2c4.com/wireguard/tun/netstack" + + sshauth "github.com/netbirdio/netbird/client/ssh/auth" + nbjwt "github.com/netbirdio/netbird/shared/auth/jwt" +) + +// Connection modes sent by the client in the session header. +const ( + ModeAttach byte = 0 // Capture current display + ModeSession byte = 1 // Virtual session as specified user +) + +// RFB security-failure reason codes sent to the client. These prefixes are +// stable so dashboard/noVNC integrations can branch on them without parsing +// free text. Format: "CODE: human message". +const ( + RejectCodeJWTMissing = "AUTH_JWT_MISSING" + RejectCodeJWTExpired = "AUTH_JWT_EXPIRED" + RejectCodeJWTInvalid = "AUTH_JWT_INVALID" + RejectCodeAuthForbidden = "AUTH_FORBIDDEN" + RejectCodeAuthConfig = "AUTH_CONFIG" + RejectCodeSessionError = "SESSION_ERROR" + RejectCodeCapturerError = "CAPTURER_ERROR" + RejectCodeUnsupportedOS = "UNSUPPORTED" + RejectCodeBadRequest = "BAD_REQUEST" +) + +// EnvVNCDisableDownscale disables any platform-specific framebuffer +// downscaling (e.g. Retina 2:1). Set to 1/true to send the native resolution. +const EnvVNCDisableDownscale = "NB_VNC_DISABLE_DOWNSCALE" + +// ScreenCapturer grabs desktop frames for the VNC server. +type ScreenCapturer interface { + // Width returns the current screen width in pixels. + Width() int + // Height returns the current screen height in pixels. + Height() int + // Capture returns the current desktop as an RGBA image. + Capture() (*image.RGBA, error) +} + +// InputInjector delivers keyboard and mouse events to the OS. +type InputInjector interface { + // InjectKey simulates a key press or release. keysym is an X11 KeySym. + InjectKey(keysym uint32, down bool) + // InjectPointer simulates mouse movement and button state. + InjectPointer(buttonMask uint8, x, y, serverW, serverH int) + // SetClipboard sets the system clipboard to the given text. + SetClipboard(text string) + // GetClipboard returns the current system clipboard text. + GetClipboard() string +} + +// JWTConfig holds JWT validation configuration for VNC auth. +type JWTConfig struct { + Issuer string + KeysLocation string + MaxTokenAge int64 + Audiences []string +} + +// connectionHeader is sent by the client before the RFB handshake to specify +// the VNC session mode and authenticate. +type connectionHeader struct { + mode byte + username string + jwt string + sessionID uint32 // Windows session ID (0 = console/auto) +} + +// Server is the embedded VNC server that listens on the WireGuard interface. +// It supports two operating modes: +// - Direct mode: captures the screen and handles VNC sessions in-process. +// Used when running in a user session with desktop access. +// - Service mode: proxies VNC connections to an agent process spawned in +// the active console session. Used when running as a Windows service in +// Session 0. +// +// Within direct mode, each connection can request one of two session modes +// via the connection header: +// - Attach: capture the current physical display. +// - Session: start a virtual Xvfb display as the requested user. +type Server struct { + capturer ScreenCapturer + injector InputInjector + password string + serviceMode bool + disableAuth bool + localAddr netip.Addr // NetBird WireGuard IP this server is bound to + network netip.Prefix // NetBird overlay network + log *log.Entry + + recordingDir string // when set, VNC sessions are recorded to this directory + recordingEncKey string // base64-encoded X25519 public key for encrypting recordings + + mu sync.Mutex + listener net.Listener + ctx context.Context + cancel context.CancelFunc + vmgr virtualSessionManager + jwtConfig *JWTConfig + jwtValidator *nbjwt.Validator + jwtExtractor *nbjwt.ClaimsExtractor + authorizer *sshauth.Authorizer + netstackNet *netstack.Net + agentToken []byte // raw token bytes for agent-mode auth +} + +// vncSession provides capturer and injector for a virtual display session. +type vncSession interface { + Capturer() ScreenCapturer + Injector() InputInjector + Display() string + ClientConnect() + ClientDisconnect() +} + +// virtualSessionManager is implemented by sessionManager on Linux. +type virtualSessionManager interface { + GetOrCreate(username string) (vncSession, error) + StopAll() +} + +// New creates a VNC server with the given screen capturer and input injector. +func New(capturer ScreenCapturer, injector InputInjector, password string) *Server { + return &Server{ + capturer: capturer, + injector: injector, + password: password, + authorizer: sshauth.NewAuthorizer(), + log: log.WithField("component", "vnc-server"), + } +} + +// SetServiceMode enables proxy-to-agent mode for Windows service operation. +func (s *Server) SetServiceMode(enabled bool) { + s.serviceMode = enabled +} + +// SetJWTConfig configures JWT authentication for VNC connections. +// Pass nil to disable JWT (public mode). +func (s *Server) SetJWTConfig(config *JWTConfig) { + s.mu.Lock() + defer s.mu.Unlock() + s.jwtConfig = config + s.jwtValidator = nil + s.jwtExtractor = nil +} + +// SetDisableAuth disables authentication entirely. +func (s *Server) SetDisableAuth(disable bool) { + s.disableAuth = disable +} + +// SetAgentToken sets a hex-encoded token that must be presented by incoming +// connections before any VNC data. Used in agent mode to verify that only the +// trusted service process connects. +func (s *Server) SetAgentToken(hexToken string) { + if hexToken == "" { + return + } + b, err := hex.DecodeString(hexToken) + if err != nil { + s.log.Warnf("invalid agent token: %v", err) + return + } + s.agentToken = b +} + +// SetNetstackNet sets the netstack network for userspace-only listening. +// When set, the VNC server listens via netstack instead of a real OS socket. +func (s *Server) SetNetstackNet(n *netstack.Net) { + s.mu.Lock() + defer s.mu.Unlock() + s.netstackNet = n +} + +// SetRecordingDir enables VNC session recording to the given directory. +func (s *Server) SetRecordingDir(dir string) { + s.recordingDir = dir +} + +// SetRecordingEncryptionKey sets the base64-encoded X25519 public key for encrypting recordings. +func (s *Server) SetRecordingEncryptionKey(key string) { + s.recordingEncKey = key +} + +// UpdateVNCAuth updates the fine-grained authorization configuration. +func (s *Server) UpdateVNCAuth(config *sshauth.Config) { + s.mu.Lock() + defer s.mu.Unlock() + s.jwtValidator = nil + s.jwtExtractor = nil + s.authorizer.Update(config) +} + +// Start begins listening for VNC connections on the given address. +// network is the NetBird overlay prefix used to validate connection sources. +func (s *Server) Start(ctx context.Context, addr netip.AddrPort, network netip.Prefix) error { + s.mu.Lock() + defer s.mu.Unlock() + + if s.listener != nil { + return fmt.Errorf("server already running") + } + + s.ctx, s.cancel = context.WithCancel(ctx) + s.vmgr = s.platformSessionManager() + s.localAddr = addr.Addr() + s.network = network + + var listener net.Listener + var listenDesc string + if s.netstackNet != nil { + ln, err := s.netstackNet.ListenTCPAddrPort(addr) + if err != nil { + return fmt.Errorf("listen on netstack %s: %w", addr, err) + } + listener = ln + listenDesc = fmt.Sprintf("netstack %s", addr) + } else { + tcpAddr := net.TCPAddrFromAddrPort(addr) + ln, err := net.ListenTCP("tcp", tcpAddr) + if err != nil { + return fmt.Errorf("listen on %s: %w", addr, err) + } + listener = ln + listenDesc = addr.String() + } + s.listener = listener + + if s.serviceMode { + s.platformInit() + } + + if s.serviceMode { + go s.serviceAcceptLoop() + } else { + go s.acceptLoop() + } + + s.log.Infof("started on %s (service_mode=%v)", listenDesc, s.serviceMode) + return nil +} + +// Stop shuts down the server and closes all connections. +func (s *Server) Stop() error { + s.mu.Lock() + defer s.mu.Unlock() + + if s.cancel != nil { + s.cancel() + s.cancel = nil + } + + if s.vmgr != nil { + s.vmgr.StopAll() + } + + if c, ok := s.capturer.(interface{ Close() }); ok { + c.Close() + } + + if s.listener != nil { + err := s.listener.Close() + s.listener = nil + if err != nil { + return fmt.Errorf("close VNC listener: %w", err) + } + } + + s.log.Info("stopped") + return nil +} + +// acceptLoop handles VNC connections directly (user session mode). +func (s *Server) acceptLoop() { + for { + conn, err := s.listener.Accept() + if err != nil { + select { + case <-s.ctx.Done(): + return + default: + } + s.log.Debugf("accept VNC connection: %v", err) + continue + } + + go s.handleConnection(conn) + } +} + +func (s *Server) validateCapturer(cap ScreenCapturer) error { + // Quick check first: if already ready, return immediately. + if cap.Width() > 0 && cap.Height() > 0 { + return nil + } + // Capturer not ready: poke any retry loop that supports it so it doesn't + // wait out its full backoff (e.g. macOS waiting for Screen Recording). + if w, ok := cap.(interface{ Wake() }); ok { + w.Wake() + } + // Wait up to 5s for the capturer to become ready. + for range 50 { + time.Sleep(100 * time.Millisecond) + if cap.Width() > 0 && cap.Height() > 0 { + return nil + } + } + return errors.New("no display available (check X11 on Linux or Screen Recording permission on macOS)") +} + +// isAllowedSource rejects connections from outside the NetBird overlay network +// and from the local WireGuard IP (prevents local privilege escalation). +// Matches the SSH server's connectionValidator logic. +func (s *Server) isAllowedSource(addr net.Addr) bool { + tcpAddr, ok := addr.(*net.TCPAddr) + if !ok { + s.log.Warnf("connection rejected: non-TCP address %s", addr) + return false + } + + remoteIP, ok := netip.AddrFromSlice(tcpAddr.IP) + if !ok { + s.log.Warnf("connection rejected: invalid remote IP %s", tcpAddr.IP) + return false + } + remoteIP = remoteIP.Unmap() + + if remoteIP.IsLoopback() && s.localAddr.IsLoopback() { + return true + } + + if remoteIP == s.localAddr { + s.log.Warnf("connection rejected from own IP %s", remoteIP) + return false + } + + if s.network.IsValid() && !s.network.Contains(remoteIP) { + s.log.Warnf("connection rejected from non-NetBird IP %s", remoteIP) + return false + } + + return true +} + +func (s *Server) handleConnection(conn net.Conn) { + connLog := s.log.WithField("remote", conn.RemoteAddr().String()) + + if !s.isAllowedSource(conn.RemoteAddr()) { + conn.Close() + return + } + + if len(s.agentToken) > 0 { + buf := make([]byte, len(s.agentToken)) + if err := conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil { + connLog.Debugf("set agent token deadline: %v", err) + conn.Close() + return + } + if _, err := io.ReadFull(conn, buf); err != nil { + connLog.Warnf("agent auth: read token: %v", err) + conn.Close() + return + } + conn.SetReadDeadline(time.Time{}) //nolint:errcheck + if subtle.ConstantTimeCompare(buf, s.agentToken) != 1 { + connLog.Warn("agent auth: invalid token, rejecting") + conn.Close() + return + } + } + + header, err := readConnectionHeader(conn) + if err != nil { + connLog.Warnf("read connection header: %v", err) + conn.Close() + return + } + + if !s.disableAuth { + if s.jwtConfig == nil { + rejectConnection(conn, codeMessage(RejectCodeAuthConfig, "auth enabled but no identity provider configured")) + connLog.Warn("auth rejected: no identity provider configured") + return + } + jwtUserID, err := s.authenticateJWT(header) + if err != nil { + rejectConnection(conn, codeMessage(jwtErrorCode(err), err.Error())) + connLog.Warnf("auth rejected: %v", err) + return + } + connLog = connLog.WithField("jwt_user", jwtUserID) + } + + var capturer ScreenCapturer + var injector InputInjector + + switch header.mode { + case ModeSession: + if s.vmgr == nil { + rejectConnection(conn, codeMessage(RejectCodeUnsupportedOS, "virtual sessions not supported on this platform")) + connLog.Warn("session rejected: not supported on this platform") + return + } + if header.username == "" { + rejectConnection(conn, codeMessage(RejectCodeBadRequest, "session mode requires a username")) + connLog.Warn("session rejected: no username provided") + return + } + vs, err := s.vmgr.GetOrCreate(header.username) + if err != nil { + rejectConnection(conn, codeMessage(RejectCodeSessionError, fmt.Sprintf("create virtual session: %v", err))) + connLog.Warnf("create virtual session for %s: %v", header.username, err) + return + } + capturer = vs.Capturer() + injector = vs.Injector() + vs.ClientConnect() + defer vs.ClientDisconnect() + connLog = connLog.WithField("vnc_user", header.username) + connLog.Infof("session mode: user=%s display=%s", header.username, vs.Display()) + + default: + capturer = s.capturer + injector = s.injector + if cc, ok := capturer.(interface{ ClientConnect() }); ok { + cc.ClientConnect() + } + defer func() { + if cd, ok := capturer.(interface{ ClientDisconnect() }); ok { + cd.ClientDisconnect() + } + }() + } + + if err := s.validateCapturer(capturer); err != nil { + rejectConnection(conn, codeMessage(RejectCodeCapturerError, fmt.Sprintf("screen capturer: %v", err))) + connLog.Warnf("capturer not ready: %v", err) + return + } + + var rec *vncRecorder + if s.recordingDir != "" { + mode := "attach" + if header.mode == ModeSession { + mode = "session" + } + jwtUser, _ := connLog.Data["jwt_user"].(string) + var err error + rec, err = newVNCRecorder(s.recordingDir, capturer.Width(), capturer.Height(), &RecordingMeta{ + User: header.username, + RemoteAddr: conn.RemoteAddr().String(), + JWTUser: jwtUser, + Mode: mode, + }, s.recordingEncKey, connLog) + if err != nil { + connLog.Warnf("start VNC recording: %v", err) + } + } + + sess := &session{ + conn: conn, + capturer: capturer, + injector: injector, + serverW: capturer.Width(), + serverH: capturer.Height(), + password: s.password, + log: connLog, + recorder: rec, + } + sess.serve() +} + +// codeMessage formats a stable reject code with a human-readable message. +// Dashboards split on the first ": " to recover the code without parsing the +// free-text suffix. +func codeMessage(code, msg string) string { + return code + ": " + msg +} + +// jwtErrorCode maps a JWT auth error to a stable reject code. +func jwtErrorCode(err error) string { + if err == nil { + return RejectCodeJWTInvalid + } + if errors.Is(err, nbjwt.ErrTokenExpired) { + return RejectCodeJWTExpired + } + msg := err.Error() + switch { + case strings.Contains(msg, "JWT required but not provided"): + return RejectCodeJWTMissing + case strings.Contains(msg, "authorize") || strings.Contains(msg, "not authorized"): + return RejectCodeAuthForbidden + default: + return RejectCodeJWTInvalid + } +} + +// rejectConnection sends a minimal RFB handshake with a security failure +// reason, so VNC clients display the error message instead of a generic +// "unexpected disconnect." +func rejectConnection(conn net.Conn, reason string) { + defer conn.Close() + // RFB 3.8 server version. + io.WriteString(conn, "RFB 003.008\n") + // Read client version (12 bytes), ignore errors. + var clientVer [12]byte + conn.SetReadDeadline(time.Now().Add(2 * time.Second)) + io.ReadFull(conn, clientVer[:]) + conn.SetReadDeadline(time.Time{}) + // Send 0 security types = connection failed, followed by reason. + msg := []byte(reason) + buf := make([]byte, 1+4+len(msg)) + buf[0] = 0 // 0 security types = failure + binary.BigEndian.PutUint32(buf[1:5], uint32(len(msg))) + copy(buf[5:], msg) + conn.Write(buf) +} + +const defaultJWTMaxTokenAge = 10 * 60 // 10 minutes + +// authenticateJWT validates the JWT from the connection header and checks +// authorization. For attach mode, just checks membership in the authorized +// user list. For session mode, additionally validates the OS user mapping. +func (s *Server) authenticateJWT(header *connectionHeader) (string, error) { + if header.jwt == "" { + return "", fmt.Errorf("JWT required but not provided") + } + + s.mu.Lock() + if err := s.ensureJWTValidator(); err != nil { + s.mu.Unlock() + return "", fmt.Errorf("initialize JWT validator: %w", err) + } + validator := s.jwtValidator + extractor := s.jwtExtractor + s.mu.Unlock() + + token, err := validator.ValidateAndParse(context.Background(), header.jwt) + if err != nil { + return "", fmt.Errorf("validate JWT: %w", err) + } + + if err := s.checkTokenAge(token); err != nil { + return "", err + } + + userAuth, err := extractor.ToUserAuth(token) + if err != nil { + return "", fmt.Errorf("extract user from JWT: %w", err) + } + if userAuth.UserId == "" { + return "", fmt.Errorf("JWT has no user ID") + } + + switch header.mode { + case ModeSession: + // Session mode: check user + OS username mapping. + if _, err := s.authorizer.Authorize(userAuth.UserId, header.username); err != nil { + return "", fmt.Errorf("authorize session for %s: %w", header.username, err) + } + default: + // Attach mode: just check user is in the authorized list (wildcard OS user). + if _, err := s.authorizer.Authorize(userAuth.UserId, "*"); err != nil { + return "", fmt.Errorf("user not authorized for VNC: %w", err) + } + } + + return userAuth.UserId, nil +} + +// ensureJWTValidator lazily initializes the JWT validator. Must be called with mu held. +func (s *Server) ensureJWTValidator() error { + if s.jwtValidator != nil && s.jwtExtractor != nil { + return nil + } + if s.jwtConfig == nil { + return fmt.Errorf("no JWT config") + } + + s.jwtValidator = nbjwt.NewValidator( + s.jwtConfig.Issuer, + s.jwtConfig.Audiences, + s.jwtConfig.KeysLocation, + false, + ) + + opts := []nbjwt.ClaimsExtractorOption{nbjwt.WithAudience(s.jwtConfig.Audiences[0])} + if claim := s.authorizer.GetUserIDClaim(); claim != "" { + opts = append(opts, nbjwt.WithUserIDClaim(claim)) + } + s.jwtExtractor = nbjwt.NewClaimsExtractor(opts...) + + return nil +} + +func (s *Server) checkTokenAge(token *gojwt.Token) error { + maxAge := defaultJWTMaxTokenAge + if s.jwtConfig != nil && s.jwtConfig.MaxTokenAge > 0 { + maxAge = int(s.jwtConfig.MaxTokenAge) + } + return nbjwt.CheckTokenAge(token, time.Duration(maxAge)*time.Second) +} + +// readConnectionHeader reads the NetBird VNC session header from the connection. +// Format: [mode: 1 byte] [username_len: 2 bytes BE] [username: N bytes] +// +// [jwt_len: 2 bytes BE] [jwt: N bytes] +// +// Uses a short timeout: our WASM proxy sends the header immediately after +// connecting. Standard VNC clients don't send anything first (server speaks +// first in RFB), so they time out and get the default attach mode. +func readConnectionHeader(conn net.Conn) (*connectionHeader, error) { + if err := conn.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil { + return nil, fmt.Errorf("set deadline: %w", err) + } + defer conn.SetReadDeadline(time.Time{}) //nolint:errcheck + + var hdr [3]byte + if _, err := io.ReadFull(conn, hdr[:]); err != nil { + // Timeout or error: assume no header, use attach mode. + return &connectionHeader{mode: ModeAttach}, nil + } + + // Restore a longer deadline for reading variable-length fields. + if err := conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil { + return nil, fmt.Errorf("set deadline: %w", err) + } + + mode := hdr[0] + usernameLen := binary.BigEndian.Uint16(hdr[1:3]) + + var username string + if usernameLen > 0 { + if usernameLen > 256 { + return nil, fmt.Errorf("username too long: %d", usernameLen) + } + buf := make([]byte, usernameLen) + if _, err := io.ReadFull(conn, buf); err != nil { + return nil, fmt.Errorf("read username: %w", err) + } + username = string(buf) + } + + // Read JWT token length and data. + var jwtLenBuf [2]byte + var jwtToken string + if _, err := io.ReadFull(conn, jwtLenBuf[:]); err == nil { + jwtLen := binary.BigEndian.Uint16(jwtLenBuf[:]) + if jwtLen > 0 && jwtLen < 8192 { + buf := make([]byte, jwtLen) + if _, err := io.ReadFull(conn, buf); err != nil { + return nil, fmt.Errorf("read JWT: %w", err) + } + jwtToken = string(buf) + } + } + + // Read optional Windows session ID (4 bytes BE). Missing = 0 (console/auto). + var sessionID uint32 + var sidBuf [4]byte + if _, err := io.ReadFull(conn, sidBuf[:]); err == nil { + sessionID = binary.BigEndian.Uint32(sidBuf[:]) + } + + return &connectionHeader{mode: mode, username: username, jwt: jwtToken, sessionID: sessionID}, nil +} diff --git a/client/vnc/server/server_darwin.go b/client/vnc/server/server_darwin.go new file mode 100644 index 000000000..7b7ed114c --- /dev/null +++ b/client/vnc/server/server_darwin.go @@ -0,0 +1,15 @@ +//go:build darwin && !ios + +package server + +func (s *Server) platformInit() {} + +// serviceAcceptLoop is not supported on macOS. +func (s *Server) serviceAcceptLoop() { + s.log.Warn("service mode not supported on macOS, falling back to direct mode") + s.acceptLoop() +} + +func (s *Server) platformSessionManager() virtualSessionManager { + return nil +} diff --git a/client/vnc/server/server_stub.go b/client/vnc/server/server_stub.go new file mode 100644 index 000000000..5eb682ef2 --- /dev/null +++ b/client/vnc/server/server_stub.go @@ -0,0 +1,15 @@ +//go:build !windows && !darwin && !freebsd && !(linux && !android) + +package server + +func (s *Server) platformInit() {} + +// serviceAcceptLoop is not supported on non-Windows platforms. +func (s *Server) serviceAcceptLoop() { + s.log.Warn("service mode not supported on this platform, falling back to direct mode") + s.acceptLoop() +} + +func (s *Server) platformSessionManager() virtualSessionManager { + return nil +} diff --git a/client/vnc/server/server_test.go b/client/vnc/server/server_test.go new file mode 100644 index 000000000..768593ca1 --- /dev/null +++ b/client/vnc/server/server_test.go @@ -0,0 +1,136 @@ +package server + +import ( + "encoding/binary" + "image" + "io" + "net" + "net/netip" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// testCapturer returns a 100x100 image for test sessions. +type testCapturer struct{} + +func (t *testCapturer) Width() int { return 100 } +func (t *testCapturer) Height() int { return 100 } +func (t *testCapturer) Capture() (*image.RGBA, error) { return image.NewRGBA(image.Rect(0, 0, 100, 100)), nil } + +func startTestServer(t *testing.T, disableAuth bool, jwtConfig *JWTConfig) (net.Addr, *Server) { + t.Helper() + + srv := New(&testCapturer{}, &StubInputInjector{}, "") + srv.SetDisableAuth(disableAuth) + if jwtConfig != nil { + srv.SetJWTConfig(jwtConfig) + } + + addr := netip.MustParseAddrPort("127.0.0.1:0") + network := netip.MustParsePrefix("127.0.0.0/8") + require.NoError(t, srv.Start(t.Context(), addr, network)) + // Override local address so source validation doesn't reject 127.0.0.1 as "own IP". + srv.localAddr = netip.MustParseAddr("10.99.99.1") + t.Cleanup(func() { _ = srv.Stop() }) + + return srv.listener.Addr(), srv +} + +func TestAuthEnabled_NoJWTConfig_RejectsConnection(t *testing.T) { + addr, _ := startTestServer(t, false, nil) + + conn, err := net.Dial("tcp", addr.String()) + require.NoError(t, err) + defer conn.Close() + + // Send session header: attach mode, no username, no JWT. + header := []byte{ModeAttach, 0, 0, 0, 0} + _, err = conn.Write(header) + require.NoError(t, err) + + // Server should send RFB version then security failure. + var version [12]byte + _, err = io.ReadFull(conn, version[:]) + require.NoError(t, err) + assert.Equal(t, "RFB 003.008\n", string(version[:])) + + // Write client version to proceed through handshake. + _, err = conn.Write(version[:]) + require.NoError(t, err) + + // Read security types: 0 means failure, followed by reason. + var numTypes [1]byte + _, err = io.ReadFull(conn, numTypes[:]) + require.NoError(t, err) + assert.Equal(t, byte(0), numTypes[0], "should have 0 security types (failure)") + + var reasonLen [4]byte + _, err = io.ReadFull(conn, reasonLen[:]) + require.NoError(t, err) + + reason := make([]byte, binary.BigEndian.Uint32(reasonLen[:])) + _, err = io.ReadFull(conn, reason) + require.NoError(t, err) + assert.Contains(t, string(reason), "identity provider", "rejection reason should mention missing IdP config") +} + +func TestAuthDisabled_AllowsConnection(t *testing.T) { + addr, _ := startTestServer(t, true, nil) + + conn, err := net.Dial("tcp", addr.String()) + require.NoError(t, err) + defer conn.Close() + + // Send session header: attach mode, no username, no JWT. + header := []byte{ModeAttach, 0, 0, 0, 0} + _, err = conn.Write(header) + require.NoError(t, err) + + // Server should send RFB version. + var version [12]byte + _, err = io.ReadFull(conn, version[:]) + require.NoError(t, err) + assert.Equal(t, "RFB 003.008\n", string(version[:])) + + // Write client version. + _, err = conn.Write(version[:]) + require.NoError(t, err) + + // Should get security types (not 0 = failure). + var numTypes [1]byte + _, err = io.ReadFull(conn, numTypes[:]) + require.NoError(t, err) + assert.NotEqual(t, byte(0), numTypes[0], "should have at least one security type (auth disabled)") +} + +func TestAuthEnabled_EmptyJWT_Rejected(t *testing.T) { + // Auth enabled with a (bogus) JWT config: connections without JWT should be rejected. + addr, _ := startTestServer(t, false, &JWTConfig{ + Issuer: "https://example.com", + KeysLocation: "https://example.com/.well-known/jwks.json", + Audiences: []string{"test"}, + }) + + conn, err := net.Dial("tcp", addr.String()) + require.NoError(t, err) + defer conn.Close() + + // Send session header with empty JWT. + header := []byte{ModeAttach, 0, 0, 0, 0} + _, err = conn.Write(header) + require.NoError(t, err) + + var version [12]byte + _, err = io.ReadFull(conn, version[:]) + require.NoError(t, err) + + _, err = conn.Write(version[:]) + require.NoError(t, err) + + var numTypes [1]byte + _, err = io.ReadFull(conn, numTypes[:]) + require.NoError(t, err) + assert.Equal(t, byte(0), numTypes[0], "should reject with 0 security types") +} diff --git a/client/vnc/server/server_windows.go b/client/vnc/server/server_windows.go new file mode 100644 index 000000000..f94a2c756 --- /dev/null +++ b/client/vnc/server/server_windows.go @@ -0,0 +1,222 @@ +//go:build windows + +package server + +import ( + "bytes" + "io" + "net" + "unsafe" + + log "github.com/sirupsen/logrus" + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/registry" +) + +var ( + sasDLL = windows.NewLazySystemDLL("sas.dll") + procSendSAS = sasDLL.NewProc("SendSAS") + + procConvertStringSecurityDescriptorToSecurityDescriptor = advapi32.NewProc("ConvertStringSecurityDescriptorToSecurityDescriptorW") +) + +// sasSecurityAttributes builds a SECURITY_ATTRIBUTES that grants +// EVENT_MODIFY_STATE only to the SYSTEM account, preventing unprivileged +// local processes from triggering the Secure Attention Sequence. +func sasSecurityAttributes() (*windows.SecurityAttributes, error) { + // SDDL: grant full access to SYSTEM (creates/waits) and EVENT_MODIFY_STATE + // to the interactive user (IU) so the VNC agent in the console session can + // signal it. Other local users and network users are denied. + sddl, err := windows.UTF16PtrFromString("D:(A;;GA;;;SY)(A;;0x0002;;;IU)") + if err != nil { + return nil, err + } + var sd uintptr + r, _, lerr := procConvertStringSecurityDescriptorToSecurityDescriptor.Call( + uintptr(unsafe.Pointer(sddl)), + 1, // SDDL_REVISION_1 + uintptr(unsafe.Pointer(&sd)), + 0, + ) + if r == 0 { + return nil, lerr + } + return &windows.SecurityAttributes{ + Length: uint32(unsafe.Sizeof(windows.SecurityAttributes{})), + SecurityDescriptor: (*windows.SECURITY_DESCRIPTOR)(unsafe.Pointer(sd)), + InheritHandle: 0, + }, nil +} + +// enableSoftwareSAS sets the SoftwareSASGeneration registry key to allow +// services to trigger the Secure Attention Sequence via SendSAS. Without this, +// SendSAS silently does nothing on most Windows editions. +func enableSoftwareSAS() { + key, _, err := registry.CreateKey( + registry.LOCAL_MACHINE, + `SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System`, + registry.SET_VALUE, + ) + if err != nil { + log.Warnf("open SoftwareSASGeneration registry key: %v", err) + return + } + defer key.Close() + + if err := key.SetDWordValue("SoftwareSASGeneration", 1); err != nil { + log.Warnf("set SoftwareSASGeneration: %v", err) + return + } + log.Debug("SoftwareSASGeneration registry key set to 1 (services allowed)") +} + +// startSASListener creates a named event with a restricted DACL and waits for +// the VNC input injector to signal it. When signaled, it calls SendSAS(FALSE) +// from Session 0 to trigger the Secure Attention Sequence (Ctrl+Alt+Del). +// Only SYSTEM processes can open the event. +func startSASListener() { + enableSoftwareSAS() + namePtr, err := windows.UTF16PtrFromString(sasEventName) + if err != nil { + log.Warnf("SAS listener UTF16: %v", err) + return + } + sa, err := sasSecurityAttributes() + if err != nil { + log.Warnf("build SAS security descriptor: %v", err) + return + } + ev, err := windows.CreateEvent(sa, 0, 0, namePtr) + if err != nil { + log.Warnf("SAS CreateEvent: %v", err) + return + } + log.Info("SAS listener ready (Session 0)") + go func() { + defer windows.CloseHandle(ev) + for { + ret, _ := windows.WaitForSingleObject(ev, windows.INFINITE) + if ret == windows.WAIT_OBJECT_0 { + r, _, sasErr := procSendSAS.Call(0) // FALSE = not from service desktop + if r == 0 { + log.Warnf("SendSAS: %v", sasErr) + } else { + log.Info("SendSAS called from Session 0") + } + } + } + }() +} + +// enablePrivilege enables a named privilege on the current process token. +func enablePrivilege(name string) error { + var token windows.Token + if err := windows.OpenProcessToken(windows.CurrentProcess(), + windows.TOKEN_ADJUST_PRIVILEGES|windows.TOKEN_QUERY, &token); err != nil { + return err + } + defer token.Close() + + var luid windows.LUID + namePtr, _ := windows.UTF16PtrFromString(name) + if err := windows.LookupPrivilegeValue(nil, namePtr, &luid); err != nil { + return err + } + tp := windows.Tokenprivileges{PrivilegeCount: 1} + tp.Privileges[0].Luid = luid + tp.Privileges[0].Attributes = windows.SE_PRIVILEGE_ENABLED + return windows.AdjustTokenPrivileges(token, false, &tp, 0, nil, nil) +} + +func (s *Server) platformSessionManager() virtualSessionManager { + return nil +} + +// platformInit starts the SAS listener and enables privileges needed for +// Session 0 operations (agent spawning, SendSAS). +func (s *Server) platformInit() { + for _, priv := range []string{"SeTcbPrivilege", "SeAssignPrimaryTokenPrivilege"} { + if err := enablePrivilege(priv); err != nil { + log.Debugf("enable %s: %v", priv, err) + } + } + startSASListener() +} + +// serviceAcceptLoop runs in Session 0. It validates source IP and +// authenticates via JWT before proxying connections to the user-session agent. +func (s *Server) serviceAcceptLoop() { + + sm := newSessionManager(agentPort) + go sm.run() + + log.Infof("service mode, proxying connections to agent on 127.0.0.1:%s", agentPort) + + for { + conn, err := s.listener.Accept() + if err != nil { + select { + case <-s.ctx.Done(): + sm.Stop() + return + default: + } + s.log.Debugf("accept VNC connection: %v", err) + continue + } + + go s.handleServiceConnection(conn, sm) + } +} + +// handleServiceConnection validates the source IP and JWT, then proxies +// the connection (with header bytes replayed) to the agent. +func (s *Server) handleServiceConnection(conn net.Conn, sm *sessionManager) { + connLog := s.log.WithField("remote", conn.RemoteAddr().String()) + + if !s.isAllowedSource(conn.RemoteAddr()) { + conn.Close() + return + } + + var headerBuf bytes.Buffer + tee := io.TeeReader(conn, &headerBuf) + teeConn := &prefixConn{Reader: tee, Conn: conn} + + header, err := readConnectionHeader(teeConn) + if err != nil { + connLog.Debugf("read connection header: %v", err) + conn.Close() + return + } + + if !s.disableAuth { + if s.jwtConfig == nil { + rejectConnection(conn, codeMessage(RejectCodeAuthConfig, "auth enabled but no identity provider configured")) + connLog.Warn("auth rejected: no identity provider configured") + return + } + if _, err := s.authenticateJWT(header); err != nil { + rejectConnection(conn, codeMessage(jwtErrorCode(err), err.Error())) + connLog.Warnf("auth rejected: %v", err) + return + } + } + + // Replay buffered header bytes + remaining stream to the agent. + replayConn := &prefixConn{ + Reader: io.MultiReader(&headerBuf, conn), + Conn: conn, + } + proxyToAgent(replayConn, agentPort, sm.AuthToken()) +} + +// prefixConn wraps a net.Conn, overriding Read to use a different reader. +type prefixConn struct { + io.Reader + net.Conn +} + +func (p *prefixConn) Read(b []byte) (int, error) { + return p.Reader.Read(b) +} diff --git a/client/vnc/server/server_x11.go b/client/vnc/server/server_x11.go new file mode 100644 index 000000000..604206851 --- /dev/null +++ b/client/vnc/server/server_x11.go @@ -0,0 +1,15 @@ +//go:build (linux && !android) || freebsd + +package server + +func (s *Server) platformInit() {} + +// serviceAcceptLoop is not supported on Linux. +func (s *Server) serviceAcceptLoop() { + s.log.Warn("service mode not supported on Linux, falling back to direct mode") + s.acceptLoop() +} + +func (s *Server) platformSessionManager() virtualSessionManager { + return newSessionManager(s.log) +} diff --git a/client/vnc/server/session.go b/client/vnc/server/session.go new file mode 100644 index 000000000..2a19a6392 --- /dev/null +++ b/client/vnc/server/session.go @@ -0,0 +1,451 @@ +package server + +import ( + "bytes" + "crypto/rand" + "encoding/binary" + "fmt" + "image" + "io" + "net" + "sync" + "time" + + log "github.com/sirupsen/logrus" +) + +const ( + readDeadline = 60 * time.Second + maxCutTextBytes = 1 << 20 // 1 MiB +) + +const tileSize = 64 // pixels per tile for dirty-rect detection + +type session struct { + conn net.Conn + capturer ScreenCapturer + injector InputInjector + serverW int + serverH int + password string + log *log.Entry + recorder *vncRecorder + + writeMu sync.Mutex + pf clientPixelFormat + useZlib bool + zlib *zlibState + prevFrame *image.RGBA + idleFrames int +} + +func (s *session) addr() string { return s.conn.RemoteAddr().String() } + +// serve runs the full RFB session lifecycle. +func (s *session) serve() { + defer s.conn.Close() + if s.recorder != nil { + defer s.recorder.close() + } + s.pf = defaultClientPixelFormat() + + if err := s.handshake(); err != nil { + s.log.Warnf("handshake with %s: %v", s.addr(), err) + return + } + s.log.Infof("client connected: %s", s.addr()) + + done := make(chan struct{}) + defer close(done) + go s.clipboardPoll(done) + + if err := s.messageLoop(); err != nil && err != io.EOF { + s.log.Warnf("client %s disconnected: %v", s.addr(), err) + } else { + s.log.Infof("client disconnected: %s", s.addr()) + } +} + +// clipboardPoll periodically checks the server-side clipboard and sends +// changes to the VNC client. Only runs during active sessions. +func (s *session) clipboardPoll(done <-chan struct{}) { + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + var lastClip string + for { + select { + case <-done: + return + case <-ticker.C: + text := s.injector.GetClipboard() + if len(text) > maxCutTextBytes { + text = text[:maxCutTextBytes] + } + if text != "" && text != lastClip { + lastClip = text + if err := s.sendServerCutText(text); err != nil { + s.log.Debugf("send clipboard to client: %v", err) + return + } + } + } + } +} + +func (s *session) handshake() error { + // Send protocol version. + if _, err := io.WriteString(s.conn, rfbProtocolVersion); err != nil { + return fmt.Errorf("send version: %w", err) + } + + // Read client version. + var clientVer [12]byte + if _, err := io.ReadFull(s.conn, clientVer[:]); err != nil { + return fmt.Errorf("read client version: %w", err) + } + + // Send supported security types. + if err := s.sendSecurityTypes(); err != nil { + return err + } + + // Read chosen security type. + var secType [1]byte + if _, err := io.ReadFull(s.conn, secType[:]); err != nil { + return fmt.Errorf("read security type: %w", err) + } + + if err := s.handleSecurity(secType[0]); err != nil { + return err + } + + // Read ClientInit. + var clientInit [1]byte + if _, err := io.ReadFull(s.conn, clientInit[:]); err != nil { + return fmt.Errorf("read ClientInit: %w", err) + } + + return s.sendServerInit() +} + +func (s *session) sendSecurityTypes() error { + if s.password == "" { + _, err := s.conn.Write([]byte{1, secNone}) + return err + } + _, err := s.conn.Write([]byte{1, secVNCAuth}) + return err +} + +func (s *session) handleSecurity(secType byte) error { + switch secType { + case secVNCAuth: + return s.doVNCAuth() + case secNone: + return binary.Write(s.conn, binary.BigEndian, uint32(0)) + default: + return fmt.Errorf("unsupported security type: %d", secType) + } +} + +func (s *session) doVNCAuth() error { + challenge := make([]byte, 16) + if _, err := rand.Read(challenge); err != nil { + return fmt.Errorf("generate challenge: %w", err) + } + if _, err := s.conn.Write(challenge); err != nil { + return fmt.Errorf("send challenge: %w", err) + } + + response := make([]byte, 16) + if _, err := io.ReadFull(s.conn, response); err != nil { + return fmt.Errorf("read auth response: %w", err) + } + + var result uint32 + if s.password != "" { + expected := vncAuthEncrypt(challenge, s.password) + if !bytes.Equal(expected, response) { + result = 1 + } + } + + if err := binary.Write(s.conn, binary.BigEndian, result); err != nil { + return fmt.Errorf("send auth result: %w", err) + } + if result != 0 { + msg := "authentication failed" + _ = binary.Write(s.conn, binary.BigEndian, uint32(len(msg))) + _, _ = s.conn.Write([]byte(msg)) + return fmt.Errorf("authentication failed from %s", s.addr()) + } + return nil +} + +func (s *session) sendServerInit() error { + name := []byte("NetBird VNC") + buf := make([]byte, 0, 4+16+4+len(name)) + + // Framebuffer width and height. + buf = append(buf, byte(s.serverW>>8), byte(s.serverW)) + buf = append(buf, byte(s.serverH>>8), byte(s.serverH)) + + // Server pixel format. + buf = append(buf, serverPixelFormat[:]...) + + // Desktop name. + buf = append(buf, + byte(len(name)>>24), byte(len(name)>>16), + byte(len(name)>>8), byte(len(name)), + ) + buf = append(buf, name...) + + _, err := s.conn.Write(buf) + return err +} + +func (s *session) messageLoop() error { + for { + var msgType [1]byte + if err := s.conn.SetDeadline(time.Now().Add(readDeadline)); err != nil { + return fmt.Errorf("set deadline: %w", err) + } + if _, err := io.ReadFull(s.conn, msgType[:]); err != nil { + return err + } + _ = s.conn.SetDeadline(time.Time{}) + + switch msgType[0] { + case clientSetPixelFormat: + if err := s.handleSetPixelFormat(); err != nil { + return err + } + case clientSetEncodings: + if err := s.handleSetEncodings(); err != nil { + return err + } + case clientFramebufferUpdateRequest: + if err := s.handleFBUpdateRequest(); err != nil { + return err + } + case clientKeyEvent: + if err := s.handleKeyEvent(); err != nil { + return err + } + case clientPointerEvent: + if err := s.handlePointerEvent(); err != nil { + return err + } + case clientCutText: + if err := s.handleCutText(); err != nil { + return err + } + default: + return fmt.Errorf("unknown client message type: %d", msgType[0]) + } + } +} + +func (s *session) handleSetPixelFormat() error { + var buf [19]byte // 3 padding + 16 pixel format + if _, err := io.ReadFull(s.conn, buf[:]); err != nil { + return fmt.Errorf("read SetPixelFormat: %w", err) + } + s.pf = parsePixelFormat(buf[3:19]) + return nil +} + +func (s *session) handleSetEncodings() error { + var header [3]byte // 1 padding + 2 number-of-encodings + if _, err := io.ReadFull(s.conn, header[:]); err != nil { + return fmt.Errorf("read SetEncodings header: %w", err) + } + numEnc := binary.BigEndian.Uint16(header[1:3]) + buf := make([]byte, int(numEnc)*4) + if _, err := io.ReadFull(s.conn, buf); err != nil { + return err + } + + // Check if client supports zlib encoding. + for i := range int(numEnc) { + enc := int32(binary.BigEndian.Uint32(buf[i*4 : i*4+4])) + if enc == encZlib { + s.useZlib = true + if s.zlib == nil { + s.zlib = newZlibState() + } + s.log.Debugf("client supports zlib encoding") + break + } + } + return nil +} + +func (s *session) handleFBUpdateRequest() error { + var req [9]byte + if _, err := io.ReadFull(s.conn, req[:]); err != nil { + return fmt.Errorf("read FBUpdateRequest: %w", err) + } + incremental := req[0] + + img, err := s.capturer.Capture() + if err != nil { + return fmt.Errorf("capture screen: %w", err) + } + + if s.recorder != nil { + s.recorder.writeFrame(img) + } + + if incremental == 1 && s.prevFrame != nil { + rects := diffRects(s.prevFrame, img, s.serverW, s.serverH, tileSize) + if len(rects) == 0 { + // Nothing changed. Back off briefly before responding to reduce + // CPU usage when the screen is static. The client re-requests + // immediately after receiving our empty response, so without + // this delay we'd spin at ~1000fps checking for changes. + s.idleFrames++ + delay := min(s.idleFrames*5, 100) // 5ms → 100ms adaptive backoff + time.Sleep(time.Duration(delay) * time.Millisecond) + s.savePrevFrame(img) + return s.sendEmptyUpdate() + } + s.idleFrames = 0 + s.savePrevFrame(img) + return s.sendDirtyRects(img, rects) + } + + // Full update. + s.idleFrames = 0 + s.savePrevFrame(img) + return s.sendFullUpdate(img) +} + +// savePrevFrame copies img's pixel data into prevFrame. This is necessary +// because some capturers (DXGI) reuse the same image buffer across calls, +// so a simple pointer assignment would make prevFrame alias the live buffer +// and diffRects would always see zero changes. +func (s *session) savePrevFrame(img *image.RGBA) { + if s.prevFrame == nil || s.prevFrame.Rect != img.Rect { + s.prevFrame = image.NewRGBA(img.Rect) + } + copy(s.prevFrame.Pix, img.Pix) +} + +// sendEmptyUpdate sends a FramebufferUpdate with zero rectangles. +func (s *session) sendEmptyUpdate() error { + var buf [4]byte + buf[0] = serverFramebufferUpdate + s.writeMu.Lock() + _, err := s.conn.Write(buf[:]) + s.writeMu.Unlock() + return err +} + +func (s *session) sendFullUpdate(img *image.RGBA) error { + w, h := s.serverW, s.serverH + + var buf []byte + if s.useZlib && s.zlib != nil { + buf = encodeZlibRect(img, s.pf, 0, 0, w, h, s.zlib.w, s.zlib.buf) + } else { + buf = encodeRawRect(img, s.pf, 0, 0, w, h) + } + + s.writeMu.Lock() + _, err := s.conn.Write(buf) + s.writeMu.Unlock() + return err +} + +func (s *session) sendDirtyRects(img *image.RGBA, rects [][4]int) error { + // Build a multi-rectangle FramebufferUpdate. + // Header: type(1) + padding(1) + numRects(2) + header := make([]byte, 4) + header[0] = serverFramebufferUpdate + binary.BigEndian.PutUint16(header[2:4], uint16(len(rects))) + + s.writeMu.Lock() + defer s.writeMu.Unlock() + + if _, err := s.conn.Write(header); err != nil { + return err + } + + for _, r := range rects { + x, y, w, h := r[0], r[1], r[2], r[3] + + var rectBuf []byte + if s.useZlib && s.zlib != nil { + rectBuf = encodeZlibRect(img, s.pf, x, y, w, h, s.zlib.w, s.zlib.buf) + // encodeZlibRect includes its own FBUpdate header for 1 rect. + // For multi-rect, we need just the rect data without the FBUpdate header. + // Skip the 4-byte FBUpdate header since we already sent ours. + rectBuf = rectBuf[4:] + } else { + rectBuf = encodeRawRect(img, s.pf, x, y, w, h) + rectBuf = rectBuf[4:] // skip FBUpdate header + } + + if _, err := s.conn.Write(rectBuf); err != nil { + return err + } + } + return nil +} + +func (s *session) handleKeyEvent() error { + var data [7]byte + if _, err := io.ReadFull(s.conn, data[:]); err != nil { + return fmt.Errorf("read KeyEvent: %w", err) + } + down := data[0] == 1 + keysym := binary.BigEndian.Uint32(data[3:7]) + s.injector.InjectKey(keysym, down) + return nil +} + +func (s *session) handlePointerEvent() error { + var data [5]byte + if _, err := io.ReadFull(s.conn, data[:]); err != nil { + return fmt.Errorf("read PointerEvent: %w", err) + } + buttonMask := data[0] + x := int(binary.BigEndian.Uint16(data[1:3])) + y := int(binary.BigEndian.Uint16(data[3:5])) + s.injector.InjectPointer(buttonMask, x, y, s.serverW, s.serverH) + return nil +} + +func (s *session) handleCutText() error { + var header [7]byte // 3 padding + 4 length + if _, err := io.ReadFull(s.conn, header[:]); err != nil { + return fmt.Errorf("read CutText header: %w", err) + } + length := binary.BigEndian.Uint32(header[3:7]) + if length > maxCutTextBytes { + return fmt.Errorf("cut text too large: %d bytes", length) + } + buf := make([]byte, length) + if _, err := io.ReadFull(s.conn, buf); err != nil { + return fmt.Errorf("read CutText payload: %w", err) + } + s.injector.SetClipboard(string(buf)) + return nil +} + +// sendServerCutText sends clipboard text from the server to the client. +func (s *session) sendServerCutText(text string) error { + data := []byte(text) + buf := make([]byte, 8+len(data)) + buf[0] = serverCutText + // buf[1:4] = padding (zero) + binary.BigEndian.PutUint32(buf[4:8], uint32(len(data))) + copy(buf[8:], data) + + s.writeMu.Lock() + _, err := s.conn.Write(buf) + s.writeMu.Unlock() + return err +} diff --git a/client/vnc/server/shutdown_state.go b/client/vnc/server/shutdown_state.go new file mode 100644 index 000000000..6f7ed8b73 --- /dev/null +++ b/client/vnc/server/shutdown_state.go @@ -0,0 +1,79 @@ +//go:build !windows + +package server + +import ( + "fmt" + "os" + "strings" + "syscall" + + log "github.com/sirupsen/logrus" +) + +// ShutdownState tracks VNC virtual session processes for crash recovery. +// Persisted by the state manager; on restart, residual processes are killed. +type ShutdownState struct { + // Processes maps a description to its PID (e.g., "xvfb:50" -> 1234). + Processes map[string]int `json:"processes,omitempty"` +} + +// Name returns the state name for the state manager. +func (s *ShutdownState) Name() string { + return "vnc_sessions_state" +} + +// Cleanup kills any residual VNC session processes left from a crash. +func (s *ShutdownState) Cleanup() error { + if len(s.Processes) == 0 { + return nil + } + + for desc, pid := range s.Processes { + if pid <= 0 { + continue + } + if !isOurProcess(pid, desc) { + log.Debugf("cleanup:skipping PID %d (%s), not ours", pid, desc) + continue + } + log.Infof("cleanup:killing residual process %d (%s)", pid, desc) + // Kill the process group (negative PID) to get children too. + if err := syscall.Kill(-pid, syscall.SIGTERM); err != nil { + // Try individual process if group kill fails. + syscall.Kill(pid, syscall.SIGKILL) + } + } + + s.Processes = nil + return nil +} + +// isOurProcess verifies the PID still belongs to a VNC-related process +// by checking /proc//cmdline (Linux) or the process name. +func isOurProcess(pid int, desc string) bool { + // Check if the process exists at all. + if err := syscall.Kill(pid, 0); err != nil { + return false + } + + // On Linux, verify via /proc cmdline. + cmdline, err := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid)) + if err != nil { + // No /proc (FreeBSD): trust the PID if the process exists. + // PID reuse is unlikely in the short window between crash and restart. + return true + } + + cmd := string(cmdline) + // Match against expected process types. + if strings.Contains(desc, "xvfb") || strings.Contains(desc, "xorg") { + return strings.Contains(cmd, "Xvfb") || strings.Contains(cmd, "Xorg") + } + if strings.Contains(desc, "desktop") { + return strings.Contains(cmd, "session") || strings.Contains(cmd, "plasma") || + strings.Contains(cmd, "gnome") || strings.Contains(cmd, "xfce") || + strings.Contains(cmd, "dbus-launch") + } + return false +} diff --git a/client/vnc/server/stubs.go b/client/vnc/server/stubs.go new file mode 100644 index 000000000..436b531a2 --- /dev/null +++ b/client/vnc/server/stubs.go @@ -0,0 +1,37 @@ +package server + +import ( + "fmt" + "image" +) + +const maxCapturerRetries = 5 + +// StubCapturer is a placeholder for platforms without screen capture support. +type StubCapturer struct{} + +// Width returns 0 on unsupported platforms. +func (c *StubCapturer) Width() int { return 0 } + +// Height returns 0 on unsupported platforms. +func (c *StubCapturer) Height() int { return 0 } + +// Capture returns an error on unsupported platforms. +func (c *StubCapturer) Capture() (*image.RGBA, error) { + return nil, fmt.Errorf("screen capture not supported on this platform") +} + +// StubInputInjector is a placeholder for platforms without input injection support. +type StubInputInjector struct{} + +// InjectKey is a no-op on unsupported platforms. +func (s *StubInputInjector) InjectKey(_ uint32, _ bool) {} + +// InjectPointer is a no-op on unsupported platforms. +func (s *StubInputInjector) InjectPointer(_ uint8, _, _, _, _ int) {} + +// SetClipboard is a no-op on unsupported platforms. +func (s *StubInputInjector) SetClipboard(_ string) {} + +// GetClipboard returns empty on unsupported platforms. +func (s *StubInputInjector) GetClipboard() string { return "" } diff --git a/client/vnc/server/swizzle_windows.go b/client/vnc/server/swizzle_windows.go new file mode 100644 index 000000000..5966bbf4a --- /dev/null +++ b/client/vnc/server/swizzle_windows.go @@ -0,0 +1,19 @@ +//go:build windows + +package server + +import "unsafe" + +// swizzleBGRAtoRGBA swaps B and R channels in a BGRA pixel buffer in-place. +// Operates on uint32 words for throughput: one read-modify-write per pixel. +func swizzleBGRAtoRGBA(pix []byte) { + n := len(pix) / 4 + pixels := unsafe.Slice((*uint32)(unsafe.Pointer(&pix[0])), n) + for i := range n { + p := pixels[i] + // p = 0xAABBGGRR (little-endian BGRA in memory: B,G,R,A bytes) + // We want 0xAABBGGRR -> 0xAARRGGBB (RGBA in memory: R,G,B,A bytes) + // Swap byte 0 (B) and byte 2 (R), keep byte 1 (G) and byte 3 (A). + pixels[i] = (p & 0xFF00FF00) | ((p & 0x00FF0000) >> 16) | ((p & 0x000000FF) << 16) + } +} diff --git a/client/vnc/server/virtual_x11.go b/client/vnc/server/virtual_x11.go new file mode 100644 index 000000000..f31d890c8 --- /dev/null +++ b/client/vnc/server/virtual_x11.go @@ -0,0 +1,634 @@ +//go:build (linux && !android) || freebsd + +package server + +import ( + "fmt" + "os" + "os/exec" + "os/user" + "path/filepath" + "strconv" + "strings" + "sync" + "syscall" + "time" + + log "github.com/sirupsen/logrus" +) + +// VirtualSession manages a virtual X11 display (Xvfb) with a desktop session +// running as a target user. It implements ScreenCapturer and InputInjector by +// delegating to an X11Capturer/X11InputInjector pointed at the virtual display. +const sessionIdleTimeout = 5 * time.Minute + +type VirtualSession struct { + mu sync.Mutex + display string + user *user.User + uid uint32 + gid uint32 + groups []uint32 + xvfb *exec.Cmd + desktop *exec.Cmd + poller *X11Poller + injector *X11InputInjector + log *log.Entry + stopped bool + clients int + idleTimer *time.Timer + onIdle func() // called when idle timeout fires or Xvfb dies +} + +// StartVirtualSession creates and starts a virtual X11 session for the given user. +// Requires root privileges to create sessions as other users. +func StartVirtualSession(username string, logger *log.Entry) (*VirtualSession, error) { + if os.Getuid() != 0 { + return nil, fmt.Errorf("virtual sessions require root privileges") + } + + if _, err := exec.LookPath("Xvfb"); err != nil { + if _, err := exec.LookPath("Xorg"); err != nil { + return nil, fmt.Errorf("neither Xvfb nor Xorg found (install xvfb or xserver-xorg)") + } + if !hasDummyDriver() { + return nil, fmt.Errorf("Xvfb not found and Xorg dummy driver not installed (install xvfb or xf86-video-dummy)") + } + } + + u, err := user.Lookup(username) + if err != nil { + return nil, fmt.Errorf("lookup user %s: %w", username, err) + } + + uid, err := strconv.ParseUint(u.Uid, 10, 32) + if err != nil { + return nil, fmt.Errorf("parse uid: %w", err) + } + gid, err := strconv.ParseUint(u.Gid, 10, 32) + if err != nil { + return nil, fmt.Errorf("parse gid: %w", err) + } + + groups, err := supplementaryGroups(u) + if err != nil { + logger.Debugf("supplementary groups for %s: %v", username, err) + } + + vs := &VirtualSession{ + user: u, + uid: uint32(uid), + gid: uint32(gid), + groups: groups, + log: logger.WithField("vnc_user", username), + } + + if err := vs.start(); err != nil { + return nil, err + } + return vs, nil +} + +func (vs *VirtualSession) start() error { + display, err := findFreeDisplay() + if err != nil { + return fmt.Errorf("find free display: %w", err) + } + vs.display = display + + if err := vs.startXvfb(); err != nil { + return err + } + + socketPath := fmt.Sprintf("/tmp/.X11-unix/X%s", vs.display[1:]) + if err := waitForPath(socketPath, 5*time.Second); err != nil { + vs.stopXvfb() + return fmt.Errorf("wait for X11 socket %s: %w", socketPath, err) + } + + // Grant the target user access to the display via xhost. + xhostCmd := exec.Command("xhost", "+SI:localuser:"+vs.user.Username) + xhostCmd.Env = []string{"DISPLAY=" + vs.display} + if out, err := xhostCmd.CombinedOutput(); err != nil { + vs.log.Debugf("xhost: %s (%v)", strings.TrimSpace(string(out)), err) + } + + vs.poller = NewX11Poller(vs.display) + + injector, err := NewX11InputInjector(vs.display) + if err != nil { + vs.stopXvfb() + return fmt.Errorf("create X11 injector for %s: %w", vs.display, err) + } + vs.injector = injector + + if err := vs.startDesktop(); err != nil { + vs.injector.Close() + vs.stopXvfb() + return fmt.Errorf("start desktop: %w", err) + } + + vs.log.Infof("virtual session started: display=%s user=%s", vs.display, vs.user.Username) + return nil +} + +// ClientConnect increments the client count and cancels any idle timer. +func (vs *VirtualSession) ClientConnect() { + vs.mu.Lock() + defer vs.mu.Unlock() + vs.clients++ + if vs.idleTimer != nil { + vs.idleTimer.Stop() + vs.idleTimer = nil + } +} + +// ClientDisconnect decrements the client count. When the last client +// disconnects, starts an idle timer that destroys the session. +func (vs *VirtualSession) ClientDisconnect() { + vs.mu.Lock() + defer vs.mu.Unlock() + vs.clients-- + if vs.clients <= 0 { + vs.clients = 0 + vs.log.Infof("no VNC clients connected, session will be destroyed in %s", sessionIdleTimeout) + vs.idleTimer = time.AfterFunc(sessionIdleTimeout, vs.idleExpired) + } +} + +// idleExpired is called by the idle timer. It stops the session and +// notifies the session manager via onIdle so it removes us from the map. +func (vs *VirtualSession) idleExpired() { + vs.log.Info("idle timeout reached, destroying virtual session") + vs.Stop() + // onIdle acquires sessionManager.mu; safe because Stop() has released vs.mu. + if vs.onIdle != nil { + vs.onIdle() + } +} + +// isAlive returns true if the session is running and its X server socket exists. +func (vs *VirtualSession) isAlive() bool { + vs.mu.Lock() + stopped := vs.stopped + display := vs.display + vs.mu.Unlock() + + if stopped { + return false + } + // Verify the X socket still exists on disk. + socketPath := fmt.Sprintf("/tmp/.X11-unix/X%s", display[1:]) + if _, err := os.Stat(socketPath); err != nil { + return false + } + return true +} + +// Capturer returns the screen capturer for this virtual session. +func (vs *VirtualSession) Capturer() ScreenCapturer { + return vs.poller +} + +// Injector returns the input injector for this virtual session. +func (vs *VirtualSession) Injector() InputInjector { + return vs.injector +} + +// Display returns the X11 display string (e.g., ":99"). +func (vs *VirtualSession) Display() string { + return vs.display +} + +// Stop terminates the virtual session, killing the desktop and Xvfb. +func (vs *VirtualSession) Stop() { + vs.mu.Lock() + defer vs.mu.Unlock() + + if vs.stopped { + return + } + vs.stopped = true + + if vs.injector != nil { + vs.injector.Close() + } + + vs.stopDesktop() + vs.stopXvfb() + + vs.log.Info("virtual session stopped") +} + +func (vs *VirtualSession) startXvfb() error { + if _, err := exec.LookPath("Xvfb"); err == nil { + return vs.startXvfbDirect() + } + return vs.startXorgDummy() +} + +func (vs *VirtualSession) startXvfbDirect() error { + vs.xvfb = exec.Command("Xvfb", vs.display, + "-screen", "0", "1280x800x24", + "-ac", + "-nolisten", "tcp", + ) + vs.xvfb.SysProcAttr = &syscall.SysProcAttr{Setsid: true, Pdeathsig: syscall.SIGTERM} + + if err := vs.xvfb.Start(); err != nil { + return fmt.Errorf("start Xvfb on %s: %w", vs.display, err) + } + vs.log.Infof("Xvfb started on %s (pid=%d)", vs.display, vs.xvfb.Process.Pid) + + go vs.monitorXvfb() + + return nil +} + +// startXorgDummy starts Xorg with the dummy video driver as a fallback when +// Xvfb is not installed. Most systems with a desktop have Xorg available. +func (vs *VirtualSession) startXorgDummy() error { + confPath := fmt.Sprintf("/tmp/nbvnc-dummy-%s.conf", vs.display[1:]) + conf := `Section "Device" + Identifier "dummy" + Driver "dummy" + VideoRam 256000 +EndSection +Section "Screen" + Identifier "screen" + Device "dummy" + DefaultDepth 24 + SubSection "Display" + Depth 24 + Modes "1280x800" + EndSubSection +EndSection +` + if err := os.WriteFile(confPath, []byte(conf), 0644); err != nil { + return fmt.Errorf("write Xorg dummy config: %w", err) + } + + vs.xvfb = exec.Command("Xorg", vs.display, + "-config", confPath, + "-noreset", + "-nolisten", "tcp", + "-ac", + ) + vs.xvfb.SysProcAttr = &syscall.SysProcAttr{Setsid: true, Pdeathsig: syscall.SIGTERM} + + if err := vs.xvfb.Start(); err != nil { + os.Remove(confPath) + return fmt.Errorf("start Xorg dummy on %s: %w", vs.display, err) + } + vs.log.Infof("Xorg (dummy driver) started on %s (pid=%d)", vs.display, vs.xvfb.Process.Pid) + + go func() { + vs.monitorXvfb() + os.Remove(confPath) + }() + + return nil +} + +// monitorXvfb waits for the Xvfb/Xorg process to exit. If it exits +// unexpectedly (not via Stop), the session is marked as dead and the +// onIdle callback fires so the session manager removes it from the map. +// The next GetOrCreate call for this user will create a fresh session. +func (vs *VirtualSession) monitorXvfb() { + if err := vs.xvfb.Wait(); err != nil { + vs.log.Debugf("X server exited: %v", err) + } + + vs.mu.Lock() + alreadyStopped := vs.stopped + if !alreadyStopped { + vs.log.Warn("X server exited unexpectedly, marking session as dead") + vs.stopped = true + if vs.idleTimer != nil { + vs.idleTimer.Stop() + vs.idleTimer = nil + } + if vs.injector != nil { + vs.injector.Close() + } + vs.stopDesktop() + } + onIdle := vs.onIdle + vs.mu.Unlock() + + if !alreadyStopped && onIdle != nil { + onIdle() + } +} + +func (vs *VirtualSession) stopXvfb() { + if vs.xvfb != nil && vs.xvfb.Process != nil { + syscall.Kill(-vs.xvfb.Process.Pid, syscall.SIGTERM) + time.Sleep(200 * time.Millisecond) + syscall.Kill(-vs.xvfb.Process.Pid, syscall.SIGKILL) + } +} + +func (vs *VirtualSession) startDesktop() error { + session := detectDesktopSession() + + // Wrap the desktop command with dbus-launch to provide a session bus. + // Without this, most desktop environments (XFCE, MATE, etc.) fail immediately. + var args []string + if _, err := exec.LookPath("dbus-launch"); err == nil { + args = append([]string{"dbus-launch", "--exit-with-session"}, session...) + } else { + args = session + } + + vs.desktop = exec.Command(args[0], args[1:]...) + vs.desktop.Dir = vs.user.HomeDir + vs.desktop.Env = vs.buildUserEnv() + vs.desktop.SysProcAttr = &syscall.SysProcAttr{ + Credential: &syscall.Credential{ + Uid: vs.uid, + Gid: vs.gid, + Groups: vs.groups, + }, + Setsid: true, + Pdeathsig: syscall.SIGTERM, + } + + if err := vs.desktop.Start(); err != nil { + return fmt.Errorf("start desktop session (%v): %w", args, err) + } + vs.log.Infof("desktop session started: %v (pid=%d)", args, vs.desktop.Process.Pid) + + go func() { + if err := vs.desktop.Wait(); err != nil { + vs.log.Debugf("desktop session exited: %v", err) + } + }() + + return nil +} + +func (vs *VirtualSession) stopDesktop() { + if vs.desktop != nil && vs.desktop.Process != nil { + syscall.Kill(-vs.desktop.Process.Pid, syscall.SIGTERM) + time.Sleep(200 * time.Millisecond) + syscall.Kill(-vs.desktop.Process.Pid, syscall.SIGKILL) + } +} + +func (vs *VirtualSession) buildUserEnv() []string { + return []string{ + "DISPLAY=" + vs.display, + "HOME=" + vs.user.HomeDir, + "USER=" + vs.user.Username, + "LOGNAME=" + vs.user.Username, + "SHELL=" + getUserShell(vs.user.Uid), + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "XDG_RUNTIME_DIR=/run/user/" + vs.user.Uid, + "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/" + vs.user.Uid + "/bus", + } +} + +// detectDesktopSession discovers available desktop sessions from the standard +// /usr/share/xsessions/*.desktop files (FreeDesktop standard, used by all +// display managers). Falls back to a hardcoded list if no .desktop files found. +func detectDesktopSession() []string { + // Scan xsessions directories (Linux: /usr/share, FreeBSD: /usr/local/share). + for _, dir := range []string{"/usr/share/xsessions", "/usr/local/share/xsessions"} { + if cmd := findXSession(dir); cmd != nil { + return cmd + } + } + + // Fallback: try common session commands directly. + fallbacks := [][]string{ + {"startplasma-x11"}, + {"gnome-session"}, + {"xfce4-session"}, + {"mate-session"}, + {"cinnamon-session"}, + {"openbox-session"}, + {"xterm"}, + } + for _, s := range fallbacks { + if _, err := exec.LookPath(s[0]); err == nil { + return s + } + } + return []string{"xterm"} +} + +// sessionPriority defines preference order for desktop environments. +// Lower number = higher priority. Unknown sessions get 100. +var sessionPriority = map[string]int{ + "plasma": 1, // KDE + "gnome": 2, + "xfce": 3, + "mate": 4, + "cinnamon": 5, + "lxqt": 6, + "lxde": 7, + "budgie": 8, + "openbox": 20, + "fluxbox": 21, + "i3": 22, + "xinit": 50, // generic user session + "lightdm": 50, + "default": 50, +} + +func findXSession(dir string) []string { + entries, err := os.ReadDir(dir) + if err != nil { + return nil + } + + type candidate struct { + cmd string + priority int + } + var candidates []candidate + + for _, e := range entries { + if !strings.HasSuffix(e.Name(), ".desktop") { + continue + } + data, err := os.ReadFile(filepath.Join(dir, e.Name())) + if err != nil { + continue + } + execCmd := "" + for _, line := range strings.Split(string(data), "\n") { + if strings.HasPrefix(line, "Exec=") { + execCmd = strings.TrimSpace(strings.TrimPrefix(line, "Exec=")) + break + } + } + if execCmd == "" || execCmd == "default" { + continue + } + + // Determine priority from the filename or exec command. + pri := 100 + lower := strings.ToLower(e.Name() + " " + execCmd) + for keyword, p := range sessionPriority { + if strings.Contains(lower, keyword) && p < pri { + pri = p + } + } + candidates = append(candidates, candidate{cmd: execCmd, priority: pri}) + } + + if len(candidates) == 0 { + return nil + } + + // Pick the highest priority (lowest number). + best := candidates[0] + for _, c := range candidates[1:] { + if c.priority < best.priority { + best = c + } + } + + // Verify the binary exists. + parts := strings.Fields(best.cmd) + if _, err := exec.LookPath(parts[0]); err != nil { + return nil + } + return parts +} + +// findFreeDisplay scans for an unused X11 display number. +func findFreeDisplay() (string, error) { + for n := 50; n < 200; n++ { + lockFile := fmt.Sprintf("/tmp/.X%d-lock", n) + socketFile := fmt.Sprintf("/tmp/.X11-unix/X%d", n) + if _, err := os.Stat(lockFile); err == nil { + continue + } + if _, err := os.Stat(socketFile); err == nil { + continue + } + return fmt.Sprintf(":%d", n), nil + } + return "", fmt.Errorf("no free X11 display found (checked :50-:199)") +} + +// waitForPath polls until a filesystem path exists or the timeout expires. +func waitForPath(path string, timeout time.Duration) error { + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + if _, err := os.Stat(path); err == nil { + return nil + } + time.Sleep(50 * time.Millisecond) + } + return fmt.Errorf("timeout waiting for %s", path) +} + +// getUserShell returns the login shell for the given UID. +func getUserShell(uid string) string { + data, err := os.ReadFile("/etc/passwd") + if err != nil { + return "/bin/sh" + } + for _, line := range strings.Split(string(data), "\n") { + fields := strings.Split(line, ":") + if len(fields) >= 7 && fields[2] == uid { + return fields[6] + } + } + return "/bin/sh" +} + +// supplementaryGroups returns the supplementary group IDs for a user. +func supplementaryGroups(u *user.User) ([]uint32, error) { + gids, err := u.GroupIds() + if err != nil { + return nil, err + } + var groups []uint32 + for _, g := range gids { + id, err := strconv.ParseUint(g, 10, 32) + if err != nil { + continue + } + groups = append(groups, uint32(id)) + } + return groups, nil +} + +// sessionManager tracks active virtual sessions by username. +type sessionManager struct { + mu sync.Mutex + sessions map[string]*VirtualSession + log *log.Entry +} + +func newSessionManager(logger *log.Entry) *sessionManager { + return &sessionManager{ + sessions: make(map[string]*VirtualSession), + log: logger, + } +} + +// GetOrCreate returns an existing virtual session or creates a new one. +// If a previous session for this user is stopped or its X server died, it is replaced. +func (sm *sessionManager) GetOrCreate(username string) (vncSession, error) { + sm.mu.Lock() + defer sm.mu.Unlock() + + if vs, ok := sm.sessions[username]; ok { + if vs.isAlive() { + return vs, nil + } + sm.log.Infof("replacing dead virtual session for %s", username) + vs.Stop() + delete(sm.sessions, username) + } + + vs, err := StartVirtualSession(username, sm.log) + if err != nil { + return nil, err + } + vs.onIdle = func() { + sm.mu.Lock() + defer sm.mu.Unlock() + if cur, ok := sm.sessions[username]; ok && cur == vs { + delete(sm.sessions, username) + sm.log.Infof("removed idle virtual session for %s", username) + } + } + sm.sessions[username] = vs + return vs, nil +} + +// hasDummyDriver checks common paths for the Xorg dummy video driver. +func hasDummyDriver() bool { + paths := []string{ + "/usr/lib/xorg/modules/drivers/dummy_drv.so", // Debian/Ubuntu + "/usr/lib64/xorg/modules/drivers/dummy_drv.so", // RHEL/Fedora + "/usr/local/lib/xorg/modules/drivers/dummy_drv.so", // FreeBSD + "/usr/lib/x86_64-linux-gnu/xorg/modules/drivers/dummy_drv.so", // Debian multiarch + } + for _, p := range paths { + if _, err := os.Stat(p); err == nil { + return true + } + } + return false +} + +// StopAll terminates all active virtual sessions. +func (sm *sessionManager) StopAll() { + sm.mu.Lock() + defer sm.mu.Unlock() + + for username, vs := range sm.sessions { + vs.Stop() + delete(sm.sessions, username) + sm.log.Infof("stopped virtual session for %s", username) + } +} + diff --git a/client/vnc/server/webplayer.go b/client/vnc/server/webplayer.go new file mode 100644 index 000000000..cc56211cc --- /dev/null +++ b/client/vnc/server/webplayer.go @@ -0,0 +1,50 @@ +package server + +import ( + _ "embed" + "fmt" + "net" + "net/http" + "os" +) + +//go:embed webplayer.html +var webPlayerHTML []byte + +// ServeWebPlayer starts a local HTTP server that serves the recording file +// and an HTML player page. Returns the URL to open. +func ServeWebPlayer(recPath, listenAddr string) (string, error) { + if listenAddr == "" { + listenAddr = "localhost:0" + } + + ln, err := net.Listen("tcp", listenAddr) + if err != nil { + return "", fmt.Errorf("listen: %w", err) + } + + mux := http.NewServeMux() + + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write(webPlayerHTML) //nolint:errcheck + }) + + mux.HandleFunc("/recording.rec", func(w http.ResponseWriter, r *http.Request) { + f, err := os.Open(recPath) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + defer f.Close() + fi, _ := f.Stat() + w.Header().Set("Content-Type", "application/octet-stream") + http.ServeContent(w, r, "recording.rec", fi.ModTime(), f) + }) + + url := fmt.Sprintf("http://%s", ln.Addr()) + + go http.Serve(ln, mux) //nolint:errcheck + + return url, nil +} diff --git a/client/vnc/server/webplayer.html b/client/vnc/server/webplayer.html new file mode 100644 index 000000000..03a69dbc5 --- /dev/null +++ b/client/vnc/server/webplayer.html @@ -0,0 +1,291 @@ + + + +NetBird - VNC Session Recording + + + + +
+ + + 0:00 / 0:00 + + + Loading... +
+
+ + + + + diff --git a/client/vnc/testpage/index.html b/client/vnc/testpage/index.html new file mode 100644 index 000000000..0d73d59e0 --- /dev/null +++ b/client/vnc/testpage/index.html @@ -0,0 +1,174 @@ + + + + VNC Test + + + +
+ Loading WASM... + + +
+
+
+ + + + diff --git a/client/vnc/testpage/serve.go b/client/vnc/testpage/serve.go new file mode 100644 index 000000000..f7fca4c63 --- /dev/null +++ b/client/vnc/testpage/serve.go @@ -0,0 +1,44 @@ +//go:build ignore + +// Simple file server for the VNC test page. +// Usage: go run serve.go +// Then open: http://localhost:9090?host=100.0.23.250 +package main + +import ( + "fmt" + "net/http" + "os" + "path/filepath" +) + +func main() { + // Serve from the dashboard's public dir (has wasm, noVNC, etc.) + dashboardPublic := os.Getenv("DASHBOARD_PUBLIC") + if dashboardPublic == "" { + home, _ := os.UserHomeDir() + dashboardPublic = filepath.Join(home, "dev", "dashboard", "public") + } + + // Serve test page index.html from this directory + testDir, _ := os.Getwd() + + mux := http.NewServeMux() + // Test page itself + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" || r.URL.Path == "/index.html" { + http.ServeFile(w, r, filepath.Join(testDir, "index.html")) + return + } + // Everything else from dashboard public (wasm, noVNC, etc.) + http.FileServer(http.Dir(dashboardPublic)).ServeHTTP(w, r) + }) + + addr := ":9090" + fmt.Printf("VNC test page: http://localhost%s?host=\n", addr) + fmt.Printf("Serving assets from: %s\n", dashboardPublic) + if err := http.ListenAndServe(addr, mux); err != nil { + fmt.Fprintf(os.Stderr, "listen: %v\n", err) + os.Exit(1) + } +} diff --git a/client/wasm/cmd/main.go b/client/wasm/cmd/main.go index d8e50ab6d..1a2379751 100644 --- a/client/wasm/cmd/main.go +++ b/client/wasm/cmd/main.go @@ -15,8 +15,8 @@ import ( sshdetection "github.com/netbirdio/netbird/client/ssh/detection" nbstatus "github.com/netbirdio/netbird/client/status" "github.com/netbirdio/netbird/client/wasm/internal/http" - "github.com/netbirdio/netbird/client/wasm/internal/rdp" "github.com/netbirdio/netbird/client/wasm/internal/ssh" + "github.com/netbirdio/netbird/client/wasm/internal/vnc" "github.com/netbirdio/netbird/util" ) @@ -317,8 +317,13 @@ func createProxyRequestMethod(client *netbird.Client) js.Func { }) } -// createRDPProxyMethod creates the RDP proxy method -func createRDPProxyMethod(client *netbird.Client) js.Func { +// createVNCProxyMethod creates the VNC proxy method for raw TCP-over-WebSocket bridging. +// JS signature: createVNCProxy(hostname, port, mode?, username?, jwt?, sessionID?) +// mode: "attach" (default) or "session" +// username: required when mode is "session" +// jwt: authentication token (from OIDC session) +// sessionID: Windows session ID (0 = console/auto) +func createVNCProxyMethod(client *netbird.Client) js.Func { return js.FuncOf(func(_ js.Value, args []js.Value) any { if len(args) < 2 { return js.ValueOf("error: hostname and port required") @@ -335,8 +340,25 @@ func createRDPProxyMethod(client *netbird.Client) js.Func { }) } - proxy := rdp.NewRDCleanPathProxy(client) - return proxy.CreateProxy(args[0].String(), args[1].String()) + mode := "attach" + username := "" + jwtToken := "" + var sessionID uint32 + if len(args) > 2 && args[2].Type() == js.TypeString { + mode = args[2].String() + } + if len(args) > 3 && args[3].Type() == js.TypeString { + username = args[3].String() + } + if len(args) > 4 && args[4].Type() == js.TypeString { + jwtToken = args[4].String() + } + if len(args) > 5 && args[5].Type() == js.TypeNumber { + sessionID = uint32(args[5].Int()) + } + + proxy := vnc.NewVNCProxy(client) + return proxy.CreateProxy(args[0].String(), args[1].String(), mode, username, jwtToken, sessionID) }) } @@ -515,7 +537,7 @@ func createClientObject(client *netbird.Client) js.Value { obj["detectSSHServerType"] = createDetectSSHServerMethod(client) obj["createSSHConnection"] = createSSHMethod(client) obj["proxyRequest"] = createProxyRequestMethod(client) - obj["createRDPProxy"] = createRDPProxyMethod(client) + obj["createVNCProxy"] = createVNCProxyMethod(client) obj["status"] = createStatusMethod(client) obj["statusSummary"] = createStatusSummaryMethod(client) obj["statusDetail"] = createStatusDetailMethod(client) diff --git a/client/wasm/internal/rdp/cert_validation.go b/client/wasm/internal/rdp/cert_validation.go deleted file mode 100644 index 1678c3996..000000000 --- a/client/wasm/internal/rdp/cert_validation.go +++ /dev/null @@ -1,107 +0,0 @@ -//go:build js - -package rdp - -import ( - "crypto/tls" - "crypto/x509" - "fmt" - "syscall/js" - "time" - - log "github.com/sirupsen/logrus" -) - -const ( - certValidationTimeout = 60 * time.Second -) - -func (p *RDCleanPathProxy) validateCertificateWithJS(conn *proxyConnection, certChain [][]byte) (bool, error) { - if !conn.wsHandlers.Get("onCertificateRequest").Truthy() { - return false, fmt.Errorf("certificate validation handler not configured") - } - - certInfo := js.Global().Get("Object").New() - certInfo.Set("ServerAddr", conn.destination) - - certArray := js.Global().Get("Array").New() - for i, certBytes := range certChain { - uint8Array := js.Global().Get("Uint8Array").New(len(certBytes)) - js.CopyBytesToJS(uint8Array, certBytes) - certArray.SetIndex(i, uint8Array) - } - certInfo.Set("ServerCertChain", certArray) - if len(certChain) > 0 { - cert, err := x509.ParseCertificate(certChain[0]) - if err == nil { - info := js.Global().Get("Object").New() - info.Set("subject", cert.Subject.String()) - info.Set("issuer", cert.Issuer.String()) - info.Set("validFrom", cert.NotBefore.Format(time.RFC3339)) - info.Set("validTo", cert.NotAfter.Format(time.RFC3339)) - info.Set("serialNumber", cert.SerialNumber.String()) - certInfo.Set("CertificateInfo", info) - } - } - - promise := conn.wsHandlers.Call("onCertificateRequest", certInfo) - - resultChan := make(chan bool) - errorChan := make(chan error) - - promise.Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} { - result := args[0].Bool() - resultChan <- result - return nil - })).Call("catch", js.FuncOf(func(this js.Value, args []js.Value) interface{} { - errorChan <- fmt.Errorf("certificate validation failed") - return nil - })) - - select { - case result := <-resultChan: - if result { - log.Info("Certificate accepted by user") - } else { - log.Info("Certificate rejected by user") - } - return result, nil - case err := <-errorChan: - return false, err - case <-time.After(certValidationTimeout): - return false, fmt.Errorf("certificate validation timeout") - } -} - -func (p *RDCleanPathProxy) getTLSConfigWithValidation(conn *proxyConnection, requiresCredSSP bool) *tls.Config { - config := &tls.Config{ - InsecureSkipVerify: true, // We'll validate manually after handshake - VerifyConnection: func(cs tls.ConnectionState) error { - var certChain [][]byte - for _, cert := range cs.PeerCertificates { - certChain = append(certChain, cert.Raw) - } - - accepted, err := p.validateCertificateWithJS(conn, certChain) - if err != nil { - return err - } - if !accepted { - return fmt.Errorf("certificate rejected by user") - } - - return nil - }, - } - - // CredSSP (NLA) requires TLS 1.2 - it's incompatible with TLS 1.3 - if requiresCredSSP { - config.MinVersion = tls.VersionTLS12 - config.MaxVersion = tls.VersionTLS12 - } else { - config.MinVersion = tls.VersionTLS12 - config.MaxVersion = tls.VersionTLS13 - } - - return config -} diff --git a/client/wasm/internal/rdp/rdcleanpath.go b/client/wasm/internal/rdp/rdcleanpath.go deleted file mode 100644 index 16bf63bb9..000000000 --- a/client/wasm/internal/rdp/rdcleanpath.go +++ /dev/null @@ -1,344 +0,0 @@ -//go:build js - -package rdp - -import ( - "context" - "crypto/tls" - "encoding/asn1" - "errors" - "fmt" - "io" - "net" - "sync" - "syscall/js" - "time" - - log "github.com/sirupsen/logrus" -) - -const ( - RDCleanPathVersion = 3390 - RDCleanPathProxyHost = "rdcleanpath.proxy.local" - RDCleanPathProxyScheme = "ws" - - rdpDialTimeout = 15 * time.Second - - GeneralErrorCode = 1 - WSAETimedOut = 10060 - WSAEConnRefused = 10061 - WSAEConnAborted = 10053 - WSAEConnReset = 10054 - WSAEGenericError = 10050 -) - -type RDCleanPathPDU struct { - Version int64 `asn1:"tag:0,explicit"` - Error RDCleanPathErr `asn1:"tag:1,explicit,optional"` - Destination string `asn1:"utf8,tag:2,explicit,optional"` - ProxyAuth string `asn1:"utf8,tag:3,explicit,optional"` - ServerAuth string `asn1:"utf8,tag:4,explicit,optional"` - PreconnectionBlob string `asn1:"utf8,tag:5,explicit,optional"` - X224ConnectionPDU []byte `asn1:"tag:6,explicit,optional"` - ServerCertChain [][]byte `asn1:"tag:7,explicit,optional"` - ServerAddr string `asn1:"utf8,tag:9,explicit,optional"` -} - -type RDCleanPathErr struct { - ErrorCode int16 `asn1:"tag:0,explicit"` - HTTPStatusCode int16 `asn1:"tag:1,explicit,optional"` - WSALastError int16 `asn1:"tag:2,explicit,optional"` - TLSAlertCode int8 `asn1:"tag:3,explicit,optional"` -} - -type RDCleanPathProxy struct { - nbClient interface { - Dial(ctx context.Context, network, address string) (net.Conn, error) - } - activeConnections map[string]*proxyConnection - destinations map[string]string - mu sync.Mutex -} - -type proxyConnection struct { - id string - destination string - rdpConn net.Conn - tlsConn *tls.Conn - wsHandlers js.Value - ctx context.Context - cancel context.CancelFunc -} - -// NewRDCleanPathProxy creates a new RDCleanPath proxy -func NewRDCleanPathProxy(client interface { - Dial(ctx context.Context, network, address string) (net.Conn, error) -}) *RDCleanPathProxy { - return &RDCleanPathProxy{ - nbClient: client, - activeConnections: make(map[string]*proxyConnection), - } -} - -// CreateProxy creates a new proxy endpoint for the given destination -func (p *RDCleanPathProxy) CreateProxy(hostname, port string) js.Value { - destination := fmt.Sprintf("%s:%s", hostname, port) - - return js.Global().Get("Promise").New(js.FuncOf(func(_ js.Value, args []js.Value) any { - resolve := args[0] - - go func() { - proxyID := fmt.Sprintf("proxy_%d", len(p.activeConnections)) - - p.mu.Lock() - if p.destinations == nil { - p.destinations = make(map[string]string) - } - p.destinations[proxyID] = destination - p.mu.Unlock() - - proxyURL := fmt.Sprintf("%s://%s/%s", RDCleanPathProxyScheme, RDCleanPathProxyHost, proxyID) - - // Register the WebSocket handler for this specific proxy - js.Global().Set(fmt.Sprintf("handleRDCleanPathWebSocket_%s", proxyID), js.FuncOf(func(_ js.Value, args []js.Value) any { - if len(args) < 1 { - return js.ValueOf("error: requires WebSocket argument") - } - - ws := args[0] - p.HandleWebSocketConnection(ws, proxyID) - return nil - })) - - log.Infof("Created RDCleanPath proxy endpoint: %s for destination: %s", proxyURL, destination) - resolve.Invoke(proxyURL) - }() - - return nil - })) -} - -// HandleWebSocketConnection handles incoming WebSocket connections from IronRDP -func (p *RDCleanPathProxy) HandleWebSocketConnection(ws js.Value, proxyID string) { - p.mu.Lock() - destination := p.destinations[proxyID] - p.mu.Unlock() - - if destination == "" { - log.Errorf("No destination found for proxy ID: %s", proxyID) - return - } - - ctx, cancel := context.WithCancel(context.Background()) - // Don't defer cancel here - it will be called by cleanupConnection - - conn := &proxyConnection{ - id: proxyID, - destination: destination, - wsHandlers: ws, - ctx: ctx, - cancel: cancel, - } - - p.mu.Lock() - p.activeConnections[proxyID] = conn - p.mu.Unlock() - - p.setupWebSocketHandlers(ws, conn) - - log.Infof("RDCleanPath proxy WebSocket connection established for %s", proxyID) -} - -func (p *RDCleanPathProxy) setupWebSocketHandlers(ws js.Value, conn *proxyConnection) { - ws.Set("onGoMessage", js.FuncOf(func(this js.Value, args []js.Value) any { - if len(args) < 1 { - return nil - } - - data := args[0] - go p.handleWebSocketMessage(conn, data) - return nil - })) - - ws.Set("onGoClose", js.FuncOf(func(_ js.Value, args []js.Value) any { - log.Debug("WebSocket closed by JavaScript") - conn.cancel() - return nil - })) -} - -func (p *RDCleanPathProxy) handleWebSocketMessage(conn *proxyConnection, data js.Value) { - if !data.InstanceOf(js.Global().Get("Uint8Array")) { - return - } - - length := data.Get("length").Int() - bytes := make([]byte, length) - js.CopyBytesToGo(bytes, data) - - if conn.rdpConn != nil || conn.tlsConn != nil { - p.forwardToRDP(conn, bytes) - return - } - - var pdu RDCleanPathPDU - _, err := asn1.Unmarshal(bytes, &pdu) - if err != nil { - log.Warnf("Failed to parse RDCleanPath PDU: %v", err) - n := len(bytes) - if n > 20 { - n = 20 - } - log.Warnf("First %d bytes: %x", n, bytes[:n]) - - if len(bytes) > 0 && bytes[0] == 0x03 { - log.Debug("Received raw RDP packet instead of RDCleanPath PDU") - go p.handleDirectRDP(conn, bytes) - return - } - return - } - - go p.processRDCleanPathPDU(conn, pdu) -} - -func (p *RDCleanPathProxy) forwardToRDP(conn *proxyConnection, bytes []byte) { - var writer io.Writer - var connType string - - if conn.tlsConn != nil { - writer = conn.tlsConn - connType = "TLS" - } else if conn.rdpConn != nil { - writer = conn.rdpConn - connType = "TCP" - } else { - log.Error("No RDP connection available") - return - } - - if _, err := writer.Write(bytes); err != nil { - log.Errorf("Failed to write to %s: %v", connType, err) - } -} - -func (p *RDCleanPathProxy) handleDirectRDP(conn *proxyConnection, firstPacket []byte) { - defer p.cleanupConnection(conn) - - destination := conn.destination - log.Infof("Direct RDP mode: Connecting to %s via NetBird", destination) - - ctx, cancel := context.WithTimeout(conn.ctx, rdpDialTimeout) - defer cancel() - - rdpConn, err := p.nbClient.Dial(ctx, "tcp", destination) - if err != nil { - log.Errorf("Failed to connect to %s: %v", destination, err) - p.sendRDCleanPathError(conn, newWSAError(err)) - return - } - conn.rdpConn = rdpConn - - _, err = rdpConn.Write(firstPacket) - if err != nil { - log.Errorf("Failed to write first packet: %v", err) - p.sendRDCleanPathError(conn, newWSAError(err)) - return - } - - response := make([]byte, 1024) - n, err := rdpConn.Read(response) - if err != nil { - log.Errorf("Failed to read X.224 response: %v", err) - p.sendRDCleanPathError(conn, newWSAError(err)) - return - } - - p.sendToWebSocket(conn, response[:n]) - - go p.forwardWSToConn(conn, conn.rdpConn, "TCP") - go p.forwardConnToWS(conn, conn.rdpConn, "TCP") -} - -func (p *RDCleanPathProxy) cleanupConnection(conn *proxyConnection) { - log.Debugf("Cleaning up connection %s", conn.id) - conn.cancel() - if conn.tlsConn != nil { - log.Debug("Closing TLS connection") - if err := conn.tlsConn.Close(); err != nil { - log.Debugf("Error closing TLS connection: %v", err) - } - conn.tlsConn = nil - } - if conn.rdpConn != nil { - log.Debug("Closing TCP connection") - if err := conn.rdpConn.Close(); err != nil { - log.Debugf("Error closing TCP connection: %v", err) - } - conn.rdpConn = nil - } - p.mu.Lock() - delete(p.activeConnections, conn.id) - p.mu.Unlock() -} - -func (p *RDCleanPathProxy) sendToWebSocket(conn *proxyConnection, data []byte) { - if conn.wsHandlers.Get("receiveFromGo").Truthy() { - uint8Array := js.Global().Get("Uint8Array").New(len(data)) - js.CopyBytesToJS(uint8Array, data) - conn.wsHandlers.Call("receiveFromGo", uint8Array.Get("buffer")) - } else if conn.wsHandlers.Get("send").Truthy() { - uint8Array := js.Global().Get("Uint8Array").New(len(data)) - js.CopyBytesToJS(uint8Array, data) - conn.wsHandlers.Call("send", uint8Array.Get("buffer")) - } -} - -func (p *RDCleanPathProxy) sendRDCleanPathError(conn *proxyConnection, pdu RDCleanPathPDU) { - data, err := asn1.Marshal(pdu) - if err != nil { - log.Errorf("Failed to marshal error PDU: %v", err) - return - } - p.sendToWebSocket(conn, data) -} - -func errorToWSACode(err error) int16 { - if err == nil { - return WSAEGenericError - } - var netErr *net.OpError - if errors.As(err, &netErr) && netErr.Timeout() { - return WSAETimedOut - } - if errors.Is(err, context.DeadlineExceeded) { - return WSAETimedOut - } - if errors.Is(err, context.Canceled) { - return WSAEConnAborted - } - if errors.Is(err, io.EOF) { - return WSAEConnReset - } - return WSAEGenericError -} - -func newWSAError(err error) RDCleanPathPDU { - return RDCleanPathPDU{ - Version: RDCleanPathVersion, - Error: RDCleanPathErr{ - ErrorCode: GeneralErrorCode, - WSALastError: errorToWSACode(err), - }, - } -} - -func newHTTPError(statusCode int16) RDCleanPathPDU { - return RDCleanPathPDU{ - Version: RDCleanPathVersion, - Error: RDCleanPathErr{ - ErrorCode: GeneralErrorCode, - HTTPStatusCode: statusCode, - }, - } -} diff --git a/client/wasm/internal/rdp/rdcleanpath_handlers.go b/client/wasm/internal/rdp/rdcleanpath_handlers.go deleted file mode 100644 index 97bb46338..000000000 --- a/client/wasm/internal/rdp/rdcleanpath_handlers.go +++ /dev/null @@ -1,244 +0,0 @@ -//go:build js - -package rdp - -import ( - "context" - "crypto/tls" - "encoding/asn1" - "io" - "syscall/js" - - log "github.com/sirupsen/logrus" -) - -const ( - // MS-RDPBCGR: confusingly named, actually means PROTOCOL_HYBRID (CredSSP) - protocolSSL = 0x00000001 - protocolHybridEx = 0x00000008 -) - -func (p *RDCleanPathProxy) processRDCleanPathPDU(conn *proxyConnection, pdu RDCleanPathPDU) { - log.Infof("Processing RDCleanPath PDU: Version=%d, Destination=%s", pdu.Version, pdu.Destination) - - if pdu.Version != RDCleanPathVersion { - p.sendRDCleanPathError(conn, newHTTPError(400)) - return - } - - destination := conn.destination - if pdu.Destination != "" { - destination = pdu.Destination - } - - ctx, cancel := context.WithTimeout(conn.ctx, rdpDialTimeout) - defer cancel() - - rdpConn, err := p.nbClient.Dial(ctx, "tcp", destination) - if err != nil { - log.Errorf("Failed to connect to %s: %v", destination, err) - p.sendRDCleanPathError(conn, newWSAError(err)) - p.cleanupConnection(conn) - return - } - conn.rdpConn = rdpConn - - // RDP always starts with X.224 negotiation, then determines if TLS is needed - // Modern RDP (since Windows Vista/2008) typically requires TLS - // The X.224 Connection Confirm response will indicate if TLS is required - // For now, we'll attempt TLS for all connections as it's the modern default - p.setupTLSConnection(conn, pdu) -} - -// detectCredSSPFromX224 checks if the X.224 response indicates NLA/CredSSP is required. -// Per MS-RDPBCGR spec: byte 11 = TYPE_RDP_NEG_RSP (0x02), bytes 15-18 = selectedProtocol flags. -// Returns (requiresTLS12, selectedProtocol, detectionSuccessful). -func (p *RDCleanPathProxy) detectCredSSPFromX224(x224Response []byte) (bool, uint32, bool) { - const minResponseLength = 19 - - if len(x224Response) < minResponseLength { - return false, 0, false - } - - // Per X.224 specification: - // x224Response[0] == 0x03: Length of X.224 header (3 bytes) - // x224Response[5] == 0xD0: X.224 Data TPDU code - if x224Response[0] != 0x03 || x224Response[5] != 0xD0 { - return false, 0, false - } - - if x224Response[11] == 0x02 { - flags := uint32(x224Response[15]) | uint32(x224Response[16])<<8 | - uint32(x224Response[17])<<16 | uint32(x224Response[18])<<24 - - hasNLA := (flags & (protocolSSL | protocolHybridEx)) != 0 - return hasNLA, flags, true - } - - return false, 0, false -} - -func (p *RDCleanPathProxy) setupTLSConnection(conn *proxyConnection, pdu RDCleanPathPDU) { - var x224Response []byte - if len(pdu.X224ConnectionPDU) > 0 { - log.Debugf("Forwarding X.224 Connection Request (%d bytes)", len(pdu.X224ConnectionPDU)) - _, err := conn.rdpConn.Write(pdu.X224ConnectionPDU) - if err != nil { - log.Errorf("Failed to write X.224 PDU: %v", err) - p.sendRDCleanPathError(conn, newWSAError(err)) - return - } - - response := make([]byte, 1024) - n, err := conn.rdpConn.Read(response) - if err != nil { - log.Errorf("Failed to read X.224 response: %v", err) - p.sendRDCleanPathError(conn, newWSAError(err)) - return - } - x224Response = response[:n] - log.Debugf("Received X.224 Connection Confirm (%d bytes)", n) - } - - requiresCredSSP, selectedProtocol, detected := p.detectCredSSPFromX224(x224Response) - if detected { - if requiresCredSSP { - log.Warnf("Detected NLA/CredSSP (selectedProtocol: 0x%08X), forcing TLS 1.2 for compatibility", selectedProtocol) - } else { - log.Warnf("No NLA/CredSSP detected (selectedProtocol: 0x%08X), allowing up to TLS 1.3", selectedProtocol) - } - } else { - log.Warnf("Could not detect RDP security protocol, allowing up to TLS 1.3") - } - - tlsConfig := p.getTLSConfigWithValidation(conn, requiresCredSSP) - - tlsConn := tls.Client(conn.rdpConn, tlsConfig) - conn.tlsConn = tlsConn - - if err := tlsConn.Handshake(); err != nil { - log.Errorf("TLS handshake failed: %v", err) - p.sendRDCleanPathError(conn, newWSAError(err)) - return - } - - log.Info("TLS handshake successful") - - // Certificate validation happens during handshake via VerifyConnection callback - var certChain [][]byte - connState := tlsConn.ConnectionState() - if len(connState.PeerCertificates) > 0 { - for _, cert := range connState.PeerCertificates { - certChain = append(certChain, cert.Raw) - } - log.Debugf("Extracted %d certificates from TLS connection", len(certChain)) - } - - responsePDU := RDCleanPathPDU{ - Version: RDCleanPathVersion, - ServerAddr: conn.destination, - ServerCertChain: certChain, - } - - if len(x224Response) > 0 { - responsePDU.X224ConnectionPDU = x224Response - } - - p.sendRDCleanPathPDU(conn, responsePDU) - - log.Debug("Starting TLS forwarding") - go p.forwardConnToWS(conn, conn.tlsConn, "TLS") - go p.forwardWSToConn(conn, conn.tlsConn, "TLS") - - <-conn.ctx.Done() - log.Debug("TLS connection context done, cleaning up") - p.cleanupConnection(conn) -} - -func (p *RDCleanPathProxy) sendRDCleanPathPDU(conn *proxyConnection, pdu RDCleanPathPDU) { - data, err := asn1.Marshal(pdu) - if err != nil { - log.Errorf("Failed to marshal RDCleanPath PDU: %v", err) - return - } - - log.Debugf("Sending RDCleanPath PDU response (%d bytes)", len(data)) - p.sendToWebSocket(conn, data) -} - -func (p *RDCleanPathProxy) readWebSocketMessage(conn *proxyConnection) ([]byte, error) { - msgChan := make(chan []byte) - errChan := make(chan error) - - handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} { - if len(args) < 1 { - errChan <- io.EOF - return nil - } - - data := args[0] - if data.InstanceOf(js.Global().Get("Uint8Array")) { - length := data.Get("length").Int() - bytes := make([]byte, length) - js.CopyBytesToGo(bytes, data) - msgChan <- bytes - } - return nil - }) - defer handler.Release() - - conn.wsHandlers.Set("onceGoMessage", handler) - - select { - case msg := <-msgChan: - return msg, nil - case err := <-errChan: - return nil, err - case <-conn.ctx.Done(): - return nil, conn.ctx.Err() - } -} - -func (p *RDCleanPathProxy) forwardWSToConn(conn *proxyConnection, dst io.Writer, connType string) { - for { - if conn.ctx.Err() != nil { - return - } - - msg, err := p.readWebSocketMessage(conn) - if err != nil { - if err != io.EOF { - log.Errorf("Failed to read from WebSocket: %v", err) - } - return - } - - _, err = dst.Write(msg) - if err != nil { - log.Errorf("Failed to write to %s: %v", connType, err) - return - } - } -} - -func (p *RDCleanPathProxy) forwardConnToWS(conn *proxyConnection, src io.Reader, connType string) { - buffer := make([]byte, 32*1024) - - for { - if conn.ctx.Err() != nil { - return - } - - n, err := src.Read(buffer) - if err != nil { - if err != io.EOF { - log.Errorf("Failed to read from %s: %v", connType, err) - } - return - } - - if n > 0 { - p.sendToWebSocket(conn, buffer[:n]) - } - } -} diff --git a/client/wasm/internal/vnc/proxy.go b/client/wasm/internal/vnc/proxy.go new file mode 100644 index 000000000..016861e92 --- /dev/null +++ b/client/wasm/internal/vnc/proxy.go @@ -0,0 +1,360 @@ +//go:build js + +package vnc + +import ( + "context" + "fmt" + "io" + "net" + "sync" + "sync/atomic" + "syscall/js" + "time" + + log "github.com/sirupsen/logrus" +) + +const ( + vncProxyHost = "vnc.proxy.local" + vncProxyScheme = "ws" + vncDialTimeout = 15 * time.Second + + // Connection modes matching server/server.go constants. + modeAttach byte = 0 + modeSession byte = 1 +) + +// VNCProxy bridges WebSocket connections from noVNC in the browser +// to TCP VNC server connections through the NetBird tunnel. +type VNCProxy struct { + nbClient interface { + Dial(ctx context.Context, network, address string) (net.Conn, error) + } + activeConnections map[string]*vncConnection + destinations map[string]vncDestination + // pendingHandlers holds the js.Func for handleVNCWebSocket_ between + // CreateProxy and handleWebSocketConnection so we can move it onto the + // vncConnection for later release. + pendingHandlers map[string]js.Func + mu sync.Mutex + nextID atomic.Uint64 +} + +type vncDestination struct { + address string + mode byte + username string + jwt string + sessionID uint32 // Windows session ID (0 = auto/console) +} + +type vncConnection struct { + id string + destination vncDestination + mu sync.Mutex + vncConn net.Conn + wsHandlers js.Value + ctx context.Context + cancel context.CancelFunc + // Go-side callbacks exposed to JS. js.FuncOf pins the Go closure in a + // global handle map and MUST be released, otherwise every connection + // leaks the Go memory the closure captures. + wsHandlerFn js.Func + onMessageFn js.Func + onCloseFn js.Func +} + +// NewVNCProxy creates a new VNC proxy. +func NewVNCProxy(client interface { + Dial(ctx context.Context, network, address string) (net.Conn, error) +}) *VNCProxy { + return &VNCProxy{ + nbClient: client, + activeConnections: make(map[string]*vncConnection), + } +} + +// CreateProxy creates a new proxy endpoint for the given VNC destination. +// mode is "attach" (capture current display) or "session" (virtual session). +// username is required for session mode. +// Returns a JS Promise that resolves to the WebSocket proxy URL. +func (p *VNCProxy) CreateProxy(hostname, port, mode, username, jwt string, sessionID uint32) js.Value { + address := fmt.Sprintf("%s:%s", hostname, port) + + var m byte + if mode == "session" { + m = modeSession + } + + dest := vncDestination{ + address: address, + mode: m, + username: username, + jwt: jwt, + sessionID: sessionID, + } + + return js.Global().Get("Promise").New(js.FuncOf(func(_ js.Value, args []js.Value) any { + resolve := args[0] + + go func() { + proxyID := fmt.Sprintf("vnc_proxy_%d", p.nextID.Add(1)) + + p.mu.Lock() + if p.destinations == nil { + p.destinations = make(map[string]vncDestination) + } + p.destinations[proxyID] = dest + p.mu.Unlock() + + proxyURL := fmt.Sprintf("%s://%s/%s", vncProxyScheme, vncProxyHost, proxyID) + + handlerFn := js.FuncOf(func(_ js.Value, args []js.Value) any { + if len(args) < 1 { + return js.ValueOf("error: requires WebSocket argument") + } + p.handleWebSocketConnection(args[0], proxyID) + return nil + }) + p.mu.Lock() + if p.pendingHandlers == nil { + p.pendingHandlers = make(map[string]js.Func) + } + p.pendingHandlers[proxyID] = handlerFn + p.mu.Unlock() + js.Global().Set(fmt.Sprintf("handleVNCWebSocket_%s", proxyID), handlerFn) + + log.Infof("created VNC proxy: %s -> %s (mode=%s, user=%s)", proxyURL, address, mode, username) + resolve.Invoke(proxyURL) + }() + + return nil + })) +} + +func (p *VNCProxy) handleWebSocketConnection(ws js.Value, proxyID string) { + p.mu.Lock() + dest, ok := p.destinations[proxyID] + handlerFn := p.pendingHandlers[proxyID] + delete(p.pendingHandlers, proxyID) + p.mu.Unlock() + + if !ok { + log.Errorf("no destination for VNC proxy %s", proxyID) + return + } + + ctx, cancel := context.WithCancel(context.Background()) + + conn := &vncConnection{ + id: proxyID, + destination: dest, + wsHandlers: ws, + ctx: ctx, + cancel: cancel, + wsHandlerFn: handlerFn, + } + + p.mu.Lock() + p.activeConnections[proxyID] = conn + p.mu.Unlock() + + p.setupWebSocketHandlers(ws, conn) + go p.connectToVNC(conn) + + log.Infof("VNC proxy WebSocket connection established for %s", proxyID) +} + +func (p *VNCProxy) setupWebSocketHandlers(ws js.Value, conn *vncConnection) { + conn.onMessageFn = js.FuncOf(func(_ js.Value, args []js.Value) any { + if len(args) < 1 { + return nil + } + data := args[0] + go p.handleWebSocketMessage(conn, data) + return nil + }) + ws.Set("onGoMessage", conn.onMessageFn) + + conn.onCloseFn = js.FuncOf(func(_ js.Value, _ []js.Value) any { + log.Debug("VNC WebSocket closed by JavaScript") + conn.cancel() + return nil + }) + ws.Set("onGoClose", conn.onCloseFn) +} + +func (p *VNCProxy) handleWebSocketMessage(conn *vncConnection, data js.Value) { + if !data.InstanceOf(js.Global().Get("Uint8Array")) { + return + } + + length := data.Get("length").Int() + buf := make([]byte, length) + js.CopyBytesToGo(buf, data) + + conn.mu.Lock() + vncConn := conn.vncConn + conn.mu.Unlock() + + if vncConn == nil { + return + } + + if _, err := vncConn.Write(buf); err != nil { + log.Debugf("write to VNC server: %v", err) + } +} + +func (p *VNCProxy) connectToVNC(conn *vncConnection) { + ctx, cancel := context.WithTimeout(conn.ctx, vncDialTimeout) + defer cancel() + + vncConn, err := p.nbClient.Dial(ctx, "tcp", conn.destination.address) + if err != nil { + log.Errorf("VNC connect to %s: %v", conn.destination.address, err) + // Close the WebSocket so noVNC fires a disconnect event. + if conn.wsHandlers.Get("close").Truthy() { + conn.wsHandlers.Call("close", 1006, fmt.Sprintf("connect to peer: %v", err)) + } + p.cleanupConnection(conn) + return + } + conn.mu.Lock() + conn.vncConn = vncConn + conn.mu.Unlock() + + // Send the NetBird VNC session header before the RFB handshake. + if err := p.sendSessionHeader(vncConn, conn.destination); err != nil { + log.Errorf("send VNC session header: %v", err) + p.cleanupConnection(conn) + return + } + + // WS→TCP is handled by the onGoMessage handler set in setupWebSocketHandlers, + // which writes directly to the VNC connection as data arrives from JS. + // Only the TCP→WS direction needs a read loop here. + go p.forwardConnToWS(conn) + + <-conn.ctx.Done() + p.cleanupConnection(conn) +} + +// sendSessionHeader writes mode, username, and JWT to the VNC server. +// Format: [mode: 1 byte] [username_len: 2 bytes BE] [username: N bytes] +// +// [jwt_len: 2 bytes BE] [jwt: N bytes] +func (p *VNCProxy) sendSessionHeader(conn net.Conn, dest vncDestination) error { + usernameBytes := []byte(dest.username) + jwtBytes := []byte(dest.jwt) + // Format: [mode:1] [username_len:2] [username:N] [jwt_len:2] [jwt:N] [session_id:4] + hdr := make([]byte, 3+len(usernameBytes)+2+len(jwtBytes)+4) + hdr[0] = dest.mode + hdr[1] = byte(len(usernameBytes) >> 8) + hdr[2] = byte(len(usernameBytes)) + off := 3 + copy(hdr[off:], usernameBytes) + off += len(usernameBytes) + hdr[off] = byte(len(jwtBytes) >> 8) + hdr[off+1] = byte(len(jwtBytes)) + off += 2 + copy(hdr[off:], jwtBytes) + off += len(jwtBytes) + hdr[off] = byte(dest.sessionID >> 24) + hdr[off+1] = byte(dest.sessionID >> 16) + hdr[off+2] = byte(dest.sessionID >> 8) + hdr[off+3] = byte(dest.sessionID) + + _, err := conn.Write(hdr) + return err +} + +func (p *VNCProxy) forwardConnToWS(conn *vncConnection) { + buf := make([]byte, 32*1024) + + for { + if conn.ctx.Err() != nil { + return + } + + // Set a read deadline so we detect dead connections instead of + // blocking forever when the remote peer dies. + conn.mu.Lock() + vc := conn.vncConn + conn.mu.Unlock() + if vc == nil { + return + } + vc.SetReadDeadline(time.Now().Add(30 * time.Second)) + + n, err := vc.Read(buf) + if err != nil { + if conn.ctx.Err() != nil { + return + } + if netErr, ok := err.(interface{ Timeout() bool }); ok && netErr.Timeout() { + // Read timeout: connection might be stale. Send a ping-like + // empty read to check. If the connection is truly dead, the + // next iteration will fail too and we'll close. + continue + } + if err != io.EOF { + log.Debugf("read from VNC connection: %v", err) + } + // Close the WebSocket to notify noVNC. + if conn.wsHandlers.Get("close").Truthy() { + conn.wsHandlers.Call("close", 1006, "VNC connection lost") + } + return + } + + if n > 0 { + p.sendToWebSocket(conn, buf[:n]) + } + } +} + +func (p *VNCProxy) sendToWebSocket(conn *vncConnection, data []byte) { + if conn.wsHandlers.Get("receiveFromGo").Truthy() { + uint8Array := js.Global().Get("Uint8Array").New(len(data)) + js.CopyBytesToJS(uint8Array, data) + conn.wsHandlers.Call("receiveFromGo", uint8Array.Get("buffer")) + } else if conn.wsHandlers.Get("send").Truthy() { + uint8Array := js.Global().Get("Uint8Array").New(len(data)) + js.CopyBytesToJS(uint8Array, data) + conn.wsHandlers.Call("send", uint8Array.Get("buffer")) + } +} + +func (p *VNCProxy) cleanupConnection(conn *vncConnection) { + log.Debugf("cleaning up VNC connection %s", conn.id) + conn.cancel() + + conn.mu.Lock() + vncConn := conn.vncConn + conn.vncConn = nil + conn.mu.Unlock() + + if vncConn != nil { + if err := vncConn.Close(); err != nil { + log.Debugf("close VNC connection: %v", err) + } + } + + // Remove the global JS handler registered in CreateProxy. + globalName := fmt.Sprintf("handleVNCWebSocket_%s", conn.id) + js.Global().Delete(globalName) + + // Release all js.Func handles; js.FuncOf pins the Go closure and the + // allocations it captures until Release is called. + conn.wsHandlerFn.Release() + conn.onMessageFn.Release() + conn.onCloseFn.Release() + + p.mu.Lock() + delete(p.activeConnections, conn.id) + delete(p.destinations, conn.id) + delete(p.pendingHandlers, conn.id) + p.mu.Unlock() +} diff --git a/go.mod b/go.mod index 5172b1a78..cb4543a93 100644 --- a/go.mod +++ b/go.mod @@ -225,6 +225,7 @@ require ( github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect + github.com/jezek/xgb v1.3.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect @@ -232,6 +233,7 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect github.com/kelseyhightower/envconfig v1.4.0 // indirect + github.com/kirides/go-d3d v1.0.1 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/koron/go-ssdp v0.0.4 // indirect diff --git a/go.sum b/go.sum index 9293ce73b..06c39570c 100644 --- a/go.sum +++ b/go.sum @@ -346,6 +346,8 @@ github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZ github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE= github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o= +github.com/jezek/xgb v1.3.0 h1:Wa1pn4GVtcmNVAVB6/pnQVJ7xPFZVZ/W1Tc27msDhgI= +github.com/jezek/xgb v1.3.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= @@ -364,6 +366,8 @@ github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6U github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/kirides/go-d3d v1.0.1 h1:ZDANfvo34vskBMET1uwUUMNw8545Kbe8qYSiRwlNIuA= +github.com/kirides/go-d3d v1.0.1/go.mod h1:99AjD+5mRTFEnkpRWkwq8UYMQDljGIIvLn2NyRdVImY= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= diff --git a/management/internals/shared/grpc/conversion.go b/management/internals/shared/grpc/conversion.go index ef417d3cf..398990e21 100644 --- a/management/internals/shared/grpc/conversion.go +++ b/management/internals/shared/grpc/conversion.go @@ -88,16 +88,23 @@ func toNetbirdConfig(config *nbconfig.Config, turnCredentials *Token, relayToken return nbConfig } -func toPeerConfig(peer *nbpeer.Peer, network *types.Network, dnsName string, settings *types.Settings, httpConfig *nbconfig.HttpServerConfig, deviceFlowConfig *nbconfig.DeviceAuthorizationFlow, enableSSH bool) *proto.PeerConfig { +func toPeerConfig(peer *nbpeer.Peer, network *types.Network, dnsName string, settings *types.Settings, httpConfig *nbconfig.HttpServerConfig, deviceFlowConfig *nbconfig.DeviceAuthorizationFlow, enableSSH bool, peerGroups []string) *proto.PeerConfig { netmask, _ := network.Net.Mask.Size() fqdn := peer.FQDN(dnsName) sshConfig := &proto.SSHConfig{ SshEnabled: peer.SSHEnabled || enableSSH, + JwtConfig: buildJWTConfig(httpConfig, deviceFlowConfig), } - if sshConfig.SshEnabled { - sshConfig.JwtConfig = buildJWTConfig(httpConfig, deviceFlowConfig) + if settings.RecordingEnabled && peerInGroups(peerGroups, settings.RecordingGroups) { + sshConfig.EnableRecording = true + sshConfig.RecordingMaxSessions = settings.RecordingMaxSessions + sshConfig.RecordingMaxTotalSizeMb = settings.RecordingMaxTotalSizeMB + sshConfig.RecordInputEnabled = settings.RecordingInputEnabled == nil || *settings.RecordingInputEnabled + if settings.RecordingEncryptionKey != "" { + sshConfig.RecordingEncryptionKey = []byte(settings.RecordingEncryptionKey) + } } return &proto.PeerConfig{ @@ -114,13 +121,14 @@ func toPeerConfig(peer *nbpeer.Peer, network *types.Network, dnsName string, set } func ToSyncResponse(ctx context.Context, config *nbconfig.Config, httpConfig *nbconfig.HttpServerConfig, deviceFlowConfig *nbconfig.DeviceAuthorizationFlow, peer *nbpeer.Peer, turnCredentials *Token, relayCredentials *Token, networkMap *types.NetworkMap, dnsName string, checks []*posture.Checks, dnsCache *cache.DNSConfigCache, settings *types.Settings, extraSettings *types.ExtraSettings, peerGroups []string, dnsFwdPort int64) *proto.SyncResponse { + peerConfig := toPeerConfig(peer, networkMap.Network, dnsName, settings, httpConfig, deviceFlowConfig, networkMap.EnableSSH, peerGroups) response := &proto.SyncResponse{ - PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, settings, httpConfig, deviceFlowConfig, networkMap.EnableSSH), + PeerConfig: peerConfig, NetworkMap: &proto.NetworkMap{ Serial: networkMap.Network.CurrentSerial(), Routes: toProtocolRoutes(networkMap.Routes), DNSConfig: toProtocolDNSConfig(networkMap.DNSConfig, dnsCache, dnsFwdPort), - PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, settings, httpConfig, deviceFlowConfig, networkMap.EnableSSH), + PeerConfig: peerConfig, }, Checks: toProtocolChecks(ctx, checks), } @@ -129,8 +137,6 @@ func ToSyncResponse(ctx context.Context, config *nbconfig.Config, httpConfig *nb extendedConfig := integrationsConfig.ExtendNetBirdConfig(peer.ID, peerGroups, nbConfig, extraSettings) response.NetbirdConfig = extendedConfig - response.NetworkMap.PeerConfig = response.PeerConfig - remotePeers := make([]*proto.RemotePeerConfig, 0, len(networkMap.Peers)+len(networkMap.OfflinePeers)) remotePeers = appendRemotePeerConfig(remotePeers, networkMap.Peers, dnsName) response.RemotePeers = remotePeers @@ -156,15 +162,21 @@ func ToSyncResponse(ctx context.Context, config *nbconfig.Config, httpConfig *nb response.NetworkMap.ForwardingRules = forwardingRules } + userIDClaim := auth.DefaultUserIDClaim + if httpConfig != nil && httpConfig.AuthUserIDClaim != "" { + userIDClaim = httpConfig.AuthUserIDClaim + } + if networkMap.AuthorizedUsers != nil { hashedUsers, machineUsers := buildAuthorizedUsersProto(ctx, networkMap.AuthorizedUsers) - userIDClaim := auth.DefaultUserIDClaim - if httpConfig != nil && httpConfig.AuthUserIDClaim != "" { - userIDClaim = httpConfig.AuthUserIDClaim - } response.NetworkMap.SshAuth = &proto.SSHAuth{AuthorizedUsers: hashedUsers, MachineUsers: machineUsers, UserIDClaim: userIDClaim} } + if networkMap.VNCAuthorizedUsers != nil { + hashedUsers, machineUsers := buildAuthorizedUsersProto(ctx, networkMap.VNCAuthorizedUsers) + response.NetworkMap.VncAuth = &proto.VNCAuth{AuthorizedUsers: hashedUsers, MachineUsers: machineUsers, UserIDClaim: userIDClaim} + } + return response } @@ -195,6 +207,17 @@ func buildAuthorizedUsersProto(ctx context.Context, authorizedUsers map[string]m return hashedUsers, machineUsers } +func peerInGroups(peerGroups, targetGroups []string) bool { + for _, pg := range peerGroups { + for _, tg := range targetGroups { + if pg == tg { + return true + } + } + } + return false +} + func appendRemotePeerConfig(dst []*proto.RemotePeerConfig, peers []*nbpeer.Peer, dnsName string) []*proto.RemotePeerConfig { for _, rPeer := range peers { dst = append(dst, &proto.RemotePeerConfig{ diff --git a/management/internals/shared/grpc/server.go b/management/internals/shared/grpc/server.go index 6e8358f02..95c0567d3 100644 --- a/management/internals/shared/grpc/server.go +++ b/management/internals/shared/grpc/server.go @@ -665,6 +665,7 @@ func extractPeerMeta(ctx context.Context, meta *proto.PeerSystemMeta) nbpeer.Pee RosenpassEnabled: meta.GetFlags().GetRosenpassEnabled(), RosenpassPermissive: meta.GetFlags().GetRosenpassPermissive(), ServerSSHAllowed: meta.GetFlags().GetServerSSHAllowed(), + ServerVNCAllowed: meta.GetFlags().GetServerVNCAllowed(), DisableClientRoutes: meta.GetFlags().GetDisableClientRoutes(), DisableServerRoutes: meta.GetFlags().GetDisableServerRoutes(), DisableDNS: meta.GetFlags().GetDisableDNS(), @@ -672,6 +673,7 @@ func extractPeerMeta(ctx context.Context, meta *proto.PeerSystemMeta) nbpeer.Pee BlockLANAccess: meta.GetFlags().GetBlockLANAccess(), BlockInbound: meta.GetFlags().GetBlockInbound(), LazyConnectionEnabled: meta.GetFlags().GetLazyConnectionEnabled(), + DisableVNCAuth: meta.GetFlags().GetDisableVNCAuth(), }, Files: files, } @@ -818,10 +820,16 @@ func (s *Server) prepareLoginResponse(ctx context.Context, peer *nbpeer.Peer, ne return nil, status.Errorf(codes.Internal, "failed getting settings") } + peerGroupIDs, err := s.accountManager.GetStore().GetPeerGroupIDs(ctx, store.LockingStrengthNone, peer.AccountID, peer.ID) + if err != nil { + log.WithContext(ctx).Warnf("failed getting peer groups for peer %s: %s", peer.Key, err) + return nil, status.Errorf(codes.Internal, "failed getting peer groups") + } + // if peer has reached this point then it has logged in loginResp := &proto.LoginResponse{ NetbirdConfig: toNetbirdConfig(s.config, nil, relayToken, nil), - PeerConfig: toPeerConfig(peer, netMap.Network, s.networkMapController.GetDNSDomain(settings), settings, s.config.HttpConfig, s.config.DeviceAuthorizationFlow, netMap.EnableSSH), + PeerConfig: toPeerConfig(peer, netMap.Network, s.networkMapController.GetDNSDomain(settings), settings, s.config.HttpConfig, s.config.DeviceAuthorizationFlow, netMap.EnableSSH, peerGroupIDs), Checks: toProtocolChecks(ctx, postureChecks), } diff --git a/management/server/http/handlers/accounts/accounts_handler.go b/management/server/http/handlers/accounts/accounts_handler.go index cc5567e3d..d2d70c663 100644 --- a/management/server/http/handlers/accounts/accounts_handler.go +++ b/management/server/http/handlers/accounts/accounts_handler.go @@ -184,6 +184,25 @@ func (h *handler) updateAccountRequestSettings(req api.PutApiAccountsAccountIdJS PeerExposeGroups: req.Settings.PeerExposeGroups, } + if req.Settings.RecordingEnabled != nil { + returnSettings.RecordingEnabled = *req.Settings.RecordingEnabled + } + if req.Settings.RecordingGroups != nil { + returnSettings.RecordingGroups = *req.Settings.RecordingGroups + } + if req.Settings.RecordingMaxSessions != nil { + returnSettings.RecordingMaxSessions = int32(*req.Settings.RecordingMaxSessions) + } + if req.Settings.RecordingMaxTotalSizeMb != nil { + returnSettings.RecordingMaxTotalSizeMB = int64(*req.Settings.RecordingMaxTotalSizeMb) + } + if req.Settings.RecordingInputEnabled != nil { + returnSettings.RecordingInputEnabled = req.Settings.RecordingInputEnabled + } + if req.Settings.RecordingEncryptionKey != nil { + returnSettings.RecordingEncryptionKey = *req.Settings.RecordingEncryptionKey + } + if req.Settings.Extra != nil { returnSettings.Extra = &types.ExtraSettings{ PeerApprovalEnabled: req.Settings.Extra.PeerApprovalEnabled, @@ -348,6 +367,12 @@ func toAccountResponse(accountID string, settings *types.Settings, meta *types.A RoutingPeerDnsResolutionEnabled: &settings.RoutingPeerDNSResolutionEnabled, PeerExposeEnabled: settings.PeerExposeEnabled, PeerExposeGroups: settings.PeerExposeGroups, + RecordingEnabled: &settings.RecordingEnabled, + RecordingGroups: &settings.RecordingGroups, + RecordingMaxSessions: toIntPtr(settings.RecordingMaxSessions), + RecordingMaxTotalSizeMb: &settings.RecordingMaxTotalSizeMB, + RecordingInputEnabled: settings.RecordingInputEnabled, + RecordingEncryptionKey: &settings.RecordingEncryptionKey, LazyConnectionEnabled: &settings.LazyConnectionEnabled, DnsDomain: &settings.DNSDomain, AutoUpdateVersion: &settings.AutoUpdateVersion, @@ -386,3 +411,8 @@ func toAccountResponse(accountID string, settings *types.Settings, meta *types.A Onboarding: apiOnboarding, } } + +func toIntPtr(v int32) *int { + i := int(v) + return &i +} diff --git a/management/server/http/handlers/peers/peers_handler.go b/management/server/http/handlers/peers/peers_handler.go index 6b9a69f04..ee4eaa3b8 100644 --- a/management/server/http/handlers/peers/peers_handler.go +++ b/management/server/http/handlers/peers/peers_handler.go @@ -487,7 +487,7 @@ func (h *Handler) CreateTemporaryAccess(w http.ResponseWriter, r *http.Request) PortRanges: []types.RulePortRange{portRange}, }}, } - if protocol == types.PolicyRuleProtocolNetbirdSSH { + if protocol == types.PolicyRuleProtocolNetbirdSSH || protocol == types.PolicyRuleProtocolNetbirdVNC { policy.Rules[0].AuthorizedUser = userAuth.UserId } @@ -581,6 +581,8 @@ func toSinglePeerResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dnsD RosenpassEnabled: &peer.Meta.Flags.RosenpassEnabled, RosenpassPermissive: &peer.Meta.Flags.RosenpassPermissive, ServerSshAllowed: &peer.Meta.Flags.ServerSSHAllowed, + ServerVncAllowed: &peer.Meta.Flags.ServerVNCAllowed, + DisableVncAuth: &peer.Meta.Flags.DisableVNCAuth, }, } @@ -635,6 +637,8 @@ func toPeerListItemResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dn RosenpassEnabled: &peer.Meta.Flags.RosenpassEnabled, RosenpassPermissive: &peer.Meta.Flags.RosenpassPermissive, ServerSshAllowed: &peer.Meta.Flags.ServerSSHAllowed, + ServerVncAllowed: &peer.Meta.Flags.ServerVNCAllowed, + DisableVncAuth: &peer.Meta.Flags.DisableVNCAuth, }, } } diff --git a/management/server/http/handlers/policies/policies_handler.go b/management/server/http/handlers/policies/policies_handler.go index e4d1d73df..6362e61d4 100644 --- a/management/server/http/handlers/policies/policies_handler.go +++ b/management/server/http/handlers/policies/policies_handler.go @@ -223,6 +223,8 @@ func (h *handler) savePolicy(w http.ResponseWriter, r *http.Request, accountID s pr.Protocol = types.PolicyRuleProtocolICMP case api.PolicyRuleUpdateProtocolNetbirdSsh: pr.Protocol = types.PolicyRuleProtocolNetbirdSSH + case api.PolicyRuleUpdateProtocolNetbirdVnc: + pr.Protocol = types.PolicyRuleProtocolNetbirdVNC default: util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "unknown protocol type: %v", rule.Protocol), w) return @@ -256,7 +258,8 @@ func (h *handler) savePolicy(w http.ResponseWriter, r *http.Request, accountID s } } - if pr.Protocol == types.PolicyRuleProtocolNetbirdSSH && rule.AuthorizedGroups != nil && len(*rule.AuthorizedGroups) != 0 { + isNetBirdService := pr.Protocol == types.PolicyRuleProtocolNetbirdSSH || pr.Protocol == types.PolicyRuleProtocolNetbirdVNC + if isNetBirdService && rule.AuthorizedGroups != nil && len(*rule.AuthorizedGroups) != 0 { for _, sourceGroupID := range pr.Sources { _, ok := (*rule.AuthorizedGroups)[sourceGroupID] if !ok { diff --git a/management/server/management_proto_test.go b/management/server/management_proto_test.go index 4e6eb0a33..f8d319222 100644 --- a/management/server/management_proto_test.go +++ b/management/server/management_proto_test.go @@ -714,6 +714,7 @@ func Test_LoginPerformance(t *testing.T) { RosenpassEnabled: meta.GetFlags().GetRosenpassEnabled(), RosenpassPermissive: meta.GetFlags().GetRosenpassPermissive(), ServerSSHAllowed: meta.GetFlags().GetServerSSHAllowed(), + ServerVNCAllowed: meta.GetFlags().GetServerVNCAllowed(), DisableClientRoutes: meta.GetFlags().GetDisableClientRoutes(), DisableServerRoutes: meta.GetFlags().GetDisableServerRoutes(), DisableDNS: meta.GetFlags().GetDisableDNS(), diff --git a/management/server/peer.go b/management/server/peer.go index a02e34e0d..03f996b6e 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -93,7 +93,7 @@ func (am *DefaultAccountManager) getUserAccessiblePeers(ctx context.Context, acc // fetch all the peers that have access to the user's peers for _, peer := range peers { - aclPeers, _, _, _ := account.GetPeerConnectionResources(ctx, peer, approvedPeersMap, account.GetActiveGroupUsers()) + aclPeers, _, _, _, _ := account.GetPeerConnectionResources(ctx, peer, approvedPeersMap, account.GetActiveGroupUsers()) for _, p := range aclPeers { peersMap[p.ID] = p } @@ -1277,7 +1277,7 @@ func (am *DefaultAccountManager) checkIfUserOwnsPeer(ctx context.Context, accoun } for _, p := range userPeers { - aclPeers, _, _, _ := account.GetPeerConnectionResources(ctx, p, approvedPeersMap, account.GetActiveGroupUsers()) + aclPeers, _, _, _, _ := account.GetPeerConnectionResources(ctx, p, approvedPeersMap, account.GetActiveGroupUsers()) for _, aclPeer := range aclPeers { if aclPeer.ID == peer.ID { return peer, nil diff --git a/management/server/peer/peer.go b/management/server/peer/peer.go index db392ddda..180aeb416 100644 --- a/management/server/peer/peer.go +++ b/management/server/peer/peer.go @@ -108,6 +108,7 @@ type Flags struct { RosenpassEnabled bool RosenpassPermissive bool ServerSSHAllowed bool + ServerVNCAllowed bool DisableClientRoutes bool DisableServerRoutes bool @@ -117,6 +118,8 @@ type Flags struct { BlockInbound bool LazyConnectionEnabled bool + + DisableVNCAuth bool } // PeerSystemMeta is a metadata of a Peer machine system @@ -363,11 +366,13 @@ func (f Flags) isEqual(other Flags) bool { return f.RosenpassEnabled == other.RosenpassEnabled && f.RosenpassPermissive == other.RosenpassPermissive && f.ServerSSHAllowed == other.ServerSSHAllowed && + f.ServerVNCAllowed == other.ServerVNCAllowed && f.DisableClientRoutes == other.DisableClientRoutes && f.DisableServerRoutes == other.DisableServerRoutes && f.DisableDNS == other.DisableDNS && f.DisableFirewall == other.DisableFirewall && f.BlockLANAccess == other.BlockLANAccess && f.BlockInbound == other.BlockInbound && - f.LazyConnectionEnabled == other.LazyConnectionEnabled + f.LazyConnectionEnabled == other.LazyConnectionEnabled && + f.DisableVNCAuth == other.DisableVNCAuth } diff --git a/management/server/policy_test.go b/management/server/policy_test.go index a3f987732..bfef159c4 100644 --- a/management/server/policy_test.go +++ b/management/server/policy_test.go @@ -246,14 +246,14 @@ func TestAccount_getPeersByPolicy(t *testing.T) { t.Run("check that all peers get map", func(t *testing.T) { for _, p := range account.Peers { - peers, firewallRules, _, _ := account.GetPeerConnectionResources(context.Background(), p, validatedPeers, account.GetActiveGroupUsers()) + peers, firewallRules, _, _, _ := account.GetPeerConnectionResources(context.Background(), p, validatedPeers, account.GetActiveGroupUsers()) assert.GreaterOrEqual(t, len(peers), 1, "minimum number peers should present") assert.GreaterOrEqual(t, len(firewallRules), 1, "minimum number of firewall rules should present") } }) t.Run("check first peer map details", func(t *testing.T) { - peers, firewallRules, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerB"], validatedPeers, account.GetActiveGroupUsers()) + peers, firewallRules, _, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerB"], validatedPeers, account.GetActiveGroupUsers()) assert.Len(t, peers, 8) assert.Contains(t, peers, account.Peers["peerA"]) assert.Contains(t, peers, account.Peers["peerC"]) @@ -509,7 +509,7 @@ func TestAccount_getPeersByPolicy(t *testing.T) { }) t.Run("check port ranges support for older peers", func(t *testing.T) { - peers, firewallRules, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerK"], validatedPeers, account.GetActiveGroupUsers()) + peers, firewallRules, _, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerK"], validatedPeers, account.GetActiveGroupUsers()) assert.Len(t, peers, 1) assert.Contains(t, peers, account.Peers["peerI"]) @@ -635,7 +635,7 @@ func TestAccount_getPeersByPolicyDirect(t *testing.T) { } t.Run("check first peer map", func(t *testing.T) { - peers, firewallRules, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerB"], approvedPeers, account.GetActiveGroupUsers()) + peers, firewallRules, _, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerB"], approvedPeers, account.GetActiveGroupUsers()) assert.Contains(t, peers, account.Peers["peerC"]) expectedFirewallRules := []*types.FirewallRule{ @@ -665,7 +665,7 @@ func TestAccount_getPeersByPolicyDirect(t *testing.T) { }) t.Run("check second peer map", func(t *testing.T) { - peers, firewallRules, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerC"], approvedPeers, account.GetActiveGroupUsers()) + peers, firewallRules, _, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerC"], approvedPeers, account.GetActiveGroupUsers()) assert.Contains(t, peers, account.Peers["peerB"]) expectedFirewallRules := []*types.FirewallRule{ @@ -697,7 +697,7 @@ func TestAccount_getPeersByPolicyDirect(t *testing.T) { account.Policies[1].Rules[0].Bidirectional = false t.Run("check first peer map directional only", func(t *testing.T) { - peers, firewallRules, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerB"], approvedPeers, account.GetActiveGroupUsers()) + peers, firewallRules, _, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerB"], approvedPeers, account.GetActiveGroupUsers()) assert.Contains(t, peers, account.Peers["peerC"]) expectedFirewallRules := []*types.FirewallRule{ @@ -719,7 +719,7 @@ func TestAccount_getPeersByPolicyDirect(t *testing.T) { }) t.Run("check second peer map directional only", func(t *testing.T) { - peers, firewallRules, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerC"], approvedPeers, account.GetActiveGroupUsers()) + peers, firewallRules, _, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerC"], approvedPeers, account.GetActiveGroupUsers()) assert.Contains(t, peers, account.Peers["peerB"]) expectedFirewallRules := []*types.FirewallRule{ @@ -917,7 +917,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { t.Run("verify peer's network map with default group peer list", func(t *testing.T) { // peerB doesn't fulfill the NB posture check but is included in the destination group Swarm, // will establish a connection with all source peers satisfying the NB posture check. - peers, firewallRules, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerB"], approvedPeers, account.GetActiveGroupUsers()) + peers, firewallRules, _, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerB"], approvedPeers, account.GetActiveGroupUsers()) assert.Len(t, peers, 4) assert.Len(t, firewallRules, 4) assert.Contains(t, peers, account.Peers["peerA"]) @@ -927,7 +927,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { // peerC satisfy the NB posture check, should establish connection to all destination group peer's // We expect a single permissive firewall rule which all outgoing connections - peers, firewallRules, _, _ = account.GetPeerConnectionResources(context.Background(), account.Peers["peerC"], approvedPeers, account.GetActiveGroupUsers()) + peers, firewallRules, _, _, _ = account.GetPeerConnectionResources(context.Background(), account.Peers["peerC"], approvedPeers, account.GetActiveGroupUsers()) assert.Len(t, peers, len(account.Groups["GroupSwarm"].Peers)) assert.Len(t, firewallRules, 7) expectedFirewallRules := []*types.FirewallRule{ @@ -992,7 +992,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { // peerE doesn't fulfill the NB posture check and exists in only destination group Swarm, // all source group peers satisfying the NB posture check should establish connection - peers, firewallRules, _, _ = account.GetPeerConnectionResources(context.Background(), account.Peers["peerE"], approvedPeers, account.GetActiveGroupUsers()) + peers, firewallRules, _, _, _ = account.GetPeerConnectionResources(context.Background(), account.Peers["peerE"], approvedPeers, account.GetActiveGroupUsers()) assert.Len(t, peers, 4) assert.Len(t, firewallRules, 4) assert.Contains(t, peers, account.Peers["peerA"]) @@ -1002,7 +1002,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { // peerI doesn't fulfill the OS version posture check and exists in only destination group Swarm, // all source group peers satisfying the NB posture check should establish connection - peers, firewallRules, _, _ = account.GetPeerConnectionResources(context.Background(), account.Peers["peerI"], approvedPeers, account.GetActiveGroupUsers()) + peers, firewallRules, _, _, _ = account.GetPeerConnectionResources(context.Background(), account.Peers["peerI"], approvedPeers, account.GetActiveGroupUsers()) assert.Len(t, peers, 4) assert.Len(t, firewallRules, 4) assert.Contains(t, peers, account.Peers["peerA"]) @@ -1017,19 +1017,19 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { // peerB doesn't satisfy the NB posture check, and doesn't exist in destination group peer's // no connection should be established to any peer of destination group - peers, firewallRules, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerB"], approvedPeers, account.GetActiveGroupUsers()) + peers, firewallRules, _, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerB"], approvedPeers, account.GetActiveGroupUsers()) assert.Len(t, peers, 0) assert.Len(t, firewallRules, 0) // peerI doesn't satisfy the OS version posture check, and doesn't exist in destination group peer's // no connection should be established to any peer of destination group - peers, firewallRules, _, _ = account.GetPeerConnectionResources(context.Background(), account.Peers["peerI"], approvedPeers, account.GetActiveGroupUsers()) + peers, firewallRules, _, _, _ = account.GetPeerConnectionResources(context.Background(), account.Peers["peerI"], approvedPeers, account.GetActiveGroupUsers()) assert.Len(t, peers, 0) assert.Len(t, firewallRules, 0) // peerC satisfy the NB posture check, should establish connection to all destination group peer's // We expect a single permissive firewall rule which all outgoing connections - peers, firewallRules, _, _ = account.GetPeerConnectionResources(context.Background(), account.Peers["peerC"], approvedPeers, account.GetActiveGroupUsers()) + peers, firewallRules, _, _, _ = account.GetPeerConnectionResources(context.Background(), account.Peers["peerC"], approvedPeers, account.GetActiveGroupUsers()) assert.Len(t, peers, len(account.Groups["GroupSwarm"].Peers)) assert.Len(t, firewallRules, len(account.Groups["GroupSwarm"].Peers)) @@ -1044,14 +1044,14 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { // peerE doesn't fulfill the NB posture check and exists in only destination group Swarm, // all source group peers satisfying the NB posture check should establish connection - peers, firewallRules, _, _ = account.GetPeerConnectionResources(context.Background(), account.Peers["peerE"], approvedPeers, account.GetActiveGroupUsers()) + peers, firewallRules, _, _, _ = account.GetPeerConnectionResources(context.Background(), account.Peers["peerE"], approvedPeers, account.GetActiveGroupUsers()) assert.Len(t, peers, 3) assert.Len(t, firewallRules, 3) assert.Contains(t, peers, account.Peers["peerA"]) assert.Contains(t, peers, account.Peers["peerC"]) assert.Contains(t, peers, account.Peers["peerD"]) - peers, firewallRules, _, _ = account.GetPeerConnectionResources(context.Background(), account.Peers["peerA"], approvedPeers, account.GetActiveGroupUsers()) + peers, firewallRules, _, _, _ = account.GetPeerConnectionResources(context.Background(), account.Peers["peerA"], approvedPeers, account.GetActiveGroupUsers()) assert.Len(t, peers, 5) // assert peers from Group Swarm assert.Contains(t, peers, account.Peers["peerD"]) diff --git a/management/server/types/account.go b/management/server/types/account.go index c448813db..179761ac7 100644 --- a/management/server/types/account.go +++ b/management/server/types/account.go @@ -54,6 +54,9 @@ const ( // defaultSSHPortString defines the standard SSH port number as a string, commonly used for default SSH connections. defaultSSHPortString = "22" defaultSSHPortNumber = 22 + + // vncInternalPort is the internal port the VNC server listens on (behind DNAT from 5900). + vncInternalPort = 25900 ) type supportedFeatures struct { @@ -304,7 +307,7 @@ func (a *Account) GetPeerNetworkMap( peerGroups := a.GetPeerGroups(peerID) - aclPeers, firewallRules, authorizedUsers, enableSSH := a.GetPeerConnectionResources(ctx, peer, validatedPeersMap, groupIDToUserIDs) + aclPeers, firewallRules, authorizedUsers, vncAuthorizedUsers, enableSSH := a.GetPeerConnectionResources(ctx, peer, validatedPeersMap, groupIDToUserIDs) // exclude expired peers var peersToConnect []*nbpeer.Peer var expiredPeers []*nbpeer.Peer @@ -358,6 +361,7 @@ func (a *Account) GetPeerNetworkMap( FirewallRules: firewallRules, RoutesFirewallRules: slices.Concat(networkResourcesFirewallRules, routesFirewallRules), AuthorizedUsers: authorizedUsers, + VNCAuthorizedUsers: vncAuthorizedUsers, EnableSSH: enableSSH, } @@ -1042,9 +1046,10 @@ func (a *Account) UserGroupsRemoveFromPeers(userID string, groups ...string) map // GetPeerConnectionResources for a given peer // // This function returns the list of peers and firewall rules that are applicable to a given peer. -func (a *Account) GetPeerConnectionResources(ctx context.Context, peer *nbpeer.Peer, validatedPeersMap map[string]struct{}, groupIDToUserIDs map[string][]string) ([]*nbpeer.Peer, []*FirewallRule, map[string]map[string]struct{}, bool) { +func (a *Account) GetPeerConnectionResources(ctx context.Context, peer *nbpeer.Peer, validatedPeersMap map[string]struct{}, groupIDToUserIDs map[string][]string) ([]*nbpeer.Peer, []*FirewallRule, map[string]map[string]struct{}, map[string]map[string]struct{}, bool) { generateResources, getAccumulatedResources := a.connResourcesGenerator(ctx, peer) - authorizedUsers := make(map[string]map[string]struct{}) // machine user to list of userIDs + authorizedUsers := make(map[string]map[string]struct{}) + vncAuthorizedUsers := make(map[string]map[string]struct{}) sshEnabled := false for _, policy := range a.Policies { @@ -1091,36 +1096,9 @@ func (a *Account) GetPeerConnectionResources(ctx context.Context, peer *nbpeer.P if peerInDestinations && rule.Protocol == PolicyRuleProtocolNetbirdSSH { sshEnabled = true - switch { - case len(rule.AuthorizedGroups) > 0: - for groupID, localUsers := range rule.AuthorizedGroups { - userIDs, ok := groupIDToUserIDs[groupID] - if !ok { - log.WithContext(ctx).Tracef("no user IDs found for group ID %s", groupID) - continue - } - - if len(localUsers) == 0 { - localUsers = []string{auth.Wildcard} - } - - for _, localUser := range localUsers { - if authorizedUsers[localUser] == nil { - authorizedUsers[localUser] = make(map[string]struct{}) - } - for _, userID := range userIDs { - authorizedUsers[localUser][userID] = struct{}{} - } - } - } - case rule.AuthorizedUser != "": - if authorizedUsers[auth.Wildcard] == nil { - authorizedUsers[auth.Wildcard] = make(map[string]struct{}) - } - authorizedUsers[auth.Wildcard][rule.AuthorizedUser] = struct{}{} - default: - authorizedUsers[auth.Wildcard] = a.getAllowedUserIDs() - } + a.collectAuthorizedUsers(ctx, rule, groupIDToUserIDs, authorizedUsers) + } else if peerInDestinations && rule.Protocol == PolicyRuleProtocolNetbirdVNC { + a.collectAuthorizedUsers(ctx, rule, groupIDToUserIDs, vncAuthorizedUsers) } else if peerInDestinations && policyRuleImpliesLegacySSH(rule) && peer.SSHEnabled { sshEnabled = true authorizedUsers[auth.Wildcard] = a.getAllowedUserIDs() @@ -1129,7 +1107,41 @@ func (a *Account) GetPeerConnectionResources(ctx context.Context, peer *nbpeer.P } peers, fwRules := getAccumulatedResources() - return peers, fwRules, authorizedUsers, sshEnabled + return peers, fwRules, authorizedUsers, vncAuthorizedUsers, sshEnabled +} + +// collectAuthorizedUsers populates the target map with authorized user mappings from the rule. +func (a *Account) collectAuthorizedUsers(ctx context.Context, rule *PolicyRule, groupIDToUserIDs map[string][]string, target map[string]map[string]struct{}) { + switch { + case len(rule.AuthorizedGroups) > 0: + for groupID, localUsers := range rule.AuthorizedGroups { + userIDs, ok := groupIDToUserIDs[groupID] + if !ok { + log.WithContext(ctx).Tracef("no user IDs found for group ID %s", groupID) + continue + } + + if len(localUsers) == 0 { + localUsers = []string{auth.Wildcard} + } + + for _, localUser := range localUsers { + if target[localUser] == nil { + target[localUser] = make(map[string]struct{}) + } + for _, userID := range userIDs { + target[localUser][userID] = struct{}{} + } + } + } + case rule.AuthorizedUser != "": + if target[auth.Wildcard] == nil { + target[auth.Wildcard] = make(map[string]struct{}) + } + target[auth.Wildcard][rule.AuthorizedUser] = struct{}{} + default: + target[auth.Wildcard] = a.getAllowedUserIDs() + } } func (a *Account) getAllowedUserIDs() map[string]struct{} { @@ -1165,7 +1177,7 @@ func (a *Account) connResourcesGenerator(ctx context.Context, targetPeer *nbpeer } protocol := rule.Protocol - if protocol == PolicyRuleProtocolNetbirdSSH { + if protocol == PolicyRuleProtocolNetbirdSSH || protocol == PolicyRuleProtocolNetbirdVNC { protocol = PolicyRuleProtocolTCP } diff --git a/management/server/types/network.go b/management/server/types/network.go index 0d13de10f..2f292ee78 100644 --- a/management/server/types/network.go +++ b/management/server/types/network.go @@ -39,6 +39,7 @@ type NetworkMap struct { RoutesFirewallRules []*RouteFirewallRule ForwardingRules []*ForwardingRule AuthorizedUsers map[string]map[string]struct{} + VNCAuthorizedUsers map[string]map[string]struct{} EnableSSH bool } diff --git a/management/server/types/networkmap_components.go b/management/server/types/networkmap_components.go index 23d84a994..28742d2ce 100644 --- a/management/server/types/networkmap_components.go +++ b/management/server/types/networkmap_components.go @@ -112,7 +112,8 @@ func (c *NetworkMapComponents) Calculate(ctx context.Context) *NetworkMap { peerGroups := c.GetPeerGroups(targetPeerID) - aclPeers, firewallRules, authorizedUsers, sshEnabled := c.getPeerConnectionResources(targetPeerID) + connRes := c.getPeerConnectionResources(targetPeerID) + aclPeers := connRes.peers peersToConnect, expiredPeers := c.filterPeersByLoginExpiration(aclPeers) @@ -161,21 +162,32 @@ func (c *NetworkMapComponents) Calculate(ctx context.Context) *NetworkMap { Routes: append(networkResourcesRoutes, routesUpdate...), DNSConfig: dnsUpdate, OfflinePeers: expiredPeers, - FirewallRules: firewallRules, + FirewallRules: connRes.firewallRules, RoutesFirewallRules: append(networkResourcesFirewallRules, routesFirewallRules...), - AuthorizedUsers: authorizedUsers, - EnableSSH: sshEnabled, + AuthorizedUsers: connRes.authorizedUsers, + VNCAuthorizedUsers: connRes.vncAuthorizedUsers, + EnableSSH: connRes.sshEnabled, } } -func (c *NetworkMapComponents) getPeerConnectionResources(targetPeerID string) ([]*nbpeer.Peer, []*FirewallRule, map[string]map[string]struct{}, bool) { +// peerConnectionResult holds the output of getPeerConnectionResources. +type peerConnectionResult struct { + peers []*nbpeer.Peer + firewallRules []*FirewallRule + authorizedUsers map[string]map[string]struct{} + vncAuthorizedUsers map[string]map[string]struct{} + sshEnabled bool +} + +func (c *NetworkMapComponents) getPeerConnectionResources(targetPeerID string) peerConnectionResult { targetPeer := c.GetPeerInfo(targetPeerID) if targetPeer == nil { - return nil, nil, nil, false + return peerConnectionResult{} } generateResources, getAccumulatedResources := c.connResourcesGenerator(targetPeer) authorizedUsers := make(map[string]map[string]struct{}) + vncAuthorizedUsers := make(map[string]map[string]struct{}) sshEnabled := false for _, policy := range c.Policies { @@ -222,35 +234,9 @@ func (c *NetworkMapComponents) getPeerConnectionResources(targetPeerID string) ( if peerInDestinations && rule.Protocol == PolicyRuleProtocolNetbirdSSH { sshEnabled = true - switch { - case len(rule.AuthorizedGroups) > 0: - for groupID, localUsers := range rule.AuthorizedGroups { - userIDs, ok := c.GroupIDToUserIDs[groupID] - if !ok { - continue - } - - if len(localUsers) == 0 { - localUsers = []string{auth.Wildcard} - } - - for _, localUser := range localUsers { - if authorizedUsers[localUser] == nil { - authorizedUsers[localUser] = make(map[string]struct{}) - } - for _, userID := range userIDs { - authorizedUsers[localUser][userID] = struct{}{} - } - } - } - case rule.AuthorizedUser != "": - if authorizedUsers[auth.Wildcard] == nil { - authorizedUsers[auth.Wildcard] = make(map[string]struct{}) - } - authorizedUsers[auth.Wildcard][rule.AuthorizedUser] = struct{}{} - default: - authorizedUsers[auth.Wildcard] = c.getAllowedUserIDs() - } + c.collectAuthorizedUsers(rule, authorizedUsers) + } else if peerInDestinations && rule.Protocol == PolicyRuleProtocolNetbirdVNC { + c.collectAuthorizedUsers(rule, vncAuthorizedUsers) } else if peerInDestinations && policyRuleImpliesLegacySSH(rule) && targetPeer.SSHEnabled { sshEnabled = true authorizedUsers[auth.Wildcard] = c.getAllowedUserIDs() @@ -259,7 +245,46 @@ func (c *NetworkMapComponents) getPeerConnectionResources(targetPeerID string) ( } peers, fwRules := getAccumulatedResources() - return peers, fwRules, authorizedUsers, sshEnabled + return peerConnectionResult{ + peers: peers, + firewallRules: fwRules, + authorizedUsers: authorizedUsers, + vncAuthorizedUsers: vncAuthorizedUsers, + sshEnabled: sshEnabled, + } +} + +// collectAuthorizedUsers populates the target map with authorized user mappings from the rule. +func (c *NetworkMapComponents) collectAuthorizedUsers(rule *PolicyRule, target map[string]map[string]struct{}) { + switch { + case len(rule.AuthorizedGroups) > 0: + for groupID, localUsers := range rule.AuthorizedGroups { + userIDs, ok := c.GroupIDToUserIDs[groupID] + if !ok { + continue + } + + if len(localUsers) == 0 { + localUsers = []string{auth.Wildcard} + } + + for _, localUser := range localUsers { + if target[localUser] == nil { + target[localUser] = make(map[string]struct{}) + } + for _, userID := range userIDs { + target[localUser][userID] = struct{}{} + } + } + } + case rule.AuthorizedUser != "": + if target[auth.Wildcard] == nil { + target[auth.Wildcard] = make(map[string]struct{}) + } + target[auth.Wildcard][rule.AuthorizedUser] = struct{}{} + default: + target[auth.Wildcard] = c.getAllowedUserIDs() + } } func (c *NetworkMapComponents) getAllowedUserIDs() map[string]struct{} { @@ -279,7 +304,7 @@ func (c *NetworkMapComponents) connResourcesGenerator(targetPeer *nbpeer.Peer) ( return func(rule *PolicyRule, groupPeers []*nbpeer.Peer, direction int) { protocol := rule.Protocol - if protocol == PolicyRuleProtocolNetbirdSSH { + if protocol == PolicyRuleProtocolNetbirdSSH || protocol == PolicyRuleProtocolNetbirdVNC { protocol = PolicyRuleProtocolTCP } @@ -526,7 +551,6 @@ func (c *NetworkMapComponents) getRoutingPeerRoutes(peerID string) (enabledRoute return enabledRoutes, disabledRoutes } - func (c *NetworkMapComponents) filterRoutesByGroups(routes []*route.Route, groupListMap LookupMap) []*route.Route { var filteredRoutes []*route.Route for _, r := range routes { diff --git a/management/server/types/networkmapbuilder.go b/management/server/types/networkmapbuilder.go index 6448b8403..d21f89bec 100644 --- a/management/server/types/networkmapbuilder.go +++ b/management/server/types/networkmapbuilder.go @@ -1019,7 +1019,7 @@ func (b *NetworkMapBuilder) buildAllowedUserIDs(account *Account) map[string]str } func firewallRuleProtocol(protocol PolicyRuleProtocolType) string { - if protocol == PolicyRuleProtocolNetbirdSSH { + if protocol == PolicyRuleProtocolNetbirdSSH || protocol == PolicyRuleProtocolNetbirdVNC { return string(PolicyRuleProtocolTCP) } return string(protocol) diff --git a/management/server/types/policy.go b/management/server/types/policy.go index d4e1a8816..5d0e7a0d6 100644 --- a/management/server/types/policy.go +++ b/management/server/types/policy.go @@ -25,6 +25,8 @@ const ( PolicyRuleProtocolICMP = PolicyRuleProtocolType("icmp") // PolicyRuleProtocolNetbirdSSH type of traffic PolicyRuleProtocolNetbirdSSH = PolicyRuleProtocolType("netbird-ssh") + // PolicyRuleProtocolNetbirdVNC type of traffic + PolicyRuleProtocolNetbirdVNC = PolicyRuleProtocolType("netbird-vnc") ) const ( @@ -171,6 +173,8 @@ func ParseRuleString(rule string) (PolicyRuleProtocolType, RulePortRange, error) return "", RulePortRange{}, errors.New("icmp does not accept ports; use 'icmp' without '/…'") case "netbird-ssh": return PolicyRuleProtocolNetbirdSSH, RulePortRange{Start: nativeSSHPortNumber, End: nativeSSHPortNumber}, nil + case "netbird-vnc": + return PolicyRuleProtocolNetbirdVNC, RulePortRange{Start: vncInternalPort, End: vncInternalPort}, nil default: return "", RulePortRange{}, fmt.Errorf("invalid protocol: %q", protoStr) } diff --git a/management/server/types/settings.go b/management/server/types/settings.go index 4ea79ec72..015e989f3 100644 --- a/management/server/types/settings.go +++ b/management/server/types/settings.go @@ -52,6 +52,19 @@ type Settings struct { // PeerExposeGroups list of peer group IDs allowed to expose services PeerExposeGroups []string `gorm:"serializer:json"` + // RecordingEnabled enables session recording for peers in RecordingGroups + RecordingEnabled bool + // RecordingGroups list of peer group IDs that have session recording enabled + RecordingGroups []string `gorm:"serializer:json"` + // RecordingMaxSessions limits the number of recording files kept per peer (0 = unlimited) + RecordingMaxSessions int32 + // RecordingMaxTotalSizeMB limits total recording size in MB per peer (0 = unlimited) + RecordingMaxTotalSizeMB int64 + // RecordingInputEnabled controls whether keyboard input is captured (default true) + RecordingInputEnabled *bool + // RecordingEncryptionKey is a base64-encoded public key for encrypting recordings + RecordingEncryptionKey string + // Extra is a dictionary of Account settings Extra *ExtraSettings `gorm:"embedded;embeddedPrefix:extra_"` @@ -91,6 +104,12 @@ func (s *Settings) Copy() *Settings { RoutingPeerDNSResolutionEnabled: s.RoutingPeerDNSResolutionEnabled, PeerExposeEnabled: s.PeerExposeEnabled, PeerExposeGroups: slices.Clone(s.PeerExposeGroups), + RecordingEnabled: s.RecordingEnabled, + RecordingGroups: slices.Clone(s.RecordingGroups), + RecordingMaxSessions: s.RecordingMaxSessions, + RecordingMaxTotalSizeMB: s.RecordingMaxTotalSizeMB, + RecordingInputEnabled: s.RecordingInputEnabled, + RecordingEncryptionKey: s.RecordingEncryptionKey, LazyConnectionEnabled: s.LazyConnectionEnabled, DNSDomain: s.DNSDomain, NetworkRange: s.NetworkRange, diff --git a/shared/auth/jwt/token_age.go b/shared/auth/jwt/token_age.go new file mode 100644 index 000000000..1874a9f8f --- /dev/null +++ b/shared/auth/jwt/token_age.go @@ -0,0 +1,65 @@ +package jwt + +import ( + "errors" + "fmt" + "time" + + gojwt "github.com/golang-jwt/jwt/v5" +) + +// ErrTokenExpired signals that the iat-based token age check failed. Callers +// use errors.Is to branch on it when they want to surface a stable machine- +// readable reason (e.g. so a dashboard can prompt for re-login). +var ErrTokenExpired = errors.New("token expired") + +// CheckTokenAge validates that a JWT token's iat claim is within the given +// maxAge duration. Returns an error if the claims are unparseable, the iat +// claim is missing, or the token is too old. +func CheckTokenAge(token *gojwt.Token, maxAge time.Duration) error { + claims, ok := token.Claims.(gojwt.MapClaims) + if !ok { + return fmt.Errorf("token has invalid claims format (user=%s)", UserIDFromToken(token)) + } + + iat, ok := claims["iat"].(float64) + if !ok { + return fmt.Errorf("token missing iat claim (user=%s)", UserIDFromToken(token)) + } + + issuedAt := time.Unix(int64(iat), 0) + tokenAge := time.Since(issuedAt) + if tokenAge > maxAge { + return fmt.Errorf("%w for user=%s: age=%v, max=%v", ErrTokenExpired, userIDFromClaims(claims), tokenAge, maxAge) + } + + return nil +} + +// UserIDFromToken extracts a human-readable user identifier from a JWT token +// for use in error messages. Returns "unknown" if the token or claims are nil. +func UserIDFromToken(token *gojwt.Token) string { + if token == nil { + return "unknown" + } + claims, ok := token.Claims.(gojwt.MapClaims) + if !ok { + return "unknown" + } + return userIDFromClaims(claims) +} + +// userIDFromClaims extracts a user identifier from JWT claims, trying sub, +// user_id, and email in order. +func userIDFromClaims(claims gojwt.MapClaims) string { + if sub, ok := claims["sub"].(string); ok && sub != "" { + return sub + } + if userID, ok := claims["user_id"].(string); ok && userID != "" { + return userID + } + if email, ok := claims["email"].(string); ok && email != "" { + return email + } + return "unknown" +} diff --git a/shared/management/client/grpc.go b/shared/management/client/grpc.go index a01e51abc..9360a1172 100644 --- a/shared/management/client/grpc.go +++ b/shared/management/client/grpc.go @@ -941,6 +941,7 @@ func infoToMetaData(info *system.Info) *proto.PeerSystemMeta { RosenpassEnabled: info.RosenpassEnabled, RosenpassPermissive: info.RosenpassPermissive, ServerSSHAllowed: info.ServerSSHAllowed, + ServerVNCAllowed: info.ServerVNCAllowed, DisableClientRoutes: info.DisableClientRoutes, DisableServerRoutes: info.DisableServerRoutes, @@ -950,6 +951,9 @@ func infoToMetaData(info *system.Info) *proto.PeerSystemMeta { BlockInbound: info.BlockInbound, LazyConnectionEnabled: info.LazyConnectionEnabled, + + DisableSSHAuth: info.DisableSSHAuth, + DisableVNCAuth: info.DisableVNCAuth, }, } } diff --git a/shared/management/http/api/openapi.yml b/shared/management/http/api/openapi.yml index 0b855db67..dc0e6eb94 100644 --- a/shared/management/http/api/openapi.yml +++ b/shared/management/http/api/openapi.yml @@ -352,6 +352,33 @@ components: items: type: string example: ch8i4ug6lnn4g9hqv7m0 + recording_enabled: + description: Enables session recording (SSH and VNC) for peers in the selected groups. + type: boolean + example: false + recording_groups: + description: Peer group IDs that have session recording enabled. + type: array + items: + type: string + example: ch8i4ug6lnn4g9hqv7m0 + recording_max_sessions: + description: Maximum number of recording files to keep per peer. 0 means unlimited. + type: integer + example: 0 + recording_max_total_size_mb: + description: Maximum total size in MB of recordings per peer. 0 means unlimited. + type: integer + format: int64 + example: 0 + recording_input_enabled: + description: Controls whether keyboard input is captured in SSH recordings. Defaults to true. + type: boolean + example: true + recording_encryption_key: + description: Base64-encoded public key for encrypting session recordings. When set, recordings are encrypted with a per-session AES-256-GCM key wrapped with this public key. + type: string + example: "" extra: $ref: '#/components/schemas/AccountExtraSettings' lazy_connection_enabled: @@ -934,6 +961,14 @@ components: description: Indicates whether SSH access this peer is allowed or not type: boolean example: true + server_vnc_allowed: + description: Indicates whether the embedded VNC server is enabled on this peer + type: boolean + example: false + disable_vnc_auth: + description: Indicates whether VNC JWT authentication is disabled on this peer + type: boolean + example: false disable_client_routes: description: Indicates whether client routes are disabled on this peer or not type: boolean @@ -1384,7 +1419,7 @@ components: protocol: description: Policy rule type of the traffic type: string - enum: [ "all", "tcp", "udp", "icmp", "netbird-ssh" ] + enum: [ "all", "tcp", "udp", "icmp", "netbird-ssh", "netbird-vnc" ] example: "tcp" ports: description: Policy rule affected ports diff --git a/shared/management/http/api/types.gen.go b/shared/management/http/api/types.gen.go index 0317b8183..e02303419 100644 --- a/shared/management/http/api/types.gen.go +++ b/shared/management/http/api/types.gen.go @@ -763,6 +763,7 @@ const ( PolicyRuleProtocolAll PolicyRuleProtocol = "all" PolicyRuleProtocolIcmp PolicyRuleProtocol = "icmp" PolicyRuleProtocolNetbirdSsh PolicyRuleProtocol = "netbird-ssh" + PolicyRuleProtocolNetbirdVnc PolicyRuleProtocol = "netbird-vnc" PolicyRuleProtocolTcp PolicyRuleProtocol = "tcp" PolicyRuleProtocolUdp PolicyRuleProtocol = "udp" ) @@ -776,6 +777,8 @@ func (e PolicyRuleProtocol) Valid() bool { return true case PolicyRuleProtocolNetbirdSsh: return true + case PolicyRuleProtocolNetbirdVnc: + return true case PolicyRuleProtocolTcp: return true case PolicyRuleProtocolUdp: @@ -808,6 +811,7 @@ const ( PolicyRuleMinimumProtocolAll PolicyRuleMinimumProtocol = "all" PolicyRuleMinimumProtocolIcmp PolicyRuleMinimumProtocol = "icmp" PolicyRuleMinimumProtocolNetbirdSsh PolicyRuleMinimumProtocol = "netbird-ssh" + PolicyRuleMinimumProtocolNetbirdVnc PolicyRuleMinimumProtocol = "netbird-vnc" PolicyRuleMinimumProtocolTcp PolicyRuleMinimumProtocol = "tcp" PolicyRuleMinimumProtocolUdp PolicyRuleMinimumProtocol = "udp" ) @@ -821,6 +825,8 @@ func (e PolicyRuleMinimumProtocol) Valid() bool { return true case PolicyRuleMinimumProtocolNetbirdSsh: return true + case PolicyRuleMinimumProtocolNetbirdVnc: + return true case PolicyRuleMinimumProtocolTcp: return true case PolicyRuleMinimumProtocolUdp: @@ -853,6 +859,7 @@ const ( PolicyRuleUpdateProtocolAll PolicyRuleUpdateProtocol = "all" PolicyRuleUpdateProtocolIcmp PolicyRuleUpdateProtocol = "icmp" PolicyRuleUpdateProtocolNetbirdSsh PolicyRuleUpdateProtocol = "netbird-ssh" + PolicyRuleUpdateProtocolNetbirdVnc PolicyRuleUpdateProtocol = "netbird-vnc" PolicyRuleUpdateProtocolTcp PolicyRuleUpdateProtocol = "tcp" PolicyRuleUpdateProtocolUdp PolicyRuleUpdateProtocol = "udp" ) @@ -866,6 +873,8 @@ func (e PolicyRuleUpdateProtocol) Valid() bool { return true case PolicyRuleUpdateProtocolNetbirdSsh: return true + case PolicyRuleUpdateProtocolNetbirdVnc: + return true case PolicyRuleUpdateProtocolTcp: return true case PolicyRuleUpdateProtocolUdp: @@ -1498,6 +1507,24 @@ type AccountSettings struct { // PeerLoginExpirationEnabled Enables or disables peer login expiration globally. After peer's login has expired the user has to log in (authenticate). Applies only to peers that were added by a user (interactive SSO login). PeerLoginExpirationEnabled bool `json:"peer_login_expiration_enabled"` + // RecordingEnabled Enables session recording (SSH and VNC) for peers in the selected groups. + RecordingEnabled *bool `json:"recording_enabled,omitempty"` + + // RecordingEncryptionKey Base64-encoded public key for encrypting session recordings. When set, recordings are encrypted with a per-session AES-256-GCM key wrapped with this public key. + RecordingEncryptionKey *string `json:"recording_encryption_key,omitempty"` + + // RecordingGroups Peer group IDs that have session recording enabled. + RecordingGroups *[]string `json:"recording_groups,omitempty"` + + // RecordingInputEnabled Controls whether keyboard input is captured in SSH recordings. Defaults to true. + RecordingInputEnabled *bool `json:"recording_input_enabled,omitempty"` + + // RecordingMaxSessions Maximum number of recording files to keep per peer. 0 means unlimited. + RecordingMaxSessions *int `json:"recording_max_sessions,omitempty"` + + // RecordingMaxTotalSizeMb Maximum total size in MB of recordings per peer. 0 means unlimited. + RecordingMaxTotalSizeMb *int64 `json:"recording_max_total_size_mb,omitempty"` + // RegularUsersViewBlocked Allows blocking regular users from viewing parts of the system. RegularUsersViewBlocked bool `json:"regular_users_view_blocked"` @@ -3287,6 +3314,9 @@ type PeerLocalFlags struct { // DisableServerRoutes Indicates whether server routes are disabled on this peer or not DisableServerRoutes *bool `json:"disable_server_routes,omitempty"` + // DisableVncAuth Indicates whether VNC JWT authentication is disabled on this peer + DisableVncAuth *bool `json:"disable_vnc_auth,omitempty"` + // LazyConnectionEnabled Indicates whether lazy connection is enabled on this peer LazyConnectionEnabled *bool `json:"lazy_connection_enabled,omitempty"` @@ -3298,6 +3328,9 @@ type PeerLocalFlags struct { // ServerSshAllowed Indicates whether SSH access this peer is allowed or not ServerSshAllowed *bool `json:"server_ssh_allowed,omitempty"` + + // ServerVncAllowed Indicates whether the embedded VNC server is enabled on this peer + ServerVncAllowed *bool `json:"server_vnc_allowed,omitempty"` } // PeerMinimum defines model for PeerMinimum. diff --git a/shared/management/proto/management.pb.go b/shared/management/proto/management.pb.go index 604f9c793..39ec6272a 100644 --- a/shared/management/proto/management.pb.go +++ b/shared/management/proto/management.pb.go @@ -371,7 +371,7 @@ func (x DeviceAuthorizationFlowProvider) Number() protoreflect.EnumNumber { // Deprecated: Use DeviceAuthorizationFlowProvider.Descriptor instead. func (DeviceAuthorizationFlowProvider) EnumDescriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{31, 0} + return file_management_proto_rawDescGZIP(), []int{32, 0} } type EncryptedMessage struct { @@ -1201,6 +1201,8 @@ type Flags struct { EnableSSHLocalPortForwarding bool `protobuf:"varint,13,opt,name=enableSSHLocalPortForwarding,proto3" json:"enableSSHLocalPortForwarding,omitempty"` EnableSSHRemotePortForwarding bool `protobuf:"varint,14,opt,name=enableSSHRemotePortForwarding,proto3" json:"enableSSHRemotePortForwarding,omitempty"` DisableSSHAuth bool `protobuf:"varint,15,opt,name=disableSSHAuth,proto3" json:"disableSSHAuth,omitempty"` + DisableVNCAuth bool `protobuf:"varint,16,opt,name=disableVNCAuth,proto3" json:"disableVNCAuth,omitempty"` + ServerVNCAllowed bool `protobuf:"varint,17,opt,name=serverVNCAllowed,proto3" json:"serverVNCAllowed,omitempty"` } func (x *Flags) Reset() { @@ -1340,6 +1342,20 @@ func (x *Flags) GetDisableSSHAuth() bool { return false } +func (x *Flags) GetDisableVNCAuth() bool { + if x != nil { + return x.DisableVNCAuth + } + return false +} + +func (x *Flags) GetServerVNCAllowed() bool { + if x != nil { + return x.ServerVNCAllowed + } + return false +} + // PeerSystemMeta is machine meta data like OS and version. type PeerSystemMeta struct { state protoimpl.MessageState @@ -2343,6 +2359,8 @@ type NetworkMap struct { ForwardingRules []*ForwardingRule `protobuf:"bytes,12,rep,name=forwardingRules,proto3" json:"forwardingRules,omitempty"` // SSHAuth represents SSH authorization configuration SshAuth *SSHAuth `protobuf:"bytes,13,opt,name=sshAuth,proto3" json:"sshAuth,omitempty"` + // VNCAuth represents VNC authorization configuration + VncAuth *VNCAuth `protobuf:"bytes,14,opt,name=vncAuth,proto3" json:"vncAuth,omitempty"` } func (x *NetworkMap) Reset() { @@ -2468,6 +2486,13 @@ func (x *NetworkMap) GetSshAuth() *SSHAuth { return nil } +func (x *NetworkMap) GetVncAuth() *VNCAuth { + if x != nil { + return x.VncAuth + } + return nil +} + type SSHAuth struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2581,6 +2606,75 @@ func (x *MachineUserIndexes) GetIndexes() []uint32 { return nil } +// VNCAuth represents VNC authorization configuration for a peer. +type VNCAuth struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // UserIDClaim is the JWT claim to be used to get the users ID + UserIDClaim string `protobuf:"bytes,1,opt,name=UserIDClaim,proto3" json:"UserIDClaim,omitempty"` + // AuthorizedUsers is a list of hashed user IDs authorized to access this peer via VNC + AuthorizedUsers [][]byte `protobuf:"bytes,2,rep,name=AuthorizedUsers,proto3" json:"AuthorizedUsers,omitempty"` + // MachineUsers maps OS user names to their corresponding indexes in the AuthorizedUsers list. + // Used in session mode to determine which OS user to create the virtual session as. + // The wildcard "*" allows any OS user. + MachineUsers map[string]*MachineUserIndexes `protobuf:"bytes,3,rep,name=machine_users,json=machineUsers,proto3" json:"machine_users,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *VNCAuth) Reset() { + *x = VNCAuth{} + if protoimpl.UnsafeEnabled { + mi := &file_management_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VNCAuth) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VNCAuth) ProtoMessage() {} + +func (x *VNCAuth) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[28] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VNCAuth.ProtoReflect.Descriptor instead. +func (*VNCAuth) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{28} +} + +func (x *VNCAuth) GetUserIDClaim() string { + if x != nil { + return x.UserIDClaim + } + return "" +} + +func (x *VNCAuth) GetAuthorizedUsers() [][]byte { + if x != nil { + return x.AuthorizedUsers + } + return nil +} + +func (x *VNCAuth) GetMachineUsers() map[string]*MachineUserIndexes { + if x != nil { + return x.MachineUsers + } + return nil +} + // RemotePeerConfig represents a configuration of a remote peer. // The properties are used to configure WireGuard Peers sections type RemotePeerConfig struct { @@ -2602,7 +2696,7 @@ type RemotePeerConfig struct { func (x *RemotePeerConfig) Reset() { *x = RemotePeerConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[28] + mi := &file_management_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2615,7 +2709,7 @@ func (x *RemotePeerConfig) String() string { func (*RemotePeerConfig) ProtoMessage() {} func (x *RemotePeerConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[28] + mi := &file_management_proto_msgTypes[29] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2628,7 +2722,7 @@ func (x *RemotePeerConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use RemotePeerConfig.ProtoReflect.Descriptor instead. func (*RemotePeerConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{28} + return file_management_proto_rawDescGZIP(), []int{29} } func (x *RemotePeerConfig) GetWgPubKey() string { @@ -2678,12 +2772,21 @@ type SSHConfig struct { // This property should be ignore if SSHConfig comes from PeerConfig. SshPubKey []byte `protobuf:"bytes,2,opt,name=sshPubKey,proto3" json:"sshPubKey,omitempty"` JwtConfig *JWTConfig `protobuf:"bytes,3,opt,name=jwtConfig,proto3" json:"jwtConfig,omitempty"` + // Session recording settings (shared for SSH and VNC) + EnableRecording bool `protobuf:"varint,4,opt,name=enableRecording,proto3" json:"enableRecording,omitempty"` + RecordingMaxSessions int32 `protobuf:"varint,5,opt,name=recordingMaxSessions,proto3" json:"recordingMaxSessions,omitempty"` + RecordingMaxTotalSizeMb int64 `protobuf:"varint,6,opt,name=recordingMaxTotalSizeMb,proto3" json:"recordingMaxTotalSizeMb,omitempty"` + RecordInputEnabled bool `protobuf:"varint,7,opt,name=recordInputEnabled,proto3" json:"recordInputEnabled,omitempty"` + // Recording encryption: DER-encoded public key for hybrid AES-GCM encryption. + // If set, recordings are encrypted with a per-session AES-256 key, which is + // itself encrypted with this public key and stored in the recording header. + RecordingEncryptionKey []byte `protobuf:"bytes,8,opt,name=recordingEncryptionKey,proto3" json:"recordingEncryptionKey,omitempty"` } func (x *SSHConfig) Reset() { *x = SSHConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[29] + mi := &file_management_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2696,7 +2799,7 @@ func (x *SSHConfig) String() string { func (*SSHConfig) ProtoMessage() {} func (x *SSHConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[29] + mi := &file_management_proto_msgTypes[30] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2709,7 +2812,7 @@ func (x *SSHConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use SSHConfig.ProtoReflect.Descriptor instead. func (*SSHConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{29} + return file_management_proto_rawDescGZIP(), []int{30} } func (x *SSHConfig) GetSshEnabled() bool { @@ -2733,6 +2836,41 @@ func (x *SSHConfig) GetJwtConfig() *JWTConfig { return nil } +func (x *SSHConfig) GetEnableRecording() bool { + if x != nil { + return x.EnableRecording + } + return false +} + +func (x *SSHConfig) GetRecordingMaxSessions() int32 { + if x != nil { + return x.RecordingMaxSessions + } + return 0 +} + +func (x *SSHConfig) GetRecordingMaxTotalSizeMb() int64 { + if x != nil { + return x.RecordingMaxTotalSizeMb + } + return 0 +} + +func (x *SSHConfig) GetRecordInputEnabled() bool { + if x != nil { + return x.RecordInputEnabled + } + return false +} + +func (x *SSHConfig) GetRecordingEncryptionKey() []byte { + if x != nil { + return x.RecordingEncryptionKey + } + return nil +} + // DeviceAuthorizationFlowRequest empty struct for future expansion type DeviceAuthorizationFlowRequest struct { state protoimpl.MessageState @@ -2743,7 +2881,7 @@ type DeviceAuthorizationFlowRequest struct { func (x *DeviceAuthorizationFlowRequest) Reset() { *x = DeviceAuthorizationFlowRequest{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[30] + mi := &file_management_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2756,7 +2894,7 @@ func (x *DeviceAuthorizationFlowRequest) String() string { func (*DeviceAuthorizationFlowRequest) ProtoMessage() {} func (x *DeviceAuthorizationFlowRequest) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[30] + mi := &file_management_proto_msgTypes[31] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2769,7 +2907,7 @@ func (x *DeviceAuthorizationFlowRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeviceAuthorizationFlowRequest.ProtoReflect.Descriptor instead. func (*DeviceAuthorizationFlowRequest) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{30} + return file_management_proto_rawDescGZIP(), []int{31} } // DeviceAuthorizationFlow represents Device Authorization Flow information @@ -2788,7 +2926,7 @@ type DeviceAuthorizationFlow struct { func (x *DeviceAuthorizationFlow) Reset() { *x = DeviceAuthorizationFlow{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[31] + mi := &file_management_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2801,7 +2939,7 @@ func (x *DeviceAuthorizationFlow) String() string { func (*DeviceAuthorizationFlow) ProtoMessage() {} func (x *DeviceAuthorizationFlow) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[31] + mi := &file_management_proto_msgTypes[32] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2814,7 +2952,7 @@ func (x *DeviceAuthorizationFlow) ProtoReflect() protoreflect.Message { // Deprecated: Use DeviceAuthorizationFlow.ProtoReflect.Descriptor instead. func (*DeviceAuthorizationFlow) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{31} + return file_management_proto_rawDescGZIP(), []int{32} } func (x *DeviceAuthorizationFlow) GetProvider() DeviceAuthorizationFlowProvider { @@ -2841,7 +2979,7 @@ type PKCEAuthorizationFlowRequest struct { func (x *PKCEAuthorizationFlowRequest) Reset() { *x = PKCEAuthorizationFlowRequest{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[32] + mi := &file_management_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2854,7 +2992,7 @@ func (x *PKCEAuthorizationFlowRequest) String() string { func (*PKCEAuthorizationFlowRequest) ProtoMessage() {} func (x *PKCEAuthorizationFlowRequest) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[32] + mi := &file_management_proto_msgTypes[33] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2867,7 +3005,7 @@ func (x *PKCEAuthorizationFlowRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PKCEAuthorizationFlowRequest.ProtoReflect.Descriptor instead. func (*PKCEAuthorizationFlowRequest) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{32} + return file_management_proto_rawDescGZIP(), []int{33} } // PKCEAuthorizationFlow represents Authorization Code Flow information @@ -2884,7 +3022,7 @@ type PKCEAuthorizationFlow struct { func (x *PKCEAuthorizationFlow) Reset() { *x = PKCEAuthorizationFlow{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[33] + mi := &file_management_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2897,7 +3035,7 @@ func (x *PKCEAuthorizationFlow) String() string { func (*PKCEAuthorizationFlow) ProtoMessage() {} func (x *PKCEAuthorizationFlow) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[33] + mi := &file_management_proto_msgTypes[34] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2910,7 +3048,7 @@ func (x *PKCEAuthorizationFlow) ProtoReflect() protoreflect.Message { // Deprecated: Use PKCEAuthorizationFlow.ProtoReflect.Descriptor instead. func (*PKCEAuthorizationFlow) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{33} + return file_management_proto_rawDescGZIP(), []int{34} } func (x *PKCEAuthorizationFlow) GetProviderConfig() *ProviderConfig { @@ -2958,7 +3096,7 @@ type ProviderConfig struct { func (x *ProviderConfig) Reset() { *x = ProviderConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[34] + mi := &file_management_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2971,7 +3109,7 @@ func (x *ProviderConfig) String() string { func (*ProviderConfig) ProtoMessage() {} func (x *ProviderConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[34] + mi := &file_management_proto_msgTypes[35] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2984,7 +3122,7 @@ func (x *ProviderConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderConfig.ProtoReflect.Descriptor instead. func (*ProviderConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{34} + return file_management_proto_rawDescGZIP(), []int{35} } func (x *ProviderConfig) GetClientID() string { @@ -3093,7 +3231,7 @@ type Route struct { func (x *Route) Reset() { *x = Route{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[35] + mi := &file_management_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3106,7 +3244,7 @@ func (x *Route) String() string { func (*Route) ProtoMessage() {} func (x *Route) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[35] + mi := &file_management_proto_msgTypes[36] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3119,7 +3257,7 @@ func (x *Route) ProtoReflect() protoreflect.Message { // Deprecated: Use Route.ProtoReflect.Descriptor instead. func (*Route) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{35} + return file_management_proto_rawDescGZIP(), []int{36} } func (x *Route) GetID() string { @@ -3208,7 +3346,7 @@ type DNSConfig struct { func (x *DNSConfig) Reset() { *x = DNSConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[36] + mi := &file_management_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3221,7 +3359,7 @@ func (x *DNSConfig) String() string { func (*DNSConfig) ProtoMessage() {} func (x *DNSConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[36] + mi := &file_management_proto_msgTypes[37] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3234,7 +3372,7 @@ func (x *DNSConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use DNSConfig.ProtoReflect.Descriptor instead. func (*DNSConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{36} + return file_management_proto_rawDescGZIP(), []int{37} } func (x *DNSConfig) GetServiceEnable() bool { @@ -3283,7 +3421,7 @@ type CustomZone struct { func (x *CustomZone) Reset() { *x = CustomZone{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[37] + mi := &file_management_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3296,7 +3434,7 @@ func (x *CustomZone) String() string { func (*CustomZone) ProtoMessage() {} func (x *CustomZone) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[37] + mi := &file_management_proto_msgTypes[38] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3309,7 +3447,7 @@ func (x *CustomZone) ProtoReflect() protoreflect.Message { // Deprecated: Use CustomZone.ProtoReflect.Descriptor instead. func (*CustomZone) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{37} + return file_management_proto_rawDescGZIP(), []int{38} } func (x *CustomZone) GetDomain() string { @@ -3356,7 +3494,7 @@ type SimpleRecord struct { func (x *SimpleRecord) Reset() { *x = SimpleRecord{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[38] + mi := &file_management_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3369,7 +3507,7 @@ func (x *SimpleRecord) String() string { func (*SimpleRecord) ProtoMessage() {} func (x *SimpleRecord) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[38] + mi := &file_management_proto_msgTypes[39] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3382,7 +3520,7 @@ func (x *SimpleRecord) ProtoReflect() protoreflect.Message { // Deprecated: Use SimpleRecord.ProtoReflect.Descriptor instead. func (*SimpleRecord) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{38} + return file_management_proto_rawDescGZIP(), []int{39} } func (x *SimpleRecord) GetName() string { @@ -3435,7 +3573,7 @@ type NameServerGroup struct { func (x *NameServerGroup) Reset() { *x = NameServerGroup{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[39] + mi := &file_management_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3448,7 +3586,7 @@ func (x *NameServerGroup) String() string { func (*NameServerGroup) ProtoMessage() {} func (x *NameServerGroup) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[39] + mi := &file_management_proto_msgTypes[40] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3461,7 +3599,7 @@ func (x *NameServerGroup) ProtoReflect() protoreflect.Message { // Deprecated: Use NameServerGroup.ProtoReflect.Descriptor instead. func (*NameServerGroup) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{39} + return file_management_proto_rawDescGZIP(), []int{40} } func (x *NameServerGroup) GetNameServers() []*NameServer { @@ -3506,7 +3644,7 @@ type NameServer struct { func (x *NameServer) Reset() { *x = NameServer{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[40] + mi := &file_management_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3519,7 +3657,7 @@ func (x *NameServer) String() string { func (*NameServer) ProtoMessage() {} func (x *NameServer) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[40] + mi := &file_management_proto_msgTypes[41] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3532,7 +3670,7 @@ func (x *NameServer) ProtoReflect() protoreflect.Message { // Deprecated: Use NameServer.ProtoReflect.Descriptor instead. func (*NameServer) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{40} + return file_management_proto_rawDescGZIP(), []int{41} } func (x *NameServer) GetIP() string { @@ -3575,7 +3713,7 @@ type FirewallRule struct { func (x *FirewallRule) Reset() { *x = FirewallRule{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[41] + mi := &file_management_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3588,7 +3726,7 @@ func (x *FirewallRule) String() string { func (*FirewallRule) ProtoMessage() {} func (x *FirewallRule) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[41] + mi := &file_management_proto_msgTypes[42] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3601,7 +3739,7 @@ func (x *FirewallRule) ProtoReflect() protoreflect.Message { // Deprecated: Use FirewallRule.ProtoReflect.Descriptor instead. func (*FirewallRule) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{41} + return file_management_proto_rawDescGZIP(), []int{42} } func (x *FirewallRule) GetPeerIP() string { @@ -3665,7 +3803,7 @@ type NetworkAddress struct { func (x *NetworkAddress) Reset() { *x = NetworkAddress{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[42] + mi := &file_management_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3678,7 +3816,7 @@ func (x *NetworkAddress) String() string { func (*NetworkAddress) ProtoMessage() {} func (x *NetworkAddress) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[42] + mi := &file_management_proto_msgTypes[43] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3691,7 +3829,7 @@ func (x *NetworkAddress) ProtoReflect() protoreflect.Message { // Deprecated: Use NetworkAddress.ProtoReflect.Descriptor instead. func (*NetworkAddress) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{42} + return file_management_proto_rawDescGZIP(), []int{43} } func (x *NetworkAddress) GetNetIP() string { @@ -3719,7 +3857,7 @@ type Checks struct { func (x *Checks) Reset() { *x = Checks{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[43] + mi := &file_management_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3732,7 +3870,7 @@ func (x *Checks) String() string { func (*Checks) ProtoMessage() {} func (x *Checks) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[43] + mi := &file_management_proto_msgTypes[44] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3745,7 +3883,7 @@ func (x *Checks) ProtoReflect() protoreflect.Message { // Deprecated: Use Checks.ProtoReflect.Descriptor instead. func (*Checks) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{43} + return file_management_proto_rawDescGZIP(), []int{44} } func (x *Checks) GetFiles() []string { @@ -3770,7 +3908,7 @@ type PortInfo struct { func (x *PortInfo) Reset() { *x = PortInfo{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[44] + mi := &file_management_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3783,7 +3921,7 @@ func (x *PortInfo) String() string { func (*PortInfo) ProtoMessage() {} func (x *PortInfo) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[44] + mi := &file_management_proto_msgTypes[45] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3796,7 +3934,7 @@ func (x *PortInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use PortInfo.ProtoReflect.Descriptor instead. func (*PortInfo) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{44} + return file_management_proto_rawDescGZIP(), []int{45} } func (m *PortInfo) GetPortSelection() isPortInfo_PortSelection { @@ -3867,7 +4005,7 @@ type RouteFirewallRule struct { func (x *RouteFirewallRule) Reset() { *x = RouteFirewallRule{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[45] + mi := &file_management_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3880,7 +4018,7 @@ func (x *RouteFirewallRule) String() string { func (*RouteFirewallRule) ProtoMessage() {} func (x *RouteFirewallRule) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[45] + mi := &file_management_proto_msgTypes[46] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3893,7 +4031,7 @@ func (x *RouteFirewallRule) ProtoReflect() protoreflect.Message { // Deprecated: Use RouteFirewallRule.ProtoReflect.Descriptor instead. func (*RouteFirewallRule) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{45} + return file_management_proto_rawDescGZIP(), []int{46} } func (x *RouteFirewallRule) GetSourceRanges() []string { @@ -3984,7 +4122,7 @@ type ForwardingRule struct { func (x *ForwardingRule) Reset() { *x = ForwardingRule{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[46] + mi := &file_management_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3997,7 +4135,7 @@ func (x *ForwardingRule) String() string { func (*ForwardingRule) ProtoMessage() {} func (x *ForwardingRule) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[46] + mi := &file_management_proto_msgTypes[47] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4010,7 +4148,7 @@ func (x *ForwardingRule) ProtoReflect() protoreflect.Message { // Deprecated: Use ForwardingRule.ProtoReflect.Descriptor instead. func (*ForwardingRule) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{46} + return file_management_proto_rawDescGZIP(), []int{47} } func (x *ForwardingRule) GetProtocol() RuleProtocol { @@ -4059,7 +4197,7 @@ type ExposeServiceRequest struct { func (x *ExposeServiceRequest) Reset() { *x = ExposeServiceRequest{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[47] + mi := &file_management_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4072,7 +4210,7 @@ func (x *ExposeServiceRequest) String() string { func (*ExposeServiceRequest) ProtoMessage() {} func (x *ExposeServiceRequest) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[47] + mi := &file_management_proto_msgTypes[48] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4085,7 +4223,7 @@ func (x *ExposeServiceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ExposeServiceRequest.ProtoReflect.Descriptor instead. func (*ExposeServiceRequest) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{47} + return file_management_proto_rawDescGZIP(), []int{48} } func (x *ExposeServiceRequest) GetPort() uint32 { @@ -4158,7 +4296,7 @@ type ExposeServiceResponse struct { func (x *ExposeServiceResponse) Reset() { *x = ExposeServiceResponse{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[48] + mi := &file_management_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4171,7 +4309,7 @@ func (x *ExposeServiceResponse) String() string { func (*ExposeServiceResponse) ProtoMessage() {} func (x *ExposeServiceResponse) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[48] + mi := &file_management_proto_msgTypes[49] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4184,7 +4322,7 @@ func (x *ExposeServiceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ExposeServiceResponse.ProtoReflect.Descriptor instead. func (*ExposeServiceResponse) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{48} + return file_management_proto_rawDescGZIP(), []int{49} } func (x *ExposeServiceResponse) GetServiceName() string { @@ -4226,7 +4364,7 @@ type RenewExposeRequest struct { func (x *RenewExposeRequest) Reset() { *x = RenewExposeRequest{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[49] + mi := &file_management_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4239,7 +4377,7 @@ func (x *RenewExposeRequest) String() string { func (*RenewExposeRequest) ProtoMessage() {} func (x *RenewExposeRequest) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[49] + mi := &file_management_proto_msgTypes[50] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4252,7 +4390,7 @@ func (x *RenewExposeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RenewExposeRequest.ProtoReflect.Descriptor instead. func (*RenewExposeRequest) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{49} + return file_management_proto_rawDescGZIP(), []int{50} } func (x *RenewExposeRequest) GetDomain() string { @@ -4271,7 +4409,7 @@ type RenewExposeResponse struct { func (x *RenewExposeResponse) Reset() { *x = RenewExposeResponse{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[50] + mi := &file_management_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4284,7 +4422,7 @@ func (x *RenewExposeResponse) String() string { func (*RenewExposeResponse) ProtoMessage() {} func (x *RenewExposeResponse) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[50] + mi := &file_management_proto_msgTypes[51] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4297,7 +4435,7 @@ func (x *RenewExposeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RenewExposeResponse.ProtoReflect.Descriptor instead. func (*RenewExposeResponse) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{50} + return file_management_proto_rawDescGZIP(), []int{51} } type StopExposeRequest struct { @@ -4311,7 +4449,7 @@ type StopExposeRequest struct { func (x *StopExposeRequest) Reset() { *x = StopExposeRequest{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[51] + mi := &file_management_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4324,7 +4462,7 @@ func (x *StopExposeRequest) String() string { func (*StopExposeRequest) ProtoMessage() {} func (x *StopExposeRequest) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[51] + mi := &file_management_proto_msgTypes[52] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4337,7 +4475,7 @@ func (x *StopExposeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StopExposeRequest.ProtoReflect.Descriptor instead. func (*StopExposeRequest) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{51} + return file_management_proto_rawDescGZIP(), []int{52} } func (x *StopExposeRequest) GetDomain() string { @@ -4356,7 +4494,7 @@ type StopExposeResponse struct { func (x *StopExposeResponse) Reset() { *x = StopExposeResponse{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[52] + mi := &file_management_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4369,7 +4507,7 @@ func (x *StopExposeResponse) String() string { func (*StopExposeResponse) ProtoMessage() {} func (x *StopExposeResponse) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[52] + mi := &file_management_proto_msgTypes[53] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4382,7 +4520,7 @@ func (x *StopExposeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StopExposeResponse.ProtoReflect.Descriptor instead. func (*StopExposeResponse) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{52} + return file_management_proto_rawDescGZIP(), []int{53} } type PortInfo_Range struct { @@ -4397,7 +4535,7 @@ type PortInfo_Range struct { func (x *PortInfo_Range) Reset() { *x = PortInfo_Range{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[54] + mi := &file_management_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4410,7 +4548,7 @@ func (x *PortInfo_Range) String() string { func (*PortInfo_Range) ProtoMessage() {} func (x *PortInfo_Range) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[54] + mi := &file_management_proto_msgTypes[56] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4423,7 +4561,7 @@ func (x *PortInfo_Range) ProtoReflect() protoreflect.Message { // Deprecated: Use PortInfo_Range.ProtoReflect.Descriptor instead. func (*PortInfo_Range) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{44, 0} + return file_management_proto_rawDescGZIP(), []int{45, 0} } func (x *PortInfo_Range) GetStart() uint32 { @@ -4542,7 +4680,7 @@ var file_management_proto_rawDesc = []byte{ 0x01, 0x28, 0x08, 0x52, 0x05, 0x65, 0x78, 0x69, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x10, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x49, 0x73, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x49, 0x73, 0x52, - 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x22, 0xbf, 0x05, 0x0a, 0x05, 0x46, 0x6c, 0x61, 0x67, 0x73, + 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x22, 0x93, 0x06, 0x0a, 0x05, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, @@ -4586,551 +4724,591 @@ var file_management_proto_rawDesc = []byte{ 0x74, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x26, 0x0a, 0x0e, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x53, 0x48, 0x41, 0x75, 0x74, 0x68, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, - 0x65, 0x53, 0x53, 0x48, 0x41, 0x75, 0x74, 0x68, 0x22, 0xf2, 0x04, 0x0a, 0x0e, 0x50, 0x65, 0x65, - 0x72, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1a, 0x0a, 0x08, 0x68, - 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, - 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x67, 0x6f, 0x4f, 0x53, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x67, 0x6f, 0x4f, 0x53, 0x12, 0x16, 0x0a, 0x06, 0x6b, - 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6b, 0x65, 0x72, - 0x6e, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, - 0x6f, 0x72, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, - 0x6f, 0x72, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x4f, 0x53, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x02, 0x4f, 0x53, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x56, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6e, 0x65, 0x74, - 0x62, 0x69, 0x72, 0x64, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x75, - 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, - 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x6b, 0x65, 0x72, - 0x6e, 0x65, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, - 0x1c, 0x0a, 0x09, 0x4f, 0x53, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x4f, 0x53, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x46, 0x0a, - 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, - 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, - 0x65, 0x73, 0x73, 0x52, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, - 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x0f, 0x73, 0x79, 0x73, 0x53, 0x65, 0x72, 0x69, - 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, - 0x73, 0x79, 0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, - 0x26, 0x0a, 0x0e, 0x73, 0x79, 0x73, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, - 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x79, 0x73, 0x50, 0x72, 0x6f, 0x64, - 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x73, 0x79, 0x73, 0x4d, 0x61, - 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, 0x72, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0f, 0x73, 0x79, 0x73, 0x4d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, - 0x72, 0x12, 0x39, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, - 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x52, - 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x05, - 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x05, 0x66, - 0x69, 0x6c, 0x65, 0x73, 0x12, 0x27, 0x0a, 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x11, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x52, 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x22, 0xb4, 0x01, - 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x3f, 0x0a, 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x52, 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, - 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2a, 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, - 0x6b, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x52, 0x06, 0x43, 0x68, - 0x65, 0x63, 0x6b, 0x73, 0x22, 0x79, 0x0a, 0x11, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, - 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x38, 0x0a, 0x09, 0x65, - 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, - 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, - 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0xff, 0x01, 0x0a, 0x0d, 0x4e, 0x65, 0x74, - 0x62, 0x69, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2c, 0x0a, 0x05, 0x73, 0x74, - 0x75, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x52, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x12, 0x35, 0x0a, 0x05, 0x74, 0x75, 0x72, 0x6e, - 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, - 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x74, 0x75, 0x72, 0x6e, 0x73, 0x12, - 0x2e, 0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, - 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x12, - 0x2d, 0x0a, 0x05, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6c, 0x61, - 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x12, 0x2a, - 0x0a, 0x04, 0x66, 0x6c, 0x6f, 0x77, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x6c, 0x6f, 0x77, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x52, 0x04, 0x66, 0x6c, 0x6f, 0x77, 0x22, 0x98, 0x01, 0x0a, 0x0a, 0x48, - 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x3b, 0x0a, 0x08, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, + 0x65, 0x53, 0x53, 0x48, 0x41, 0x75, 0x74, 0x68, 0x12, 0x26, 0x0a, 0x0e, 0x64, 0x69, 0x73, 0x61, + 0x62, 0x6c, 0x65, 0x56, 0x4e, 0x43, 0x41, 0x75, 0x74, 0x68, 0x18, 0x10, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x0e, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x4e, 0x43, 0x41, 0x75, 0x74, 0x68, + 0x12, 0x2a, 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x56, 0x4e, 0x43, 0x41, 0x6c, 0x6c, + 0x6f, 0x77, 0x65, 0x64, 0x18, 0x11, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x56, 0x4e, 0x43, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x22, 0xf2, 0x04, 0x0a, + 0x0e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x12, + 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x67, + 0x6f, 0x4f, 0x53, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x67, 0x6f, 0x4f, 0x53, 0x12, + 0x16, 0x0a, 0x06, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, + 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, + 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x4f, 0x53, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x4f, 0x53, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x65, 0x74, 0x62, 0x69, + 0x72, 0x64, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0e, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, + 0x1c, 0x0a, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, + 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x09, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x56, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x4f, 0x53, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4f, 0x53, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x12, 0x46, 0x0a, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x0f, 0x73, 0x79, 0x73, + 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x0c, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, + 0x62, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x0e, 0x73, 0x79, 0x73, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, + 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x79, 0x73, + 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x73, + 0x79, 0x73, 0x4d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, 0x72, 0x18, 0x0e, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, 0x4d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, + 0x74, 0x75, 0x72, 0x65, 0x72, 0x12, 0x39, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, + 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, + 0x65, 0x6e, 0x74, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, + 0x12, 0x26, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x10, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x6c, + 0x65, 0x52, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x27, 0x0a, 0x05, 0x66, 0x6c, 0x61, 0x67, + 0x73, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x52, 0x05, 0x66, 0x6c, 0x61, 0x67, + 0x73, 0x22, 0xb4, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2a, 0x0a, 0x06, + 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, + 0x52, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x22, 0x79, 0x0a, 0x11, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, + 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x38, 0x0a, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, + 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0xff, 0x01, 0x0a, + 0x0d, 0x4e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2c, + 0x0a, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x22, 0x3b, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, - 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x00, 0x12, 0x07, 0x0a, - 0x03, 0x54, 0x43, 0x50, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x10, 0x02, - 0x12, 0x09, 0x0a, 0x05, 0x48, 0x54, 0x54, 0x50, 0x53, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x44, - 0x54, 0x4c, 0x53, 0x10, 0x04, 0x22, 0x6d, 0x0a, 0x0b, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x72, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x04, 0x75, 0x72, 0x6c, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x74, 0x6f, 0x6b, 0x65, - 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, - 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x26, 0x0a, 0x0e, - 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, 0x61, - 0x74, 0x75, 0x72, 0x65, 0x22, 0xad, 0x02, 0x0a, 0x0a, 0x46, 0x6c, 0x6f, 0x77, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x22, 0x0a, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, - 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x6f, 0x6b, - 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x26, 0x0a, 0x0e, 0x74, 0x6f, 0x6b, - 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, - 0x65, 0x12, 0x35, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x08, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, - 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, - 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x73, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x73, 0x12, 0x2e, - 0x0a, 0x12, 0x65, 0x78, 0x69, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x65, 0x78, 0x69, 0x74, - 0x4e, 0x6f, 0x64, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x24, - 0x0a, 0x0d, 0x64, 0x6e, 0x73, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x64, 0x6e, 0x73, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xa3, 0x01, 0x0a, 0x09, 0x4a, 0x57, 0x54, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x75, - 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x75, - 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x6b, 0x65, 0x79, 0x73, 0x4c, 0x6f, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6b, 0x65, - 0x79, 0x73, 0x4c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, - 0x78, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x41, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x0b, 0x6d, 0x61, 0x78, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x41, 0x67, 0x65, 0x12, 0x1c, 0x0a, 0x09, - 0x61, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x09, 0x61, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x22, 0x7d, 0x0a, 0x13, 0x50, 0x72, - 0x6f, 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x68, - 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, - 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, - 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0xd3, 0x02, 0x0a, 0x0a, 0x50, 0x65, - 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, - 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, - 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x64, 0x6e, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, - 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, - 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x48, 0x0a, - 0x1f, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x65, 0x65, 0x72, 0x44, 0x6e, 0x73, 0x52, - 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1f, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, - 0x65, 0x65, 0x72, 0x44, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, - 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x15, 0x4c, 0x61, 0x7a, 0x79, 0x43, - 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, 0x4c, 0x61, 0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x10, 0x0a, - 0x03, 0x6d, 0x74, 0x75, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x6d, 0x74, 0x75, 0x12, - 0x3e, 0x0a, 0x0a, 0x61, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, - 0x6e, 0x67, 0x73, 0x52, 0x0a, 0x61, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x22, - 0x52, 0x0a, 0x12, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, 0x74, - 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, - 0x22, 0x0a, 0x0c, 0x61, 0x6c, 0x77, 0x61, 0x79, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x61, 0x6c, 0x77, 0x61, 0x79, 0x73, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x22, 0xe8, 0x05, 0x0a, 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, - 0x61, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, - 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, - 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, - 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, - 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, - 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x12, 0x29, 0x0a, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x33, 0x0a, - 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x4e, - 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x12, 0x40, 0x0a, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, - 0x72, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, - 0x65, 0x65, 0x72, 0x73, 0x12, 0x3e, 0x0a, 0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, - 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, - 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, - 0x75, 0x6c, 0x65, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, - 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x09, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x14, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, - 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x4f, 0x0a, 0x13, 0x72, 0x6f, 0x75, 0x74, - 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, - 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, - 0x52, 0x75, 0x6c, 0x65, 0x52, 0x13, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, - 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x1a, 0x72, 0x6f, 0x75, - 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, - 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1a, 0x72, - 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, - 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x44, 0x0a, 0x0f, 0x66, 0x6f, 0x72, - 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x0c, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0f, - 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, - 0x2d, 0x0a, 0x07, 0x73, 0x73, 0x68, 0x41, 0x75, 0x74, 0x68, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x13, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, - 0x48, 0x41, 0x75, 0x74, 0x68, 0x52, 0x07, 0x73, 0x73, 0x68, 0x41, 0x75, 0x74, 0x68, 0x22, 0x82, - 0x02, 0x0a, 0x07, 0x53, 0x53, 0x48, 0x41, 0x75, 0x74, 0x68, 0x12, 0x20, 0x0a, 0x0b, 0x55, 0x73, - 0x65, 0x72, 0x49, 0x44, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0b, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x12, 0x28, 0x0a, 0x0f, - 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x55, 0x73, 0x65, 0x72, 0x73, 0x18, - 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0f, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, - 0x64, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x4a, 0x0a, 0x0d, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, - 0x65, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x41, 0x75, - 0x74, 0x68, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0c, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x55, 0x73, 0x65, - 0x72, 0x73, 0x1a, 0x5f, 0x0a, 0x11, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x55, 0x73, 0x65, - 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x34, 0x0a, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x55, 0x73, 0x65, - 0x72, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, - 0x02, 0x38, 0x01, 0x22, 0x2e, 0x0a, 0x12, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x55, 0x73, - 0x65, 0x72, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x69, 0x6e, 0x64, - 0x65, 0x78, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x07, 0x69, 0x6e, 0x64, 0x65, - 0x78, 0x65, 0x73, 0x22, 0xbb, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, - 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, - 0x62, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, - 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, - 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, - 0x64, 0x49, 0x70, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, - 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, - 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x22, 0x0a, - 0x0c, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x22, 0x7e, 0x0a, 0x09, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1e, - 0x0a, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1c, - 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x33, 0x0a, 0x09, - 0x6a, 0x77, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4a, 0x57, 0x54, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x6a, 0x77, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x22, 0x20, 0x0a, 0x1e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, - 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x22, 0xbf, 0x01, 0x0a, 0x17, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, - 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, - 0x48, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0e, 0x32, 0x2c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, - 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, - 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, - 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, - 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, - 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x16, 0x0a, - 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x0a, 0x0a, 0x06, 0x48, 0x4f, 0x53, - 0x54, 0x45, 0x44, 0x10, 0x00, 0x22, 0x1e, 0x0a, 0x1c, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, - 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x5b, 0x0a, 0x15, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, - 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x42, - 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x22, 0xbc, 0x03, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, - 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, - 0x44, 0x12, 0x26, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, - 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0c, 0x43, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, - 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, - 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x2e, 0x0a, - 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, - 0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x24, 0x0a, - 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, - 0x69, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x55, 0x73, 0x65, - 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x55, - 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, 0x0a, 0x15, 0x41, 0x75, 0x74, - 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, - 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, - 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, - 0x22, 0x0a, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x18, - 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, - 0x52, 0x4c, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x72, - 0x6f, 0x6d, 0x70, 0x74, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x12, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x4c, 0x6f, - 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x46, 0x6c, 0x61, 0x67, - 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x46, 0x6c, 0x61, - 0x67, 0x22, 0x93, 0x02, 0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, - 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x4e, - 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x4e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x20, 0x0a, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, - 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x4e, 0x65, 0x74, 0x77, - 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x4d, - 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4d, 0x65, 0x74, - 0x72, 0x69, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, - 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, - 0x61, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, - 0x61, 0x69, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, - 0x69, 0x6e, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, 0x74, 0x65, - 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, 0x74, - 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x73, 0x6b, 0x69, 0x70, 0x41, 0x75, 0x74, 0x6f, 0x41, 0x70, 0x70, - 0x6c, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x73, 0x6b, 0x69, 0x70, 0x41, 0x75, - 0x74, 0x6f, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x22, 0xde, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x4e, - 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, - 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, - 0x75, 0x70, 0x52, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, - 0x6f, 0x75, 0x70, 0x73, 0x12, 0x38, 0x0a, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, - 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, - 0x65, 0x52, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x12, 0x28, - 0x0a, 0x0d, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x65, 0x72, 0x50, 0x6f, 0x72, 0x74, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x03, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0d, 0x46, 0x6f, 0x72, 0x77, 0x61, - 0x72, 0x64, 0x65, 0x72, 0x50, 0x6f, 0x72, 0x74, 0x22, 0xb8, 0x01, 0x0a, 0x0a, 0x43, 0x75, 0x73, - 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, - 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, - 0x32, 0x0a, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x69, - 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x52, 0x65, 0x63, 0x6f, - 0x72, 0x64, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, - 0x61, 0x69, 0x6e, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x44, - 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x2a, 0x0a, 0x10, 0x4e, 0x6f, 0x6e, 0x41, 0x75, - 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x61, 0x74, 0x69, 0x76, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x10, 0x4e, 0x6f, 0x6e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x61, 0x74, - 0x69, 0x76, 0x65, 0x22, 0x74, 0x0a, 0x0c, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, - 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x43, - 0x6c, 0x61, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x43, 0x6c, 0x61, 0x73, - 0x73, 0x12, 0x10, 0x0a, 0x03, 0x54, 0x54, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, - 0x54, 0x54, 0x4c, 0x12, 0x14, 0x0a, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x22, 0xb3, 0x01, 0x0a, 0x0f, 0x4e, 0x61, - 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38, 0x0a, - 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x12, 0x35, 0x0a, 0x05, + 0x74, 0x75, 0x72, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, + 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x74, 0x75, + 0x72, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x73, 0x69, 0x67, + 0x6e, 0x61, 0x6c, 0x12, 0x2d, 0x0a, 0x05, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x52, 0x65, 0x6c, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x72, 0x65, 0x6c, + 0x61, 0x79, 0x12, 0x2a, 0x0a, 0x04, 0x66, 0x6c, 0x6f, 0x77, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x6c, + 0x6f, 0x77, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x04, 0x66, 0x6c, 0x6f, 0x77, 0x22, 0x98, + 0x01, 0x0a, 0x0a, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x10, 0x0a, + 0x03, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, + 0x3b, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, + 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, + 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x22, 0x3b, 0x0a, 0x08, + 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, + 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x54, + 0x54, 0x50, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x48, 0x54, 0x54, 0x50, 0x53, 0x10, 0x03, 0x12, + 0x08, 0x0a, 0x04, 0x44, 0x54, 0x4c, 0x53, 0x10, 0x04, 0x22, 0x6d, 0x0a, 0x0b, 0x52, 0x65, 0x6c, + 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x72, 0x6c, 0x73, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x75, 0x72, 0x6c, 0x73, 0x12, 0x22, 0x0a, 0x0c, + 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, + 0x12, 0x26, 0x0a, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, + 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, + 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0xad, 0x02, 0x0a, 0x0a, 0x46, 0x6c, 0x6f, + 0x77, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x22, 0x0a, 0x0c, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x26, 0x0a, + 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, + 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x35, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, + 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x18, 0x0a, 0x07, + 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, + 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, + 0x72, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, + 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x65, 0x78, 0x69, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x43, 0x6f, + 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, + 0x65, 0x78, 0x69, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x6e, 0x73, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x64, 0x6e, 0x73, 0x43, 0x6f, + 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xa3, 0x01, 0x0a, 0x09, 0x4a, 0x57, 0x54, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x12, 0x1a, + 0x0a, 0x08, 0x61, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x61, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x6b, 0x65, + 0x79, 0x73, 0x4c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x6b, 0x65, 0x79, 0x73, 0x4c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, + 0x0a, 0x0b, 0x6d, 0x61, 0x78, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x41, 0x67, 0x65, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x0b, 0x6d, 0x61, 0x78, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x41, 0x67, 0x65, + 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x05, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x09, 0x61, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x22, 0x7d, + 0x0a, 0x13, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x52, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, + 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, + 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0xd3, 0x02, + 0x0a, 0x0a, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, + 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, + 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x6e, 0x73, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x64, 0x6e, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, + 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, + 0x6e, 0x12, 0x48, 0x0a, 0x1f, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x65, 0x65, 0x72, + 0x44, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, + 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1f, 0x52, 0x6f, 0x75, 0x74, + 0x69, 0x6e, 0x67, 0x50, 0x65, 0x65, 0x72, 0x44, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, + 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x15, 0x4c, + 0x61, 0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, + 0x62, 0x6c, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, 0x4c, 0x61, 0x7a, 0x79, + 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, + 0x64, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x74, 0x75, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, + 0x6d, 0x74, 0x75, 0x12, 0x3e, 0x0a, 0x0a, 0x61, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, + 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x0a, 0x61, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x22, 0x52, 0x0a, 0x12, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x61, 0x6c, 0x77, 0x61, 0x79, 0x73, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x61, 0x6c, 0x77, 0x61, 0x79, + 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x22, 0x97, 0x06, 0x0a, 0x0a, 0x4e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x36, + 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0b, 0x4e, 0x61, 0x6d, 0x65, - 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, - 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, - 0x79, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x53, - 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, - 0x6c, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, - 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, - 0x48, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e, 0x0a, - 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, - 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4e, - 0x53, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0xa7, 0x02, 0x0a, 0x0c, 0x46, 0x69, - 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x50, 0x65, - 0x65, 0x72, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x65, 0x65, 0x72, - 0x49, 0x50, 0x12, 0x37, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x06, 0x41, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x50, + 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, + 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, + 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, + 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, + 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, + 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, + 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, + 0x73, 0x12, 0x33, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x44, 0x4e, 0x53, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x40, 0x0a, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, + 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, + 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0c, 0x6f, 0x66, 0x66, 0x6c, + 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x3e, 0x0a, 0x0d, 0x46, 0x69, 0x72, 0x65, + 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, + 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, + 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x66, 0x69, 0x72, 0x65, + 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, + 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x4f, 0x0a, 0x13, + 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, + 0x6c, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, + 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x13, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, + 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x3e, 0x0a, + 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, + 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x0b, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, + 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x44, 0x0a, + 0x0f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, + 0x18, 0x0c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, + 0x6c, 0x65, 0x52, 0x0f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, + 0x6c, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x73, 0x73, 0x68, 0x41, 0x75, 0x74, 0x68, 0x18, 0x0d, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x53, 0x53, 0x48, 0x41, 0x75, 0x74, 0x68, 0x52, 0x07, 0x73, 0x73, 0x68, 0x41, 0x75, + 0x74, 0x68, 0x12, 0x2d, 0x0a, 0x07, 0x76, 0x6e, 0x63, 0x41, 0x75, 0x74, 0x68, 0x18, 0x0e, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x56, 0x4e, 0x43, 0x41, 0x75, 0x74, 0x68, 0x52, 0x07, 0x76, 0x6e, 0x63, 0x41, 0x75, 0x74, + 0x68, 0x22, 0x82, 0x02, 0x0a, 0x07, 0x53, 0x53, 0x48, 0x41, 0x75, 0x74, 0x68, 0x12, 0x20, 0x0a, + 0x0b, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0b, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x12, + 0x28, 0x0a, 0x0f, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x55, 0x73, 0x65, + 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0f, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, + 0x69, 0x7a, 0x65, 0x64, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x4a, 0x0a, 0x0d, 0x6d, 0x61, 0x63, + 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x25, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, + 0x48, 0x41, 0x75, 0x74, 0x68, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x55, 0x73, 0x65, + 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0c, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, + 0x55, 0x73, 0x65, 0x72, 0x73, 0x1a, 0x5f, 0x0a, 0x11, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, + 0x55, 0x73, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x34, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, + 0x55, 0x73, 0x65, 0x72, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x2e, 0x0a, 0x12, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, + 0x65, 0x55, 0x73, 0x65, 0x72, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, + 0x69, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x07, 0x69, + 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x22, 0x82, 0x02, 0x0a, 0x07, 0x56, 0x4e, 0x43, 0x41, 0x75, + 0x74, 0x68, 0x12, 0x20, 0x0a, 0x0b, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x43, 0x6c, 0x61, 0x69, + 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x43, + 0x6c, 0x61, 0x69, 0x6d, 0x12, 0x28, 0x0a, 0x0f, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x65, 0x64, 0x55, 0x73, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0f, 0x41, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x4a, + 0x0a, 0x0d, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, + 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x56, 0x4e, 0x43, 0x41, 0x75, 0x74, 0x68, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, + 0x6e, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0c, 0x6d, 0x61, + 0x63, 0x68, 0x69, 0x6e, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x1a, 0x5f, 0x0a, 0x11, 0x4d, 0x61, + 0x63, 0x68, 0x69, 0x6e, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, + 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, + 0x79, 0x12, 0x34, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1e, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x61, + 0x63, 0x68, 0x69, 0x6e, 0x65, 0x55, 0x73, 0x65, 0x72, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xbb, 0x01, 0x0a, 0x10, + 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, + 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x12, 0x33, 0x0a, 0x09, + 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x56, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0xfe, 0x02, 0x0a, 0x09, 0x53, 0x53, + 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, + 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x73, 0x68, + 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, + 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x50, + 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x33, 0x0a, 0x09, 0x6a, 0x77, 0x74, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4a, 0x57, 0x54, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, + 0x09, 0x6a, 0x77, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x28, 0x0a, 0x0f, 0x65, 0x6e, + 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x0f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, + 0x64, 0x69, 0x6e, 0x67, 0x12, 0x32, 0x0a, 0x14, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x69, 0x6e, + 0x67, 0x4d, 0x61, 0x78, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x14, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x4d, 0x61, 0x78, + 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x38, 0x0a, 0x17, 0x72, 0x65, 0x63, 0x6f, + 0x72, 0x64, 0x69, 0x6e, 0x67, 0x4d, 0x61, 0x78, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x53, 0x69, 0x7a, + 0x65, 0x4d, 0x62, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x17, 0x72, 0x65, 0x63, 0x6f, 0x72, + 0x64, 0x69, 0x6e, 0x67, 0x4d, 0x61, 0x78, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x53, 0x69, 0x7a, 0x65, + 0x4d, 0x62, 0x12, 0x2e, 0x0a, 0x12, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x70, 0x75, + 0x74, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, + 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x45, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x64, 0x12, 0x36, 0x0a, 0x16, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x45, + 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x16, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x45, 0x6e, 0x63, + 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x22, 0x20, 0x0a, 0x1e, 0x44, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xbf, 0x01, 0x0a, + 0x17, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x48, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2c, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, + 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x2e, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, + 0x65, 0x72, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x16, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, + 0x65, 0x72, 0x12, 0x0a, 0x0a, 0x06, 0x48, 0x4f, 0x53, 0x54, 0x45, 0x44, 0x10, 0x00, 0x22, 0x1e, + 0x0a, 0x1c, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x5b, + 0x0a, 0x15, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xbc, 0x03, 0x0a, 0x0e, + 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, + 0x0a, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x12, 0x26, 0x0a, 0x0c, 0x43, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x02, 0x18, 0x01, 0x52, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, + 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x41, 0x75, + 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x41, 0x75, + 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, + 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, + 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, + 0x53, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x53, 0x63, 0x6f, + 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x12, 0x34, 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x52, 0x65, 0x64, 0x69, + 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, + 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x12, 0x2e, 0x0a, 0x12, + 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x4c, 0x6f, 0x67, + 0x69, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, + 0x65, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x0a, 0x09, + 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x46, 0x6c, 0x61, 0x67, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0d, 0x52, + 0x09, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x46, 0x6c, 0x61, 0x67, 0x22, 0x93, 0x02, 0x0a, 0x05, 0x52, + 0x6f, 0x75, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x02, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x20, + 0x0a, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, + 0x12, 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x50, 0x65, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x1e, 0x0a, 0x0a, + 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, + 0x4e, 0x65, 0x74, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4e, 0x65, 0x74, + 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x08, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x1c, 0x0a, 0x09, + 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x73, 0x6b, + 0x69, 0x70, 0x41, 0x75, 0x74, 0x6f, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x0d, 0x73, 0x6b, 0x69, 0x70, 0x41, 0x75, 0x74, 0x6f, 0x41, 0x70, 0x70, 0x6c, 0x79, + 0x22, 0xde, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, + 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, + 0x61, 0x62, 0x6c, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x10, 0x4e, 0x61, 0x6d, + 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x38, 0x0a, + 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x0b, 0x43, 0x75, 0x73, 0x74, + 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x0d, 0x46, 0x6f, 0x72, 0x77, 0x61, + 0x72, 0x64, 0x65, 0x72, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x42, 0x02, + 0x18, 0x01, 0x52, 0x0d, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x65, 0x72, 0x50, 0x6f, 0x72, + 0x74, 0x22, 0xb8, 0x01, 0x0a, 0x0a, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, + 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x32, 0x0a, 0x07, 0x52, 0x65, 0x63, 0x6f, + 0x72, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, + 0x6f, 0x72, 0x64, 0x52, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x32, 0x0a, 0x14, + 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x44, 0x69, 0x73, 0x61, + 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x53, 0x65, 0x61, 0x72, + 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, + 0x12, 0x2a, 0x0a, 0x10, 0x4e, 0x6f, 0x6e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x61, + 0x74, 0x69, 0x76, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x4e, 0x6f, 0x6e, 0x41, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x61, 0x74, 0x69, 0x76, 0x65, 0x22, 0x74, 0x0a, 0x0c, + 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, + 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, + 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x54, 0x54, + 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x54, 0x54, 0x4c, 0x12, 0x14, 0x0a, 0x05, + 0x52, 0x44, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x52, 0x44, 0x61, + 0x74, 0x61, 0x22, 0xb3, 0x01, 0x0a, 0x0f, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38, 0x0a, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x52, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, + 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, + 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, + 0x61, 0x69, 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, + 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, + 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x48, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x65, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, + 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x50, 0x6f, + 0x72, 0x74, 0x22, 0xa7, 0x02, 0x0a, 0x0c, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, + 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x12, 0x37, 0x0a, 0x09, 0x44, + 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, + 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, + 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, + 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x30, + 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, + 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, + 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x22, 0x38, 0x0a, 0x0e, + 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x14, + 0x0a, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6e, + 0x65, 0x74, 0x49, 0x50, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x61, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6d, 0x61, 0x63, 0x22, 0x1e, 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, + 0x12, 0x14, 0x0a, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, + 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0d, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x32, 0x0a, 0x05, 0x72, 0x61, 0x6e, + 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x52, + 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x1a, 0x2f, 0x0a, + 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, + 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x42, 0x0f, + 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, + 0x87, 0x03, 0x0a, 0x11, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, + 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, + 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x61, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, + 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, + 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, - 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, - 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x30, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, - 0x6f, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x50, - 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x49, 0x44, 0x22, 0x38, 0x0a, 0x0e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, - 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x12, 0x10, 0x0a, 0x03, 0x6d, - 0x61, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x61, 0x63, 0x22, 0x1e, 0x0a, - 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x22, 0x96, 0x01, - 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x04, 0x70, 0x6f, - 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, - 0x12, 0x32, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, - 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x05, 0x72, - 0x61, 0x6e, 0x67, 0x65, 0x1a, 0x2f, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, - 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x74, - 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, - 0x52, 0x03, 0x65, 0x6e, 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x6c, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x87, 0x03, 0x0a, 0x11, 0x52, 0x6f, 0x75, 0x74, 0x65, - 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, - 0x12, 0x2e, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, - 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x30, 0x0a, 0x08, 0x70, 0x6f, 0x72, 0x74, - 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, - 0x52, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x73, - 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, - 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, - 0x69, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, - 0x6e, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, - 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x63, 0x75, 0x73, 0x74, - 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x50, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, - 0x44, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x44, - 0x22, 0xf2, 0x01, 0x0a, 0x0e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, - 0x75, 0x6c, 0x65, 0x12, 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, - 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x3e, 0x0a, 0x0f, 0x64, 0x65, 0x73, - 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x74, 0x72, 0x61, - 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, - 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x3c, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, - 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, - 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, - 0x64, 0x50, 0x6f, 0x72, 0x74, 0x22, 0x8b, 0x02, 0x0a, 0x14, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, - 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, - 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x70, 0x6f, - 0x72, 0x74, 0x12, 0x36, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, - 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x10, 0x0a, 0x03, 0x70, 0x69, - 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x70, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, - 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x75, 0x73, 0x65, 0x72, - 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x75, - 0x73, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, - 0x61, 0x69, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, - 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x50, 0x72, 0x65, 0x66, - 0x69, 0x78, 0x12, 0x1f, 0x0a, 0x0b, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x5f, 0x70, 0x6f, 0x72, - 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x50, - 0x6f, 0x72, 0x74, 0x22, 0xa1, 0x01, 0x0a, 0x15, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, - 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, - 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x55, 0x72, - 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x2c, 0x0a, 0x12, 0x70, 0x6f, 0x72, - 0x74, 0x5f, 0x61, 0x75, 0x74, 0x6f, 0x5f, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x70, 0x6f, 0x72, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x41, - 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x22, 0x2c, 0x0a, 0x12, 0x52, 0x65, 0x6e, 0x65, 0x77, - 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, - 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, - 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x22, 0x15, 0x0a, 0x13, 0x52, 0x65, 0x6e, 0x65, 0x77, 0x45, 0x78, - 0x70, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2b, 0x0a, 0x11, - 0x53, 0x74, 0x6f, 0x70, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x22, 0x14, 0x0a, 0x12, 0x53, 0x74, 0x6f, - 0x70, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2a, - 0x3a, 0x0a, 0x09, 0x4a, 0x6f, 0x62, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x0e, - 0x75, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x10, 0x00, - 0x12, 0x0d, 0x0a, 0x09, 0x73, 0x75, 0x63, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x10, 0x01, 0x12, - 0x0a, 0x0a, 0x06, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x10, 0x02, 0x2a, 0x4c, 0x0a, 0x0c, 0x52, - 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, - 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, - 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, - 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, 0x10, 0x04, 0x12, 0x0a, 0x0a, - 0x06, 0x43, 0x55, 0x53, 0x54, 0x4f, 0x4d, 0x10, 0x05, 0x2a, 0x20, 0x0a, 0x0d, 0x52, 0x75, 0x6c, - 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, 0x49, 0x4e, - 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x2a, 0x22, 0x0a, 0x0a, 0x52, - 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, - 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x2a, - 0x63, 0x0a, 0x0e, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, - 0x6c, 0x12, 0x0f, 0x0a, 0x0b, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x48, 0x54, 0x54, 0x50, - 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x48, 0x54, 0x54, - 0x50, 0x53, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x54, - 0x43, 0x50, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x55, - 0x44, 0x50, 0x10, 0x03, 0x12, 0x0e, 0x0a, 0x0a, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x54, - 0x4c, 0x53, 0x10, 0x04, 0x32, 0xfd, 0x06, 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, - 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, - 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, - 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, + 0x6c, 0x12, 0x30, 0x0a, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, + 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, + 0x63, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x63, + 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, + 0x63, 0x6f, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x12, + 0x18, 0x0a, 0x07, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x44, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x44, 0x22, 0xf2, 0x01, 0x0a, 0x0e, 0x46, 0x6f, + 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x34, 0x0a, 0x08, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, + 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, + 0x6f, 0x6c, 0x12, 0x3e, 0x0a, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, + 0x6f, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, + 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, + 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, 0x74, + 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, + 0x12, 0x3c, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, + 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0e, + 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x22, 0x8b, + 0x02, 0x0a, 0x14, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x36, 0x0a, 0x08, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x78, 0x70, 0x6f, 0x73, + 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x63, 0x6f, 0x6c, 0x12, 0x10, 0x0a, 0x03, 0x70, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x70, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, + 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, + 0x70, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x6e, 0x61, + 0x6d, 0x65, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x1f, 0x0a, 0x0b, 0x6c, + 0x69, 0x73, 0x74, 0x65, 0x6e, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x0a, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x22, 0xa1, 0x01, 0x0a, + 0x15, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, + 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, + 0x69, 0x6e, 0x12, 0x2c, 0x0a, 0x12, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x61, 0x75, 0x74, 0x6f, 0x5f, + 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, + 0x70, 0x6f, 0x72, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, + 0x22, 0x2c, 0x0a, 0x12, 0x52, 0x65, 0x6e, 0x65, 0x77, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x22, 0x15, + 0x0a, 0x13, 0x52, 0x65, 0x6e, 0x65, 0x77, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2b, 0x0a, 0x11, 0x53, 0x74, 0x6f, 0x70, 0x45, 0x78, 0x70, + 0x6f, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, + 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, + 0x69, 0x6e, 0x22, 0x14, 0x0a, 0x12, 0x53, 0x74, 0x6f, 0x70, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2a, 0x3a, 0x0a, 0x09, 0x4a, 0x6f, 0x62, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x0e, 0x75, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, + 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x73, 0x75, 0x63, + 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x66, 0x61, 0x69, 0x6c, + 0x65, 0x64, 0x10, 0x02, 0x2a, 0x4c, 0x0a, 0x0c, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, + 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, + 0x00, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, + 0x50, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, + 0x49, 0x43, 0x4d, 0x50, 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55, 0x53, 0x54, 0x4f, 0x4d, + 0x10, 0x05, 0x2a, 0x20, 0x0a, 0x0d, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4f, + 0x55, 0x54, 0x10, 0x01, 0x2a, 0x22, 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08, + 0x0a, 0x04, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x2a, 0x63, 0x0a, 0x0e, 0x45, 0x78, 0x70, 0x6f, + 0x73, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0f, 0x0a, 0x0b, 0x45, 0x58, + 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x48, 0x54, 0x54, 0x50, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x45, + 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x48, 0x54, 0x54, 0x50, 0x53, 0x10, 0x01, 0x12, 0x0e, 0x0a, + 0x0a, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x0e, 0x0a, + 0x0a, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x0e, 0x0a, + 0x0a, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x54, 0x4c, 0x53, 0x10, 0x04, 0x32, 0xfd, 0x06, + 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, + 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, + 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, + 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, + 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, + 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, + 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, + 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, + 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, - 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, - 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, - 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, - 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, - 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, - 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, - 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, - 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, - 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x08, 0x53, 0x79, 0x6e, 0x63, - 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x06, 0x4c, 0x6f, 0x67, 0x6f, 0x75, - 0x74, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, - 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, - 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x22, 0x00, 0x12, 0x47, 0x0a, 0x03, 0x4a, 0x6f, 0x62, 0x12, 0x1c, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, - 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, - 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x12, 0x4c, 0x0a, - 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x12, 0x1c, 0x2e, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, + 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, + 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, + 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, + 0x00, 0x12, 0x3d, 0x0a, 0x08, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, - 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, - 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0b, 0x52, - 0x65, 0x6e, 0x65, 0x77, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, + 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, + 0x12, 0x3b, 0x0a, 0x06, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, - 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x4a, 0x0a, 0x0a, 0x53, 0x74, 0x6f, 0x70, + 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x47, 0x0a, + 0x03, 0x4a, 0x6f, 0x62, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x12, 0x4c, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x67, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0b, 0x52, 0x65, 0x6e, 0x65, 0x77, 0x45, 0x78, 0x70, + 0x6f, 0x73, 0x65, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, + 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, + 0x00, 0x12, 0x4a, 0x0a, 0x0a, 0x53, 0x74, 0x6f, 0x70, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x12, + 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, + 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, + 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, + 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -5146,7 +5324,7 @@ func file_management_proto_rawDescGZIP() []byte { } var file_management_proto_enumTypes = make([]protoimpl.EnumInfo, 7) -var file_management_proto_msgTypes = make([]protoimpl.MessageInfo, 55) +var file_management_proto_msgTypes = make([]protoimpl.MessageInfo, 57) var file_management_proto_goTypes = []interface{}{ (JobStatus)(0), // 0: management.JobStatus (RuleProtocol)(0), // 1: management.RuleProtocol @@ -5183,35 +5361,37 @@ var file_management_proto_goTypes = []interface{}{ (*NetworkMap)(nil), // 32: management.NetworkMap (*SSHAuth)(nil), // 33: management.SSHAuth (*MachineUserIndexes)(nil), // 34: management.MachineUserIndexes - (*RemotePeerConfig)(nil), // 35: management.RemotePeerConfig - (*SSHConfig)(nil), // 36: management.SSHConfig - (*DeviceAuthorizationFlowRequest)(nil), // 37: management.DeviceAuthorizationFlowRequest - (*DeviceAuthorizationFlow)(nil), // 38: management.DeviceAuthorizationFlow - (*PKCEAuthorizationFlowRequest)(nil), // 39: management.PKCEAuthorizationFlowRequest - (*PKCEAuthorizationFlow)(nil), // 40: management.PKCEAuthorizationFlow - (*ProviderConfig)(nil), // 41: management.ProviderConfig - (*Route)(nil), // 42: management.Route - (*DNSConfig)(nil), // 43: management.DNSConfig - (*CustomZone)(nil), // 44: management.CustomZone - (*SimpleRecord)(nil), // 45: management.SimpleRecord - (*NameServerGroup)(nil), // 46: management.NameServerGroup - (*NameServer)(nil), // 47: management.NameServer - (*FirewallRule)(nil), // 48: management.FirewallRule - (*NetworkAddress)(nil), // 49: management.NetworkAddress - (*Checks)(nil), // 50: management.Checks - (*PortInfo)(nil), // 51: management.PortInfo - (*RouteFirewallRule)(nil), // 52: management.RouteFirewallRule - (*ForwardingRule)(nil), // 53: management.ForwardingRule - (*ExposeServiceRequest)(nil), // 54: management.ExposeServiceRequest - (*ExposeServiceResponse)(nil), // 55: management.ExposeServiceResponse - (*RenewExposeRequest)(nil), // 56: management.RenewExposeRequest - (*RenewExposeResponse)(nil), // 57: management.RenewExposeResponse - (*StopExposeRequest)(nil), // 58: management.StopExposeRequest - (*StopExposeResponse)(nil), // 59: management.StopExposeResponse - nil, // 60: management.SSHAuth.MachineUsersEntry - (*PortInfo_Range)(nil), // 61: management.PortInfo.Range - (*timestamppb.Timestamp)(nil), // 62: google.protobuf.Timestamp - (*durationpb.Duration)(nil), // 63: google.protobuf.Duration + (*VNCAuth)(nil), // 35: management.VNCAuth + (*RemotePeerConfig)(nil), // 36: management.RemotePeerConfig + (*SSHConfig)(nil), // 37: management.SSHConfig + (*DeviceAuthorizationFlowRequest)(nil), // 38: management.DeviceAuthorizationFlowRequest + (*DeviceAuthorizationFlow)(nil), // 39: management.DeviceAuthorizationFlow + (*PKCEAuthorizationFlowRequest)(nil), // 40: management.PKCEAuthorizationFlowRequest + (*PKCEAuthorizationFlow)(nil), // 41: management.PKCEAuthorizationFlow + (*ProviderConfig)(nil), // 42: management.ProviderConfig + (*Route)(nil), // 43: management.Route + (*DNSConfig)(nil), // 44: management.DNSConfig + (*CustomZone)(nil), // 45: management.CustomZone + (*SimpleRecord)(nil), // 46: management.SimpleRecord + (*NameServerGroup)(nil), // 47: management.NameServerGroup + (*NameServer)(nil), // 48: management.NameServer + (*FirewallRule)(nil), // 49: management.FirewallRule + (*NetworkAddress)(nil), // 50: management.NetworkAddress + (*Checks)(nil), // 51: management.Checks + (*PortInfo)(nil), // 52: management.PortInfo + (*RouteFirewallRule)(nil), // 53: management.RouteFirewallRule + (*ForwardingRule)(nil), // 54: management.ForwardingRule + (*ExposeServiceRequest)(nil), // 55: management.ExposeServiceRequest + (*ExposeServiceResponse)(nil), // 56: management.ExposeServiceResponse + (*RenewExposeRequest)(nil), // 57: management.RenewExposeRequest + (*RenewExposeResponse)(nil), // 58: management.RenewExposeResponse + (*StopExposeRequest)(nil), // 59: management.StopExposeRequest + (*StopExposeResponse)(nil), // 60: management.StopExposeResponse + nil, // 61: management.SSHAuth.MachineUsersEntry + nil, // 62: management.VNCAuth.MachineUsersEntry + (*PortInfo_Range)(nil), // 63: management.PortInfo.Range + (*timestamppb.Timestamp)(nil), // 64: google.protobuf.Timestamp + (*durationpb.Duration)(nil), // 65: google.protobuf.Duration } var file_management_proto_depIdxs = []int32{ 10, // 0: management.JobRequest.bundle:type_name -> management.BundleParameters @@ -5220,91 +5400,94 @@ var file_management_proto_depIdxs = []int32{ 20, // 3: management.SyncRequest.meta:type_name -> management.PeerSystemMeta 24, // 4: management.SyncResponse.netbirdConfig:type_name -> management.NetbirdConfig 30, // 5: management.SyncResponse.peerConfig:type_name -> management.PeerConfig - 35, // 6: management.SyncResponse.remotePeers:type_name -> management.RemotePeerConfig + 36, // 6: management.SyncResponse.remotePeers:type_name -> management.RemotePeerConfig 32, // 7: management.SyncResponse.NetworkMap:type_name -> management.NetworkMap - 50, // 8: management.SyncResponse.Checks:type_name -> management.Checks + 51, // 8: management.SyncResponse.Checks:type_name -> management.Checks 20, // 9: management.SyncMetaRequest.meta:type_name -> management.PeerSystemMeta 20, // 10: management.LoginRequest.meta:type_name -> management.PeerSystemMeta 16, // 11: management.LoginRequest.peerKeys:type_name -> management.PeerKeys - 49, // 12: management.PeerSystemMeta.networkAddresses:type_name -> management.NetworkAddress + 50, // 12: management.PeerSystemMeta.networkAddresses:type_name -> management.NetworkAddress 17, // 13: management.PeerSystemMeta.environment:type_name -> management.Environment 18, // 14: management.PeerSystemMeta.files:type_name -> management.File 19, // 15: management.PeerSystemMeta.flags:type_name -> management.Flags 24, // 16: management.LoginResponse.netbirdConfig:type_name -> management.NetbirdConfig 30, // 17: management.LoginResponse.peerConfig:type_name -> management.PeerConfig - 50, // 18: management.LoginResponse.Checks:type_name -> management.Checks - 62, // 19: management.ServerKeyResponse.expiresAt:type_name -> google.protobuf.Timestamp + 51, // 18: management.LoginResponse.Checks:type_name -> management.Checks + 64, // 19: management.ServerKeyResponse.expiresAt:type_name -> google.protobuf.Timestamp 25, // 20: management.NetbirdConfig.stuns:type_name -> management.HostConfig 29, // 21: management.NetbirdConfig.turns:type_name -> management.ProtectedHostConfig 25, // 22: management.NetbirdConfig.signal:type_name -> management.HostConfig 26, // 23: management.NetbirdConfig.relay:type_name -> management.RelayConfig 27, // 24: management.NetbirdConfig.flow:type_name -> management.FlowConfig 5, // 25: management.HostConfig.protocol:type_name -> management.HostConfig.Protocol - 63, // 26: management.FlowConfig.interval:type_name -> google.protobuf.Duration + 65, // 26: management.FlowConfig.interval:type_name -> google.protobuf.Duration 25, // 27: management.ProtectedHostConfig.hostConfig:type_name -> management.HostConfig - 36, // 28: management.PeerConfig.sshConfig:type_name -> management.SSHConfig + 37, // 28: management.PeerConfig.sshConfig:type_name -> management.SSHConfig 31, // 29: management.PeerConfig.autoUpdate:type_name -> management.AutoUpdateSettings 30, // 30: management.NetworkMap.peerConfig:type_name -> management.PeerConfig - 35, // 31: management.NetworkMap.remotePeers:type_name -> management.RemotePeerConfig - 42, // 32: management.NetworkMap.Routes:type_name -> management.Route - 43, // 33: management.NetworkMap.DNSConfig:type_name -> management.DNSConfig - 35, // 34: management.NetworkMap.offlinePeers:type_name -> management.RemotePeerConfig - 48, // 35: management.NetworkMap.FirewallRules:type_name -> management.FirewallRule - 52, // 36: management.NetworkMap.routesFirewallRules:type_name -> management.RouteFirewallRule - 53, // 37: management.NetworkMap.forwardingRules:type_name -> management.ForwardingRule + 36, // 31: management.NetworkMap.remotePeers:type_name -> management.RemotePeerConfig + 43, // 32: management.NetworkMap.Routes:type_name -> management.Route + 44, // 33: management.NetworkMap.DNSConfig:type_name -> management.DNSConfig + 36, // 34: management.NetworkMap.offlinePeers:type_name -> management.RemotePeerConfig + 49, // 35: management.NetworkMap.FirewallRules:type_name -> management.FirewallRule + 53, // 36: management.NetworkMap.routesFirewallRules:type_name -> management.RouteFirewallRule + 54, // 37: management.NetworkMap.forwardingRules:type_name -> management.ForwardingRule 33, // 38: management.NetworkMap.sshAuth:type_name -> management.SSHAuth - 60, // 39: management.SSHAuth.machine_users:type_name -> management.SSHAuth.MachineUsersEntry - 36, // 40: management.RemotePeerConfig.sshConfig:type_name -> management.SSHConfig - 28, // 41: management.SSHConfig.jwtConfig:type_name -> management.JWTConfig - 6, // 42: management.DeviceAuthorizationFlow.Provider:type_name -> management.DeviceAuthorizationFlow.provider - 41, // 43: management.DeviceAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig - 41, // 44: management.PKCEAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig - 46, // 45: management.DNSConfig.NameServerGroups:type_name -> management.NameServerGroup - 44, // 46: management.DNSConfig.CustomZones:type_name -> management.CustomZone - 45, // 47: management.CustomZone.Records:type_name -> management.SimpleRecord - 47, // 48: management.NameServerGroup.NameServers:type_name -> management.NameServer - 2, // 49: management.FirewallRule.Direction:type_name -> management.RuleDirection - 3, // 50: management.FirewallRule.Action:type_name -> management.RuleAction - 1, // 51: management.FirewallRule.Protocol:type_name -> management.RuleProtocol - 51, // 52: management.FirewallRule.PortInfo:type_name -> management.PortInfo - 61, // 53: management.PortInfo.range:type_name -> management.PortInfo.Range - 3, // 54: management.RouteFirewallRule.action:type_name -> management.RuleAction - 1, // 55: management.RouteFirewallRule.protocol:type_name -> management.RuleProtocol - 51, // 56: management.RouteFirewallRule.portInfo:type_name -> management.PortInfo - 1, // 57: management.ForwardingRule.protocol:type_name -> management.RuleProtocol - 51, // 58: management.ForwardingRule.destinationPort:type_name -> management.PortInfo - 51, // 59: management.ForwardingRule.translatedPort:type_name -> management.PortInfo - 4, // 60: management.ExposeServiceRequest.protocol:type_name -> management.ExposeProtocol - 34, // 61: management.SSHAuth.MachineUsersEntry.value:type_name -> management.MachineUserIndexes - 7, // 62: management.ManagementService.Login:input_type -> management.EncryptedMessage - 7, // 63: management.ManagementService.Sync:input_type -> management.EncryptedMessage - 23, // 64: management.ManagementService.GetServerKey:input_type -> management.Empty - 23, // 65: management.ManagementService.isHealthy:input_type -> management.Empty - 7, // 66: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage - 7, // 67: management.ManagementService.GetPKCEAuthorizationFlow:input_type -> management.EncryptedMessage - 7, // 68: management.ManagementService.SyncMeta:input_type -> management.EncryptedMessage - 7, // 69: management.ManagementService.Logout:input_type -> management.EncryptedMessage - 7, // 70: management.ManagementService.Job:input_type -> management.EncryptedMessage - 7, // 71: management.ManagementService.CreateExpose:input_type -> management.EncryptedMessage - 7, // 72: management.ManagementService.RenewExpose:input_type -> management.EncryptedMessage - 7, // 73: management.ManagementService.StopExpose:input_type -> management.EncryptedMessage - 7, // 74: management.ManagementService.Login:output_type -> management.EncryptedMessage - 7, // 75: management.ManagementService.Sync:output_type -> management.EncryptedMessage - 22, // 76: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse - 23, // 77: management.ManagementService.isHealthy:output_type -> management.Empty - 7, // 78: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage - 7, // 79: management.ManagementService.GetPKCEAuthorizationFlow:output_type -> management.EncryptedMessage - 23, // 80: management.ManagementService.SyncMeta:output_type -> management.Empty - 23, // 81: management.ManagementService.Logout:output_type -> management.Empty - 7, // 82: management.ManagementService.Job:output_type -> management.EncryptedMessage - 7, // 83: management.ManagementService.CreateExpose:output_type -> management.EncryptedMessage - 7, // 84: management.ManagementService.RenewExpose:output_type -> management.EncryptedMessage - 7, // 85: management.ManagementService.StopExpose:output_type -> management.EncryptedMessage - 74, // [74:86] is the sub-list for method output_type - 62, // [62:74] is the sub-list for method input_type - 62, // [62:62] is the sub-list for extension type_name - 62, // [62:62] is the sub-list for extension extendee - 0, // [0:62] is the sub-list for field type_name + 35, // 39: management.NetworkMap.vncAuth:type_name -> management.VNCAuth + 61, // 40: management.SSHAuth.machine_users:type_name -> management.SSHAuth.MachineUsersEntry + 62, // 41: management.VNCAuth.machine_users:type_name -> management.VNCAuth.MachineUsersEntry + 37, // 42: management.RemotePeerConfig.sshConfig:type_name -> management.SSHConfig + 28, // 43: management.SSHConfig.jwtConfig:type_name -> management.JWTConfig + 6, // 44: management.DeviceAuthorizationFlow.Provider:type_name -> management.DeviceAuthorizationFlow.provider + 42, // 45: management.DeviceAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig + 42, // 46: management.PKCEAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig + 47, // 47: management.DNSConfig.NameServerGroups:type_name -> management.NameServerGroup + 45, // 48: management.DNSConfig.CustomZones:type_name -> management.CustomZone + 46, // 49: management.CustomZone.Records:type_name -> management.SimpleRecord + 48, // 50: management.NameServerGroup.NameServers:type_name -> management.NameServer + 2, // 51: management.FirewallRule.Direction:type_name -> management.RuleDirection + 3, // 52: management.FirewallRule.Action:type_name -> management.RuleAction + 1, // 53: management.FirewallRule.Protocol:type_name -> management.RuleProtocol + 52, // 54: management.FirewallRule.PortInfo:type_name -> management.PortInfo + 63, // 55: management.PortInfo.range:type_name -> management.PortInfo.Range + 3, // 56: management.RouteFirewallRule.action:type_name -> management.RuleAction + 1, // 57: management.RouteFirewallRule.protocol:type_name -> management.RuleProtocol + 52, // 58: management.RouteFirewallRule.portInfo:type_name -> management.PortInfo + 1, // 59: management.ForwardingRule.protocol:type_name -> management.RuleProtocol + 52, // 60: management.ForwardingRule.destinationPort:type_name -> management.PortInfo + 52, // 61: management.ForwardingRule.translatedPort:type_name -> management.PortInfo + 4, // 62: management.ExposeServiceRequest.protocol:type_name -> management.ExposeProtocol + 34, // 63: management.SSHAuth.MachineUsersEntry.value:type_name -> management.MachineUserIndexes + 34, // 64: management.VNCAuth.MachineUsersEntry.value:type_name -> management.MachineUserIndexes + 7, // 65: management.ManagementService.Login:input_type -> management.EncryptedMessage + 7, // 66: management.ManagementService.Sync:input_type -> management.EncryptedMessage + 23, // 67: management.ManagementService.GetServerKey:input_type -> management.Empty + 23, // 68: management.ManagementService.isHealthy:input_type -> management.Empty + 7, // 69: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage + 7, // 70: management.ManagementService.GetPKCEAuthorizationFlow:input_type -> management.EncryptedMessage + 7, // 71: management.ManagementService.SyncMeta:input_type -> management.EncryptedMessage + 7, // 72: management.ManagementService.Logout:input_type -> management.EncryptedMessage + 7, // 73: management.ManagementService.Job:input_type -> management.EncryptedMessage + 7, // 74: management.ManagementService.CreateExpose:input_type -> management.EncryptedMessage + 7, // 75: management.ManagementService.RenewExpose:input_type -> management.EncryptedMessage + 7, // 76: management.ManagementService.StopExpose:input_type -> management.EncryptedMessage + 7, // 77: management.ManagementService.Login:output_type -> management.EncryptedMessage + 7, // 78: management.ManagementService.Sync:output_type -> management.EncryptedMessage + 22, // 79: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse + 23, // 80: management.ManagementService.isHealthy:output_type -> management.Empty + 7, // 81: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage + 7, // 82: management.ManagementService.GetPKCEAuthorizationFlow:output_type -> management.EncryptedMessage + 23, // 83: management.ManagementService.SyncMeta:output_type -> management.Empty + 23, // 84: management.ManagementService.Logout:output_type -> management.Empty + 7, // 85: management.ManagementService.Job:output_type -> management.EncryptedMessage + 7, // 86: management.ManagementService.CreateExpose:output_type -> management.EncryptedMessage + 7, // 87: management.ManagementService.RenewExpose:output_type -> management.EncryptedMessage + 7, // 88: management.ManagementService.StopExpose:output_type -> management.EncryptedMessage + 77, // [77:89] is the sub-list for method output_type + 65, // [65:77] is the sub-list for method input_type + 65, // [65:65] is the sub-list for extension type_name + 65, // [65:65] is the sub-list for extension extendee + 0, // [0:65] is the sub-list for field type_name } func init() { file_management_proto_init() } @@ -5650,7 +5833,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RemotePeerConfig); i { + switch v := v.(*VNCAuth); i { case 0: return &v.state case 1: @@ -5662,7 +5845,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SSHConfig); i { + switch v := v.(*RemotePeerConfig); i { case 0: return &v.state case 1: @@ -5674,7 +5857,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeviceAuthorizationFlowRequest); i { + switch v := v.(*SSHConfig); i { case 0: return &v.state case 1: @@ -5686,7 +5869,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeviceAuthorizationFlow); i { + switch v := v.(*DeviceAuthorizationFlowRequest); i { case 0: return &v.state case 1: @@ -5698,7 +5881,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PKCEAuthorizationFlowRequest); i { + switch v := v.(*DeviceAuthorizationFlow); i { case 0: return &v.state case 1: @@ -5710,7 +5893,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PKCEAuthorizationFlow); i { + switch v := v.(*PKCEAuthorizationFlowRequest); i { case 0: return &v.state case 1: @@ -5722,7 +5905,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[34].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ProviderConfig); i { + switch v := v.(*PKCEAuthorizationFlow); i { case 0: return &v.state case 1: @@ -5734,7 +5917,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Route); i { + switch v := v.(*ProviderConfig); i { case 0: return &v.state case 1: @@ -5746,7 +5929,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[36].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DNSConfig); i { + switch v := v.(*Route); i { case 0: return &v.state case 1: @@ -5758,7 +5941,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[37].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CustomZone); i { + switch v := v.(*DNSConfig); i { case 0: return &v.state case 1: @@ -5770,7 +5953,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[38].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SimpleRecord); i { + switch v := v.(*CustomZone); i { case 0: return &v.state case 1: @@ -5782,7 +5965,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[39].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NameServerGroup); i { + switch v := v.(*SimpleRecord); i { case 0: return &v.state case 1: @@ -5794,7 +5977,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[40].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NameServer); i { + switch v := v.(*NameServerGroup); i { case 0: return &v.state case 1: @@ -5806,7 +5989,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[41].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FirewallRule); i { + switch v := v.(*NameServer); i { case 0: return &v.state case 1: @@ -5818,7 +6001,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[42].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NetworkAddress); i { + switch v := v.(*FirewallRule); i { case 0: return &v.state case 1: @@ -5830,7 +6013,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[43].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Checks); i { + switch v := v.(*NetworkAddress); i { case 0: return &v.state case 1: @@ -5842,7 +6025,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[44].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PortInfo); i { + switch v := v.(*Checks); i { case 0: return &v.state case 1: @@ -5854,7 +6037,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[45].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RouteFirewallRule); i { + switch v := v.(*PortInfo); i { case 0: return &v.state case 1: @@ -5866,7 +6049,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[46].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ForwardingRule); i { + switch v := v.(*RouteFirewallRule); i { case 0: return &v.state case 1: @@ -5878,7 +6061,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[47].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ExposeServiceRequest); i { + switch v := v.(*ForwardingRule); i { case 0: return &v.state case 1: @@ -5890,7 +6073,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[48].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ExposeServiceResponse); i { + switch v := v.(*ExposeServiceRequest); i { case 0: return &v.state case 1: @@ -5902,7 +6085,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[49].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RenewExposeRequest); i { + switch v := v.(*ExposeServiceResponse); i { case 0: return &v.state case 1: @@ -5914,7 +6097,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[50].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RenewExposeResponse); i { + switch v := v.(*RenewExposeRequest); i { case 0: return &v.state case 1: @@ -5926,7 +6109,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[51].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*StopExposeRequest); i { + switch v := v.(*RenewExposeResponse); i { case 0: return &v.state case 1: @@ -5938,6 +6121,18 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[52].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*StopExposeRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_management_proto_msgTypes[53].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*StopExposeResponse); i { case 0: return &v.state @@ -5949,7 +6144,7 @@ func file_management_proto_init() { return nil } } - file_management_proto_msgTypes[54].Exporter = func(v interface{}, i int) interface{} { + file_management_proto_msgTypes[56].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*PortInfo_Range); i { case 0: return &v.state @@ -5968,7 +6163,7 @@ func file_management_proto_init() { file_management_proto_msgTypes[2].OneofWrappers = []interface{}{ (*JobResponse_Bundle)(nil), } - file_management_proto_msgTypes[44].OneofWrappers = []interface{}{ + file_management_proto_msgTypes[45].OneofWrappers = []interface{}{ (*PortInfo_Port)(nil), (*PortInfo_Range_)(nil), } @@ -5978,7 +6173,7 @@ func file_management_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_management_proto_rawDesc, NumEnums: 7, - NumMessages: 55, + NumMessages: 57, NumExtensions: 0, NumServices: 1, }, diff --git a/shared/management/proto/management.proto b/shared/management/proto/management.proto index 70a530679..e4a2e2355 100644 --- a/shared/management/proto/management.proto +++ b/shared/management/proto/management.proto @@ -200,6 +200,8 @@ message Flags { bool enableSSHLocalPortForwarding = 13; bool enableSSHRemotePortForwarding = 14; bool disableSSHAuth = 15; + bool disableVNCAuth = 16; + bool serverVNCAllowed = 17; } // PeerSystemMeta is machine meta data like OS and version. @@ -387,6 +389,9 @@ message NetworkMap { // SSHAuth represents SSH authorization configuration SSHAuth sshAuth = 13; + + // VNCAuth represents VNC authorization configuration + VNCAuth vncAuth = 14; } message SSHAuth { @@ -404,6 +409,20 @@ message MachineUserIndexes { repeated uint32 indexes = 1; } +// VNCAuth represents VNC authorization configuration for a peer. +message VNCAuth { + // UserIDClaim is the JWT claim to be used to get the users ID + string UserIDClaim = 1; + + // AuthorizedUsers is a list of hashed user IDs authorized to access this peer via VNC + repeated bytes AuthorizedUsers = 2; + + // MachineUsers maps OS user names to their corresponding indexes in the AuthorizedUsers list. + // Used in session mode to determine which OS user to create the virtual session as. + // The wildcard "*" allows any OS user. + map machine_users = 3; +} + // RemotePeerConfig represents a configuration of a remote peer. // The properties are used to configure WireGuard Peers sections message RemotePeerConfig { @@ -433,6 +452,17 @@ message SSHConfig { bytes sshPubKey = 2; JWTConfig jwtConfig = 3; + + // Session recording settings (shared for SSH and VNC) + bool enableRecording = 4; + int32 recordingMaxSessions = 5; + int64 recordingMaxTotalSizeMb = 6; + bool recordInputEnabled = 7; + + // Recording encryption: DER-encoded public key for hybrid AES-GCM encryption. + // If set, recordings are encrypted with a per-session AES-256 key, which is + // itself encrypted with this public key and stored in the recording header. + bytes recordingEncryptionKey = 8; } // DeviceAuthorizationFlowRequest empty struct for future expansion