diff --git a/client/cmd/up.go b/client/cmd/up.go index 167b418fb..8ffc7c2f7 100644 --- a/client/cmd/up.go +++ b/client/cmd/up.go @@ -364,6 +364,9 @@ func setupSetConfigReq(customDNSAddressConverted []byte, cmd *cobra.Command, pro if cmd.Flag(serverVNCAllowedFlag).Changed { req.ServerVNCAllowed = &serverVNCAllowed } + if cmd.Flag(disableVNCApprovalFlag).Changed { + req.DisableVNCApproval = &disableVNCApproval + } if cmd.Flag(enableSSHRootFlag).Changed { req.EnableSSHRoot = &enableSSHRoot } @@ -473,6 +476,9 @@ func setupConfig(customDNSAddressConverted []byte, cmd *cobra.Command, configFil if cmd.Flag(serverVNCAllowedFlag).Changed { ic.ServerVNCAllowed = &serverVNCAllowed } + if cmd.Flag(disableVNCApprovalFlag).Changed { + ic.DisableVNCApproval = &disableVNCApproval + } if cmd.Flag(enableSSHRootFlag).Changed { ic.EnableSSHRoot = &enableSSHRoot @@ -604,6 +610,9 @@ func setupLoginRequest(providedSetupKey string, customDNSAddressConverted []byte if cmd.Flag(serverVNCAllowedFlag).Changed { loginRequest.ServerVNCAllowed = &serverVNCAllowed } + if cmd.Flag(disableVNCApprovalFlag).Changed { + loginRequest.DisableVNCApproval = &disableVNCApproval + } if cmd.Flag(enableSSHRootFlag).Changed { loginRequest.EnableSSHRoot = &enableSSHRoot diff --git a/client/cmd/vnc_flags.go b/client/cmd/vnc_flags.go index cfcbaeab1..b08a4fe2b 100644 --- a/client/cmd/vnc_flags.go +++ b/client/cmd/vnc_flags.go @@ -1,9 +1,16 @@ package cmd -const serverVNCAllowedFlag = "allow-server-vnc" +const ( + serverVNCAllowedFlag = "allow-server-vnc" + disableVNCApprovalFlag = "disable-vnc-approval" +) -var serverVNCAllowed bool +var ( + serverVNCAllowed bool + disableVNCApproval bool +) func init() { upCmd.PersistentFlags().BoolVar(&serverVNCAllowed, serverVNCAllowedFlag, false, "Allow embedded VNC server on peer") + upCmd.PersistentFlags().BoolVar(&disableVNCApproval, disableVNCApprovalFlag, false, "Disable per-connection user approval prompts for the embedded VNC server") } diff --git a/client/internal/approval/broker.go b/client/internal/approval/broker.go new file mode 100644 index 000000000..0cc0d9514 --- /dev/null +++ b/client/internal/approval/broker.go @@ -0,0 +1,188 @@ +// Package approval brokers per-attempt user-accept prompts for inbound +// remote access (VNC today, SSH and others in the future). A caller pushes +// a Prompt; the broker emits a SystemEvent on the daemon→UI stream and +// blocks until the UI calls the daemon's RespondApproval RPC, the per- +// request timeout fires, or no subscriber is connected. The latter case +// fails closed so a backgrounded UI cannot silently bypass the gate. +package approval + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + "github.com/google/uuid" + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/proto" +) + +// Metadata keys the broker reserves on the emitted SystemEvent. Callers +// should not set these themselves; values in Prompt.Metadata that collide +// are overwritten by the broker. +const ( + MetaRequestID = "request_id" + MetaKind = "kind" + MetaExpiresAt = "expires_at" +) + +// Kind values for the well-known prompt subjects. New subsystems should +// add a constant here so the UI can dispatch on a known string. +const ( + KindVNC = "vnc" + KindSSH = "ssh" +) + +// DefaultTimeout is the wall-clock window the user has to accept or deny a +// pending approval before the broker fails closed and returns ErrTimeout. +// Kept well under typical VNC client and dashboard connection timeouts so +// the RFB rejection actually reaches the browser instead of racing the +// browser's own "connection timed out" message. +const DefaultTimeout = 15 * time.Second + +// timeoutValue returns the active timeout. It's a var so tests in this +// package can shorten the wait without exposing a setter on the public +// API. Production code always sees DefaultTimeout. +var timeoutValue = func() time.Duration { return DefaultTimeout } + +// ErrNoSubscriber indicates no UI is connected to consume the prompt. +// The caller must reject the underlying connection (fail-closed). +var ErrNoSubscriber = errors.New("no UI subscriber connected for approval") + +// ErrTimeout indicates the user did not respond within DefaultTimeout. +var ErrTimeout = errors.New("approval timed out") + +// ErrDenied indicates the user explicitly denied the connection. +var ErrDenied = errors.New("approval denied") + +// EventPublisher is the subset of peer.Status used to emit prompts. +type EventPublisher interface { + PublishEvent( + severity proto.SystemEvent_Severity, + category proto.SystemEvent_Category, + msg string, + userMsg string, + metadata map[string]string, + ) + HasEventSubscribers() bool +} + +// Prompt describes the pending request shown to the user. Kind selects +// the UI dispatch path (e.g. "vnc", "ssh"). Subject is the human-readable +// one-liner the UI may show as a title or notification body. Metadata is +// passed through verbatim and is the subsystem-specific payload (peer +// name, source IP, mode, etc.). +type Prompt struct { + Kind string + Subject string + Metadata map[string]string +} + +// Decision carries the user's response to an approval prompt. ViewOnly is +// only meaningful when Accept is true; it lets the host grant the +// connection but signal the requester that input control is withheld. +type Decision struct { + Accept bool + ViewOnly bool +} + +// Broker holds in-flight approval requests keyed by request ID. +type Broker struct { + pub EventPublisher + + mu sync.Mutex + pending map[string]chan Decision +} + +// New returns a broker that publishes prompts via pub. +func New(pub EventPublisher) *Broker { + return &Broker{ + pub: pub, + pending: make(map[string]chan Decision), + } +} + +// Request emits a SystemEvent for p and blocks until the UI calls Respond, +// ctx is cancelled, or DefaultTimeout elapses. Returns a Decision when +// the user replied; ErrDenied / ErrTimeout / ErrNoSubscriber / ctx.Err +// otherwise. Callers must treat any non-nil error as a deny. +func (b *Broker) Request(ctx context.Context, p Prompt) (Decision, error) { + var zero Decision + if b == nil || b.pub == nil { + return zero, fmt.Errorf("approval broker not configured") + } + if !b.pub.HasEventSubscribers() { + return zero, ErrNoSubscriber + } + + id := uuid.NewString() + resp := make(chan Decision, 1) + + b.mu.Lock() + b.pending[id] = resp + b.mu.Unlock() + + defer b.dropPending(id) + + timeout := timeoutValue() + expiresAt := time.Now().Add(timeout) + meta := make(map[string]string, len(p.Metadata)+3) + for k, v := range p.Metadata { + meta[k] = v + } + meta[MetaRequestID] = id + meta[MetaKind] = p.Kind + meta[MetaExpiresAt] = expiresAt.UTC().Format(time.RFC3339) + + subject := p.Subject + if subject == "" { + subject = fmt.Sprintf("%s connection requires approval", p.Kind) + } + b.pub.PublishEvent(proto.SystemEvent_INFO, proto.SystemEvent_APPROVAL, subject, subject, meta) + log.Debugf("approval request %s (%s) emitted: %s", id, p.Kind, subject) + + timer := time.NewTimer(timeout) + defer timer.Stop() + + select { + case d := <-resp: + if !d.Accept { + return zero, ErrDenied + } + return d, nil + case <-timer.C: + return zero, ErrTimeout + case <-ctx.Done(): + return zero, ctx.Err() + } +} + +// Respond delivers the user's decision for id. Returns true when a pending +// request matched and was woken, false when id was unknown or already done. +func (b *Broker) Respond(id string, d Decision) bool { + if b == nil { + return false + } + b.mu.Lock() + ch, ok := b.pending[id] + if ok { + delete(b.pending, id) + } + b.mu.Unlock() + if !ok { + return false + } + select { + case ch <- d: + default: + } + return true +} + +func (b *Broker) dropPending(id string) { + b.mu.Lock() + delete(b.pending, id) + b.mu.Unlock() +} diff --git a/client/internal/approval/broker_test.go b/client/internal/approval/broker_test.go new file mode 100644 index 000000000..a8e748468 --- /dev/null +++ b/client/internal/approval/broker_test.go @@ -0,0 +1,434 @@ +package approval + +import ( + "context" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/client/proto" +) + +// fakePublisher records published events and reports whether subscribers +// are connected. The subscribers flag is the security-critical signal: +// when false the broker must refuse to emit and the gate must fail closed. +type fakePublisher struct { + mu sync.Mutex + subscribers bool + events []*proto.SystemEvent +} + +func (p *fakePublisher) PublishEvent( + severity proto.SystemEvent_Severity, + category proto.SystemEvent_Category, + msg string, + userMsg string, + metadata map[string]string, +) { + p.mu.Lock() + p.events = append(p.events, &proto.SystemEvent{ + Severity: severity, + Category: category, + Message: msg, + UserMessage: userMsg, + Metadata: metadata, + }) + p.mu.Unlock() +} + +func (p *fakePublisher) HasEventSubscribers() bool { + p.mu.Lock() + defer p.mu.Unlock() + return p.subscribers +} + +func (p *fakePublisher) lastEvent(t *testing.T) *proto.SystemEvent { + t.Helper() + p.mu.Lock() + defer p.mu.Unlock() + require.NotEmpty(t, p.events, "publisher saw no events") + return p.events[len(p.events)-1] +} + +func (p *fakePublisher) eventCount() int { + p.mu.Lock() + defer p.mu.Unlock() + return len(p.events) +} + +// TestRequestNoSubscriberFailsClosed is the core fail-closed invariant: +// when the UI is not subscribed, the broker must refuse without emitting +// an event or arming a waiter. A regression here is a silent bypass. +func TestRequestNoSubscriberFailsClosed(t *testing.T) { + pub := &fakePublisher{subscribers: false} + b := New(pub) + + _, err := b.Request(context.Background(), Prompt{Kind: KindVNC, Subject: "test"}) + assert.ErrorIs(t, err, ErrNoSubscriber) + assert.Equal(t, 0, pub.eventCount(), "no event must be emitted when fail-closed") + + b.mu.Lock() + pending := len(b.pending) + b.mu.Unlock() + assert.Equal(t, 0, pending, "no waiter must be registered on fail-closed") +} + +// TestRequestTimeoutDenies verifies that a request without a UI response +// returns ErrTimeout (deny) rather than nil (silent accept). Uses a short +// per-test broker timeout via Respond after the fact to keep the test fast. +func TestRequestTimeoutDenies(t *testing.T) { + // Replace DefaultTimeout for the lifetime of this test. + orig := DefaultTimeout + defaultTimeout(t, 60*time.Millisecond) + defer defaultTimeout(t, orig) + + pub := &fakePublisher{subscribers: true} + b := New(pub) + + start := time.Now() + _, err := b.Request(context.Background(), Prompt{Kind: KindVNC, Subject: "test"}) + assert.ErrorIs(t, err, ErrTimeout, "missing user response must yield ErrTimeout, not nil") + assert.GreaterOrEqual(t, time.Since(start), 50*time.Millisecond, "timeout fired prematurely") +} + +// TestRequestDenied returns ErrDenied when the UI responds with false. +func TestRequestDenied(t *testing.T) { + pub := &fakePublisher{subscribers: true} + b := New(pub) + + var requestID string + done := make(chan error, 1) + go func() { + done <- requestErr(b, context.Background(), Prompt{Kind: KindVNC, Subject: "test"}) + }() + + requestID = waitForRequestID(t, pub) + require.True(t, b.Respond(requestID, Decision{Accept: false})) + + select { + case err := <-done: + assert.ErrorIs(t, err, ErrDenied) + case <-time.After(time.Second): + t.Fatal("Request did not return after Respond(false)") + } +} + +// TestRequestAccepted is the happy path. Failure here doesn't bypass the +// gate but breaks the feature. +func TestRequestAccepted(t *testing.T) { + pub := &fakePublisher{subscribers: true} + b := New(pub) + + done := make(chan error, 1) + go func() { + done <- requestErr(b, context.Background(), Prompt{Kind: KindVNC, Subject: "test"}) + }() + + id := waitForRequestID(t, pub) + require.True(t, b.Respond(id, Decision{Accept: true})) + + select { + case err := <-done: + assert.NoError(t, err) + case <-time.After(time.Second): + t.Fatal("Request did not return after Respond(true)") + } +} + +// TestRequestCtxCancelDenies verifies that an upstream cancel (e.g. the +// engine shutting down mid-prompt) returns the cancel error rather than +// nil. A nil here would be a silent bypass on shutdown races. +func TestRequestCtxCancelDenies(t *testing.T) { + pub := &fakePublisher{subscribers: true} + b := New(pub) + + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan error, 1) + go func() { + done <- requestErr(b, ctx, Prompt{Kind: KindVNC, Subject: "test"}) + }() + + // Wait until the prompt is in flight so cancel races a live waiter. + _ = waitForRequestID(t, pub) + cancel() + + select { + case err := <-done: + assert.ErrorIs(t, err, context.Canceled) + case <-time.After(time.Second): + t.Fatal("Request did not return after ctx cancel") + } +} + +// TestRespondUnknownIsNoop ensures a stray RespondApproval RPC cannot +// affect or accidentally accept any in-flight request whose id it doesn't +// match. Also confirms it doesn't panic. +func TestRespondUnknownIsNoop(t *testing.T) { + pub := &fakePublisher{subscribers: true} + b := New(pub) + + // No in-flight prompts: Respond returns false. + assert.False(t, b.Respond("does-not-exist", Decision{Accept: true})) + + // With an in-flight prompt, a wrong id still returns false and the + // prompt remains armed (eventually timing out as a deny). + defaultTimeout(t, 60*time.Millisecond) + defer defaultTimeout(t, DefaultTimeout) + + done := make(chan error, 1) + go func() { + done <- requestErr(b, context.Background(), Prompt{Kind: KindVNC}) + }() + realID := waitForRequestID(t, pub) + assert.False(t, b.Respond("totally-bogus", Decision{Accept: true}), "unknown id must not match") + assert.NotEqual(t, "totally-bogus", realID) + + select { + case err := <-done: + assert.ErrorIs(t, err, ErrTimeout, "armed prompt must still time out, not accept") + case <-time.After(time.Second): + t.Fatal("prompt did not resolve") + } +} + +// TestRespondAfterTimeoutNoop confirms a late accept response can't +// retroactively flip a denied (timed-out) request. The dropPending defer +// in Request must have removed the entry by the time Respond races in. +func TestRespondAfterTimeoutNoop(t *testing.T) { + defaultTimeout(t, 30*time.Millisecond) + defer defaultTimeout(t, DefaultTimeout) + + pub := &fakePublisher{subscribers: true} + b := New(pub) + + done := make(chan error, 1) + go func() { + done <- requestErr(b, context.Background(), Prompt{Kind: KindVNC}) + }() + id := waitForRequestID(t, pub) + + select { + case err := <-done: + require.ErrorIs(t, err, ErrTimeout) + case <-time.After(time.Second): + t.Fatal("prompt did not time out") + } + + assert.False(t, b.Respond(id, Decision{Accept: true}), "late respond must be no-op") +} + +// TestRespondDoubleNoop ensures a duplicate ack from the UI doesn't leak +// past the matched waiter or panic on a closed/full channel. +func TestRespondDoubleNoop(t *testing.T) { + pub := &fakePublisher{subscribers: true} + b := New(pub) + + done := make(chan error, 1) + go func() { + done <- requestErr(b, context.Background(), Prompt{Kind: KindVNC}) + }() + id := waitForRequestID(t, pub) + require.True(t, b.Respond(id, Decision{Accept: true})) + assert.False(t, b.Respond(id, Decision{Accept: false}), "second response must be no-op") + + select { + case err := <-done: + assert.NoError(t, err) + case <-time.After(time.Second): + t.Fatal("prompt did not resolve") + } +} + +// TestNilBrokerRequestErrors guards the engine pre-init path where the +// broker may not yet exist (or its publisher is nil): Request must +// error, never silently accept. +func TestNilBrokerRequestErrors(t *testing.T) { + var b *Broker + _, err := b.Request(context.Background(), Prompt{Kind: KindVNC}) + assert.Error(t, err, "nil broker must error, never silently accept") + + b2 := New(nil) + _, err = b2.Request(context.Background(), Prompt{Kind: KindVNC}) + assert.Error(t, err, "broker with nil publisher must error, never silently accept") +} + +// TestPromptMetadataInjected confirms the broker stamps request_id, kind, +// and expires_at on the emitted event. The UI relies on these keys; if +// they are dropped, the user cannot route the prompt and the response +// path breaks (which fails closed via timeout). +func TestPromptMetadataInjected(t *testing.T) { + pub := &fakePublisher{subscribers: true} + b := New(pub) + + done := make(chan error, 1) + go func() { + done <- requestErr(b, context.Background(), Prompt{ + Kind: KindVNC, + Subject: "VNC connection from peerA", + Metadata: map[string]string{"peer_name": "peerA"}, + }) + }() + + id := waitForRequestID(t, pub) + ev := pub.lastEvent(t) + + assert.Equal(t, proto.SystemEvent_APPROVAL, ev.Category) + assert.Equal(t, KindVNC, ev.Metadata[MetaKind]) + assert.Equal(t, id, ev.Metadata[MetaRequestID]) + assert.NotEmpty(t, ev.Metadata[MetaExpiresAt]) + assert.Equal(t, "peerA", ev.Metadata["peer_name"], "caller metadata must pass through") + + require.True(t, b.Respond(id, Decision{Accept: true})) + <-done +} + +// TestConcurrentRequests verifies that two concurrent prompts are tracked +// independently. A bug that aliases ids would let one Respond unblock +// the wrong waiter (a silent accept across prompts). +func TestConcurrentRequests(t *testing.T) { + pub := &fakePublisher{subscribers: true} + b := New(pub) + + const n = 20 + results := make(chan error, n) + for i := 0; i < n; i++ { + go func() { + results <- requestErr(b, context.Background(), Prompt{Kind: KindVNC}) + }() + } + + ids := waitForNRequestIDs(t, pub, n) + require.Len(t, ids, n) + + // Deny exactly half, accept the rest. Track outcome per id so we can + // match each Request's return value against the response we sent. + denySet := make(map[string]bool, n) + for i, id := range ids { + deny := i%2 == 0 + denySet[id] = deny + require.True(t, b.Respond(id, Decision{Accept: !deny})) + } + + // Collect all returns and check no nil errors slipped past a deny. + var accepted, denied atomic.Int32 + for i := 0; i < n; i++ { + select { + case err := <-results: + if err == nil { + accepted.Add(1) + } else { + assert.ErrorIs(t, err, ErrDenied) + denied.Add(1) + } + case <-time.After(2 * time.Second): + t.Fatalf("only got %d/%d responses", i, n) + } + } + assert.Equal(t, int32(n/2), denied.Load()) + assert.Equal(t, int32(n/2), accepted.Load()) +} + +// waitForRequestID blocks until the publisher sees its next event and +// returns the request_id stamped on it. +func waitForRequestID(t *testing.T, pub *fakePublisher) string { + t.Helper() + deadline := time.Now().Add(2 * time.Second) + for time.Now().Before(deadline) { + pub.mu.Lock() + count := len(pub.events) + var id string + if count > 0 { + id = pub.events[count-1].Metadata[MetaRequestID] + } + pub.mu.Unlock() + if id != "" { + return id + } + time.Sleep(2 * time.Millisecond) + } + t.Fatal("timeout waiting for emitted event") + return "" +} + +func waitForNRequestIDs(t *testing.T, pub *fakePublisher, n int) []string { + t.Helper() + deadline := time.Now().Add(2 * time.Second) + for time.Now().Before(deadline) { + pub.mu.Lock() + count := len(pub.events) + pub.mu.Unlock() + if count >= n { + break + } + time.Sleep(2 * time.Millisecond) + } + pub.mu.Lock() + defer pub.mu.Unlock() + out := make([]string, 0, len(pub.events)) + seen := make(map[string]struct{}, len(pub.events)) + for _, ev := range pub.events { + id := ev.Metadata[MetaRequestID] + if id == "" { + continue + } + if _, dup := seen[id]; dup { + continue + } + seen[id] = struct{}{} + out = append(out, id) + } + if len(out) < n { + t.Fatalf("only got %d/%d request ids", len(out), n) + } + return out +} + +// defaultTimeout swaps the broker's per-request wall-clock window so the +// timeout tests run quickly. Restores the prior value on the next call. +func defaultTimeout(t *testing.T, d time.Duration) { + t.Helper() + if d <= 0 { + t.Fatal("defaultTimeout must be > 0") + } + timeoutValue = func() time.Duration { return d } +} + +// requestErr wraps Broker.Request to drop the Decision when tests only +// care about the error path. Keeps the goroutine bodies tight. +func requestErr(b *Broker, ctx context.Context, p Prompt) error { + _, err := b.Request(ctx, p) + return err +} + +// TestRequestViewOnly checks the view-only outcome flows through Request's +// Decision return without being silently swallowed. +func TestRequestViewOnly(t *testing.T) { + pub := &fakePublisher{subscribers: true} + b := New(pub) + + type result struct { + d Decision + err error + } + done := make(chan result, 1) + go func() { + d, err := b.Request(context.Background(), Prompt{Kind: KindVNC}) + done <- result{d, err} + }() + + id := waitForRequestID(t, pub) + require.True(t, b.Respond(id, Decision{Accept: true, ViewOnly: true})) + + select { + case r := <-done: + assert.NoError(t, r.err) + assert.True(t, r.d.Accept) + assert.True(t, r.d.ViewOnly, "ViewOnly must survive the round-trip") + case <-time.After(time.Second): + t.Fatal("view-only request did not resolve") + } +} diff --git a/client/internal/connect.go b/client/internal/connect.go index af1b6a9cd..ade43e9f4 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -563,6 +563,7 @@ func createEngineConfig(key wgtypes.Key, config *profilemanager.Config, peerConf RosenpassPermissive: config.RosenpassPermissive, ServerSSHAllowed: util.ReturnBoolWithDefaultTrue(config.ServerSSHAllowed), ServerVNCAllowed: config.ServerVNCAllowed != nil && *config.ServerVNCAllowed, + DisableVNCApproval: config.DisableVNCApproval, EnableSSHRoot: config.EnableSSHRoot, EnableSSHSFTP: config.EnableSSHSFTP, EnableSSHLocalPortForwarding: config.EnableSSHLocalPortForwarding, diff --git a/client/internal/debug/debug.go b/client/internal/debug/debug.go index 09ac2c2cf..f259d4e1d 100644 --- a/client/internal/debug/debug.go +++ b/client/internal/debug/debug.go @@ -639,6 +639,9 @@ func (g *BundleGenerator) addCommonConfigFields(configContent *strings.Builder) if g.internalConfig.ServerVNCAllowed != nil { configContent.WriteString(fmt.Sprintf("ServerVNCAllowed: %v\n", *g.internalConfig.ServerVNCAllowed)) } + if g.internalConfig.DisableVNCApproval != nil { + configContent.WriteString(fmt.Sprintf("DisableVNCApproval: %v\n", *g.internalConfig.DisableVNCApproval)) + } configContent.WriteString(fmt.Sprintf("DisableClientRoutes: %v\n", g.internalConfig.DisableClientRoutes)) configContent.WriteString(fmt.Sprintf("DisableServerRoutes: %v\n", g.internalConfig.DisableServerRoutes)) diff --git a/client/internal/engine.go b/client/internal/engine.go index 98d4b9fb6..3b575a889 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -35,6 +35,7 @@ import ( "github.com/netbirdio/netbird/client/iface/udpmux" "github.com/netbirdio/netbird/client/iface/wgaddr" "github.com/netbirdio/netbird/client/internal/acl" + "github.com/netbirdio/netbird/client/internal/approval" "github.com/netbirdio/netbird/client/internal/debug" "github.com/netbirdio/netbird/client/internal/dns" dnsconfig "github.com/netbirdio/netbird/client/internal/dns/config" @@ -124,6 +125,7 @@ type EngineConfig struct { ServerSSHAllowed bool ServerVNCAllowed bool + DisableVNCApproval *bool EnableSSHRoot *bool EnableSSHSFTP *bool EnableSSHLocalPortForwarding *bool @@ -205,8 +207,9 @@ type Engine struct { networkMonitor *networkmonitor.NetworkMonitor - sshServer sshServer - vncSrv vncServer + sshServer sshServer + vncSrv vncServer + approvalBroker *approval.Broker statusRecorder *peer.Status @@ -287,6 +290,7 @@ func NewEngine( TURNs: []*stun.URI{}, networkSerial: 0, statusRecorder: services.StatusRecorder, + approvalBroker: approval.New(services.StatusRecorder), stateManager: services.StateManager, portForwardManager: portforward.NewManager(), checks: services.Checks, @@ -2608,3 +2612,16 @@ func decodeRelayIP(b []byte) netip.Addr { } return ip.Unmap() } + +// RespondApproval relays the user's decision for a pending approval to +// the broker. viewOnly is honoured only when accept is true. Returns +// true when the request_id matched a live prompt. +func (e *Engine) RespondApproval(requestID string, accept, viewOnly bool) bool { + if e == nil || e.approvalBroker == nil { + return false + } + return e.approvalBroker.Respond(requestID, approval.Decision{ + Accept: accept, + ViewOnly: accept && viewOnly, + }) +} diff --git a/client/internal/engine_ssh.go b/client/internal/engine_ssh.go index e02bcba36..17296d6d7 100644 --- a/client/internal/engine_ssh.go +++ b/client/internal/engine_ssh.go @@ -12,7 +12,7 @@ import ( firewallManager "github.com/netbirdio/netbird/client/firewall/manager" "github.com/netbirdio/netbird/client/iface/netstack" nftypes "github.com/netbirdio/netbird/client/internal/netflow/types" - sshauth "github.com/netbirdio/netbird/client/ssh/auth" + sshauth "github.com/netbirdio/netbird/shared/sessionauth" sshconfig "github.com/netbirdio/netbird/client/ssh/config" sshserver "github.com/netbirdio/netbird/client/ssh/server" mgmProto "github.com/netbirdio/netbird/shared/management/proto" diff --git a/client/internal/engine_vnc.go b/client/internal/engine_vnc.go index dfb63345c..82b92146e 100644 --- a/client/internal/engine_vnc.go +++ b/client/internal/engine_vnc.go @@ -11,9 +11,11 @@ import ( log "github.com/sirupsen/logrus" firewallManager "github.com/netbirdio/netbird/client/firewall/manager" + "github.com/netbirdio/netbird/client/internal/approval" "github.com/netbirdio/netbird/client/internal/metrics" nftypes "github.com/netbirdio/netbird/client/internal/netflow/types" - sshauth "github.com/netbirdio/netbird/client/ssh/auth" + "github.com/netbirdio/netbird/client/internal/peer" + sshauth "github.com/netbirdio/netbird/shared/sessionauth" vncserver "github.com/netbirdio/netbird/client/vnc/server" mgmProto "github.com/netbirdio/netbird/shared/management/proto" sshuserhash "github.com/netbirdio/netbird/shared/sshauth" @@ -118,6 +120,7 @@ func (e *Engine) startVNCServer() error { if serviceMode { log.Info("VNC: running as system service, enabling service mode (per-session agent proxy)") } + requireApproval := e.config.DisableVNCApproval == nil || !*e.config.DisableVNCApproval srv := vncserver.New(vncserver.Config{ Capturer: capturer, Injector: injector, @@ -125,6 +128,8 @@ func (e *Engine) startVNCServer() error { ServiceMode: serviceMode, SessionRecorder: sessionRecorder, NetstackNet: e.wgInterface.GetNet(), + RequireApproval: requireApproval, + Approver: &vncApprover{broker: e.approvalBroker, statusRecorder: e.statusRecorder}, }) listenAddr := netip.AddrPortFrom(netbirdIP, vncInternalPort) @@ -152,7 +157,6 @@ func (e *Engine) startVNCServer() error { return nil } - // updateVNCServerAuth updates VNC fine-grained access control from management. func (e *Engine) updateVNCServerAuth(vncAuth *mgmProto.VNCAuth) { if vncAuth == nil || e.vncSrv == nil { @@ -192,8 +196,9 @@ func (e *Engine) updateVNCServerAuth(vncAuth *mgmProto.VNCAuth) { continue } sessionPubKeys = append(sessionPubKeys, sshauth.SessionPubKey{ - PubKey: pub, - UserIDHash: sshuserhash.UserIDHash(hash), + PubKey: pub, + UserIDHash: sshuserhash.UserIDHash(hash), + DisplayName: e.GetDisplayName(), }) } @@ -238,3 +243,52 @@ func (e *Engine) stopVNCServer() error { } return nil } + +// vncApprover adapts the generic approval.Broker for the VNC server. +type vncApprover struct { + broker *approval.Broker + statusRecorder *peer.Status +} + +func (a *vncApprover) Request(ctx context.Context, info vncserver.ApprovalInfo) (vncserver.ApprovalDecision, error) { + // Resolve the source overlay IP to a peer FQDN for the prompt label. + if info.PeerName == "" && info.SourceIP != "" && a.statusRecorder != nil { + if fqdn, ok := a.statusRecorder.PeerByIP(info.SourceIP); ok { + info.PeerName = fqdn + } + } + subject := fmt.Sprintf("VNC connection from %s", displayPeer(info)) + meta := map[string]string{ + "peer_name": info.PeerName, + "peer_pubkey": info.PeerPubKey, + "source_ip": info.SourceIP, + "mode": info.Mode, + "username": info.Username, + "initiator": info.Initiator, + } + d, err := a.broker.Request(ctx, approval.Prompt{ + Kind: approval.KindVNC, + Subject: subject, + Metadata: meta, + }) + if err != nil { + return vncserver.ApprovalDecision{}, err + } + return vncserver.ApprovalDecision{ViewOnly: d.ViewOnly}, nil +} + +func displayPeer(info vncserver.ApprovalInfo) string { + if info.Initiator != "" { + return info.Initiator + } + if info.PeerName != "" { + return info.PeerName + } + if info.SourceIP != "" { + return info.SourceIP + } + if info.PeerPubKey != "" { + return info.PeerPubKey + } + return "unknown peer" +} diff --git a/client/internal/peer/status.go b/client/internal/peer/status.go index df746fa13..c74a3ed8c 100644 --- a/client/internal/peer/status.go +++ b/client/internal/peer/status.go @@ -1191,6 +1191,15 @@ func (d *Status) SubscribeToEvents() *EventSubscription { } } +// HasEventSubscribers reports whether any client is currently subscribed +// to the daemon's SystemEvent stream. Used by the VNC approval broker to +// fail closed when no UI is connected to prompt the user. +func (d *Status) HasEventSubscribers() bool { + d.eventMux.Lock() + defer d.eventMux.Unlock() + return len(d.eventStreams) > 0 +} + // UnsubscribeFromEvents removes an event subscription func (d *Status) UnsubscribeFromEvents(sub *EventSubscription) { if sub == nil { diff --git a/client/internal/profilemanager/config.go b/client/internal/profilemanager/config.go index 2d98e8cf7..a255a92c3 100644 --- a/client/internal/profilemanager/config.go +++ b/client/internal/profilemanager/config.go @@ -66,6 +66,7 @@ type ConfigInput struct { PreSharedKey *string ServerSSHAllowed *bool ServerVNCAllowed *bool + DisableVNCApproval *bool EnableSSHRoot *bool EnableSSHSFTP *bool EnableSSHLocalPortForwarding *bool @@ -118,6 +119,7 @@ type Config struct { RosenpassPermissive bool ServerSSHAllowed *bool ServerVNCAllowed *bool + DisableVNCApproval *bool EnableSSHRoot *bool EnableSSHSFTP *bool EnableSSHLocalPortForwarding *bool @@ -435,6 +437,18 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) { updated = true } + if input.DisableVNCApproval != nil { + if config.DisableVNCApproval == nil || *input.DisableVNCApproval != *config.DisableVNCApproval { + if *input.DisableVNCApproval { + log.Infof("disabling VNC connection approval prompt") + } else { + log.Infof("enabling VNC connection approval prompt") + } + config.DisableVNCApproval = input.DisableVNCApproval + updated = true + } + } + if input.EnableSSHRoot != nil && input.EnableSSHRoot != config.EnableSSHRoot { if *input.EnableSSHRoot { log.Infof("enabling SSH root login") diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index ec30f1ede..d5cb69277 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -203,6 +203,7 @@ const ( SystemEvent_AUTHENTICATION SystemEvent_Category = 2 SystemEvent_CONNECTIVITY SystemEvent_Category = 3 SystemEvent_SYSTEM SystemEvent_Category = 4 + SystemEvent_APPROVAL SystemEvent_Category = 5 ) // Enum value maps for SystemEvent_Category. @@ -213,6 +214,7 @@ var ( 2: "AUTHENTICATION", 3: "CONNECTIVITY", 4: "SYSTEM", + 5: "APPROVAL", } SystemEvent_Category_value = map[string]int32{ "NETWORK": 0, @@ -220,6 +222,7 @@ var ( "AUTHENTICATION": 2, "CONNECTIVITY": 3, "SYSTEM": 4, + "APPROVAL": 5, } ) @@ -344,6 +347,7 @@ type LoginRequest struct { SshJWTCacheTTL *int32 `protobuf:"varint,39,opt,name=sshJWTCacheTTL,proto3,oneof" json:"sshJWTCacheTTL,omitempty"` DisableIpv6 *bool `protobuf:"varint,40,opt,name=disable_ipv6,json=disableIpv6,proto3,oneof" json:"disable_ipv6,omitempty"` ServerVNCAllowed *bool `protobuf:"varint,41,opt,name=serverVNCAllowed,proto3,oneof" json:"serverVNCAllowed,omitempty"` + DisableVNCApproval *bool `protobuf:"varint,42,opt,name=disableVNCApproval,proto3,oneof" json:"disableVNCApproval,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -666,6 +670,13 @@ func (x *LoginRequest) GetServerVNCAllowed() bool { return false } +func (x *LoginRequest) GetDisableVNCApproval() bool { + if x != nil && x.DisableVNCApproval != nil { + return *x.DisableVNCApproval + } + return false +} + type LoginResponse struct { state protoimpl.MessageState `protogen:"open.v1"` NeedsSSOLogin bool `protobuf:"varint,1,opt,name=needsSSOLogin,proto3" json:"needsSSOLogin,omitempty"` @@ -1200,6 +1211,7 @@ type GetConfigResponse struct { SshJWTCacheTTL int32 `protobuf:"varint,26,opt,name=sshJWTCacheTTL,proto3" json:"sshJWTCacheTTL,omitempty"` DisableIpv6 bool `protobuf:"varint,27,opt,name=disable_ipv6,json=disableIpv6,proto3" json:"disable_ipv6,omitempty"` ServerVNCAllowed bool `protobuf:"varint,28,opt,name=serverVNCAllowed,proto3" json:"serverVNCAllowed,omitempty"` + DisableVNCApproval bool `protobuf:"varint,29,opt,name=disableVNCApproval,proto3" json:"disableVNCApproval,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1430,6 +1442,13 @@ func (x *GetConfigResponse) GetServerVNCAllowed() bool { return false } +func (x *GetConfigResponse) GetDisableVNCApproval() bool { + if x != nil { + return x.DisableVNCApproval + } + return false +} + // PeerState contains the latest state of a peer type PeerState struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -4191,6 +4210,7 @@ type SetConfigRequest struct { SshJWTCacheTTL *int32 `protobuf:"varint,34,opt,name=sshJWTCacheTTL,proto3,oneof" json:"sshJWTCacheTTL,omitempty"` DisableIpv6 *bool `protobuf:"varint,35,opt,name=disable_ipv6,json=disableIpv6,proto3,oneof" json:"disable_ipv6,omitempty"` ServerVNCAllowed *bool `protobuf:"varint,36,opt,name=serverVNCAllowed,proto3,oneof" json:"serverVNCAllowed,omitempty"` + DisableVNCApproval *bool `protobuf:"varint,37,opt,name=disableVNCApproval,proto3,oneof" json:"disableVNCApproval,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -4477,6 +4497,13 @@ func (x *SetConfigRequest) GetServerVNCAllowed() bool { return false } +func (x *SetConfigRequest) GetDisableVNCApproval() bool { + if x != nil && x.DisableVNCApproval != nil { + return *x.DisableVNCApproval + } + return false +} + type SetConfigResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -6325,6 +6352,109 @@ func (*StopBundleCaptureResponse) Descriptor() ([]byte, []int) { return file_daemon_proto_rawDescGZIP(), []int{95} } +type RespondApprovalRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // request_id matches the SystemEvent metadata key emitted by the daemon + // when a subsystem awaits user approval for an inbound connection. + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + // accept is true if the user approved the request, false if they + // denied it. A missing or unknown request_id is treated as a no-op. + Accept bool `protobuf:"varint,2,opt,name=accept,proto3" json:"accept,omitempty"` + // view_only signals that the user granted the connection but withheld + // input control. Only meaningful when accept is true; ignored when + // accept is false. + ViewOnly bool `protobuf:"varint,3,opt,name=view_only,json=viewOnly,proto3" json:"view_only,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RespondApprovalRequest) Reset() { + *x = RespondApprovalRequest{} + mi := &file_daemon_proto_msgTypes[96] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RespondApprovalRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RespondApprovalRequest) ProtoMessage() {} + +func (x *RespondApprovalRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[96] + 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 RespondApprovalRequest.ProtoReflect.Descriptor instead. +func (*RespondApprovalRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{96} +} + +func (x *RespondApprovalRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +func (x *RespondApprovalRequest) GetAccept() bool { + if x != nil { + return x.Accept + } + return false +} + +func (x *RespondApprovalRequest) GetViewOnly() bool { + if x != nil { + return x.ViewOnly + } + return false +} + +type RespondApprovalResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RespondApprovalResponse) Reset() { + *x = RespondApprovalResponse{} + mi := &file_daemon_proto_msgTypes[97] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RespondApprovalResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RespondApprovalResponse) ProtoMessage() {} + +func (x *RespondApprovalResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[97] + 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 RespondApprovalResponse.ProtoReflect.Descriptor instead. +func (*RespondApprovalResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{97} +} + type PortInfo_Range struct { state protoimpl.MessageState `protogen:"open.v1"` Start uint32 `protobuf:"varint,1,opt,name=start,proto3" json:"start,omitempty"` @@ -6335,7 +6465,7 @@ type PortInfo_Range struct { func (x *PortInfo_Range) Reset() { *x = PortInfo_Range{} - mi := &file_daemon_proto_msgTypes[97] + mi := &file_daemon_proto_msgTypes[99] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6347,7 +6477,7 @@ func (x *PortInfo_Range) String() string { func (*PortInfo_Range) ProtoMessage() {} func (x *PortInfo_Range) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[97] + mi := &file_daemon_proto_msgTypes[99] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6382,7 +6512,7 @@ var File_daemon_proto protoreflect.FileDescriptor const file_daemon_proto_rawDesc = "" + "\n" + "\fdaemon.proto\x12\x06daemon\x1a google/protobuf/descriptor.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/duration.proto\"\x0e\n" + - "\fEmptyRequest\"\xb5\x13\n" + + "\fEmptyRequest\"\x81\x14\n" + "\fLoginRequest\x12\x1a\n" + "\bsetupKey\x18\x01 \x01(\tR\bsetupKey\x12&\n" + "\fpreSharedKey\x18\x02 \x01(\tB\x02\x18\x01R\fpreSharedKey\x12$\n" + @@ -6428,7 +6558,8 @@ const file_daemon_proto_rawDesc = "" + "\x0edisableSSHAuth\x18& \x01(\bH\x19R\x0edisableSSHAuth\x88\x01\x01\x12+\n" + "\x0esshJWTCacheTTL\x18' \x01(\x05H\x1aR\x0esshJWTCacheTTL\x88\x01\x01\x12&\n" + "\fdisable_ipv6\x18( \x01(\bH\x1bR\vdisableIpv6\x88\x01\x01\x12/\n" + - "\x10serverVNCAllowed\x18) \x01(\bH\x1cR\x10serverVNCAllowed\x88\x01\x01B\x13\n" + + "\x10serverVNCAllowed\x18) \x01(\bH\x1cR\x10serverVNCAllowed\x88\x01\x01\x123\n" + + "\x12disableVNCApproval\x18* \x01(\bH\x1dR\x12disableVNCApproval\x88\x01\x01B\x13\n" + "\x11_rosenpassEnabledB\x10\n" + "\x0e_interfaceNameB\x10\n" + "\x0e_wireguardPortB\x17\n" + @@ -6457,7 +6588,8 @@ const file_daemon_proto_rawDesc = "" + "\x0f_disableSSHAuthB\x11\n" + "\x0f_sshJWTCacheTTLB\x0f\n" + "\r_disable_ipv6B\x13\n" + - "\x11_serverVNCAllowed\"\xb5\x01\n" + + "\x11_serverVNCAllowedB\x15\n" + + "\x13_disableVNCApproval\"\xb5\x01\n" + "\rLoginResponse\x12$\n" + "\rneedsSSOLogin\x18\x01 \x01(\bR\rneedsSSOLogin\x12\x1a\n" + "\buserCode\x18\x02 \x01(\tR\buserCode\x12(\n" + @@ -6490,7 +6622,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\"\xaa\t\n" + + "\busername\x18\x02 \x01(\tR\busername\"\xda\t\n" + "\x11GetConfigResponse\x12$\n" + "\rmanagementUrl\x18\x01 \x01(\tR\rmanagementUrl\x12\x1e\n" + "\n" + @@ -6523,7 +6655,8 @@ const file_daemon_proto_rawDesc = "" + "\x0edisableSSHAuth\x18\x19 \x01(\bR\x0edisableSSHAuth\x12&\n" + "\x0esshJWTCacheTTL\x18\x1a \x01(\x05R\x0esshJWTCacheTTL\x12!\n" + "\fdisable_ipv6\x18\x1b \x01(\bR\vdisableIpv6\x12*\n" + - "\x10serverVNCAllowed\x18\x1c \x01(\bR\x10serverVNCAllowed\"\x92\x06\n" + + "\x10serverVNCAllowed\x18\x1c \x01(\bR\x10serverVNCAllowed\x12.\n" + + "\x12disableVNCApproval\x18\x1d \x01(\bR\x12disableVNCApproval\"\x92\x06\n" + "\tPeerState\x12\x0e\n" + "\x02IP\x18\x01 \x01(\tR\x02IP\x12\x16\n" + "\x06pubKey\x18\x02 \x01(\tR\x06pubKey\x12\x1e\n" + @@ -6715,7 +6848,7 @@ const file_daemon_proto_rawDesc = "" + "\x13TracePacketResponse\x12*\n" + "\x06stages\x18\x01 \x03(\v2\x12.daemon.TraceStageR\x06stages\x12+\n" + "\x11final_disposition\x18\x02 \x01(\bR\x10finalDisposition\"\x12\n" + - "\x10SubscribeRequest\"\x93\x04\n" + + "\x10SubscribeRequest\"\xa1\x04\n" + "\vSystemEvent\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x128\n" + "\bseverity\x18\x02 \x01(\x0e2\x1c.daemon.SystemEvent.SeverityR\bseverity\x128\n" + @@ -6731,14 +6864,15 @@ const file_daemon_proto_rawDesc = "" + "\x04INFO\x10\x00\x12\v\n" + "\aWARNING\x10\x01\x12\t\n" + "\x05ERROR\x10\x02\x12\f\n" + - "\bCRITICAL\x10\x03\"R\n" + + "\bCRITICAL\x10\x03\"`\n" + "\bCategory\x12\v\n" + "\aNETWORK\x10\x00\x12\a\n" + "\x03DNS\x10\x01\x12\x12\n" + "\x0eAUTHENTICATION\x10\x02\x12\x10\n" + "\fCONNECTIVITY\x10\x03\x12\n" + "\n" + - "\x06SYSTEM\x10\x04\"\x12\n" + + "\x06SYSTEM\x10\x04\x12\f\n" + + "\bAPPROVAL\x10\x05\"\x12\n" + "\x10GetEventsRequest\"@\n" + "\x11GetEventsResponse\x12+\n" + "\x06events\x18\x01 \x03(\v2\x13.daemon.SystemEventR\x06events\"{\n" + @@ -6747,7 +6881,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\"\xde\x11\n" + + "\x15SwitchProfileResponse\"\xaa\x12\n" + "\x10SetConfigRequest\x12\x1a\n" + "\busername\x18\x01 \x01(\tR\busername\x12 \n" + "\vprofileName\x18\x02 \x01(\tR\vprofileName\x12$\n" + @@ -6788,7 +6922,8 @@ const file_daemon_proto_rawDesc = "" + "\x0edisableSSHAuth\x18! \x01(\bH\x16R\x0edisableSSHAuth\x88\x01\x01\x12+\n" + "\x0esshJWTCacheTTL\x18\" \x01(\x05H\x17R\x0esshJWTCacheTTL\x88\x01\x01\x12&\n" + "\fdisable_ipv6\x18# \x01(\bH\x18R\vdisableIpv6\x88\x01\x01\x12/\n" + - "\x10serverVNCAllowed\x18$ \x01(\bH\x19R\x10serverVNCAllowed\x88\x01\x01B\x13\n" + + "\x10serverVNCAllowed\x18$ \x01(\bH\x19R\x10serverVNCAllowed\x88\x01\x01\x123\n" + + "\x12disableVNCApproval\x18% \x01(\bH\x1aR\x12disableVNCApproval\x88\x01\x01B\x13\n" + "\x11_rosenpassEnabledB\x10\n" + "\x0e_interfaceNameB\x10\n" + "\x0e_wireguardPortB\x17\n" + @@ -6814,7 +6949,8 @@ const file_daemon_proto_rawDesc = "" + "\x0f_disableSSHAuthB\x11\n" + "\x0f_sshJWTCacheTTLB\x0f\n" + "\r_disable_ipv6B\x13\n" + - "\x11_serverVNCAllowed\"\x13\n" + + "\x11_serverVNCAllowedB\x15\n" + + "\x13_disableVNCApproval\"\x13\n" + "\x11SetConfigResponse\"Q\n" + "\x11AddProfileRequest\x12\x1a\n" + "\busername\x18\x01 \x01(\tR\busername\x12 \n" + @@ -6925,7 +7061,13 @@ const file_daemon_proto_rawDesc = "" + "\atimeout\x18\x01 \x01(\v2\x19.google.protobuf.DurationR\atimeout\"\x1c\n" + "\x1aStartBundleCaptureResponse\"\x1a\n" + "\x18StopBundleCaptureRequest\"\x1b\n" + - "\x19StopBundleCaptureResponse*b\n" + + "\x19StopBundleCaptureResponse\"l\n" + + "\x16RespondApprovalRequest\x12\x1d\n" + + "\n" + + "request_id\x18\x01 \x01(\tR\trequestId\x12\x16\n" + + "\x06accept\x18\x02 \x01(\bR\x06accept\x12\x1b\n" + + "\tview_only\x18\x03 \x01(\bR\bviewOnly\"\x19\n" + + "\x17RespondApprovalResponse*b\n" + "\bLogLevel\x12\v\n" + "\aUNKNOWN\x10\x00\x12\t\n" + "\x05PANIC\x10\x01\x12\t\n" + @@ -6943,7 +7085,7 @@ const file_daemon_proto_rawDesc = "" + "\n" + "EXPOSE_UDP\x10\x03\x12\x0e\n" + "\n" + - "EXPOSE_TLS\x10\x042\xaf\x17\n" + + "EXPOSE_TLS\x10\x042\x85\x18\n" + "\rDaemonService\x126\n" + "\x05Login\x12\x14.daemon.LoginRequest\x1a\x15.daemon.LoginResponse\"\x00\x12K\n" + "\fWaitSSOLogin\x12\x1b.daemon.WaitSSOLoginRequest\x1a\x1c.daemon.WaitSSOLoginResponse\"\x00\x12-\n" + @@ -6986,7 +7128,8 @@ const file_daemon_proto_rawDesc = "" + "\x0fStartCPUProfile\x12\x1e.daemon.StartCPUProfileRequest\x1a\x1f.daemon.StartCPUProfileResponse\"\x00\x12Q\n" + "\x0eStopCPUProfile\x12\x1d.daemon.StopCPUProfileRequest\x1a\x1e.daemon.StopCPUProfileResponse\"\x00\x12W\n" + "\x12GetInstallerResult\x12\x1e.daemon.InstallerResultRequest\x1a\x1f.daemon.InstallerResultResponse\"\x00\x12M\n" + - "\rExposeService\x12\x1c.daemon.ExposeServiceRequest\x1a\x1a.daemon.ExposeServiceEvent\"\x000\x01B\bZ\x06/protob\x06proto3" + "\rExposeService\x12\x1c.daemon.ExposeServiceRequest\x1a\x1a.daemon.ExposeServiceEvent\"\x000\x01\x12T\n" + + "\x0fRespondApproval\x12\x1e.daemon.RespondApprovalRequest\x1a\x1f.daemon.RespondApprovalResponse\"\x00B\bZ\x06/protob\x06proto3" var ( file_daemon_proto_rawDescOnce sync.Once @@ -7001,7 +7144,7 @@ func file_daemon_proto_rawDescGZIP() []byte { } var file_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 4) -var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 99) +var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 101) var file_daemon_proto_goTypes = []any{ (LogLevel)(0), // 0: daemon.LogLevel (ExposeProtocol)(0), // 1: daemon.ExposeProtocol @@ -7103,18 +7246,20 @@ var file_daemon_proto_goTypes = []any{ (*StartBundleCaptureResponse)(nil), // 97: daemon.StartBundleCaptureResponse (*StopBundleCaptureRequest)(nil), // 98: daemon.StopBundleCaptureRequest (*StopBundleCaptureResponse)(nil), // 99: daemon.StopBundleCaptureResponse - nil, // 100: daemon.Network.ResolvedIPsEntry - (*PortInfo_Range)(nil), // 101: daemon.PortInfo.Range - nil, // 102: daemon.SystemEvent.MetadataEntry - (*durationpb.Duration)(nil), // 103: google.protobuf.Duration - (*timestamppb.Timestamp)(nil), // 104: google.protobuf.Timestamp + (*RespondApprovalRequest)(nil), // 100: daemon.RespondApprovalRequest + (*RespondApprovalResponse)(nil), // 101: daemon.RespondApprovalResponse + nil, // 102: daemon.Network.ResolvedIPsEntry + (*PortInfo_Range)(nil), // 103: daemon.PortInfo.Range + nil, // 104: daemon.SystemEvent.MetadataEntry + (*durationpb.Duration)(nil), // 105: google.protobuf.Duration + (*timestamppb.Timestamp)(nil), // 106: google.protobuf.Timestamp } var file_daemon_proto_depIdxs = []int32{ - 103, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration + 105, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration 27, // 1: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus - 104, // 2: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp - 104, // 3: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp - 103, // 4: daemon.PeerState.latency:type_name -> google.protobuf.Duration + 106, // 2: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp + 106, // 3: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp + 105, // 4: daemon.PeerState.latency:type_name -> google.protobuf.Duration 23, // 5: daemon.SSHServerState.sessions:type_name -> daemon.SSHSessionInfo 25, // 6: daemon.VNCServerState.sessions:type_name -> daemon.VNCSessionInfo 20, // 7: daemon.FullStatus.managementState:type_name -> daemon.ManagementState @@ -7127,8 +7272,8 @@ var file_daemon_proto_depIdxs = []int32{ 24, // 14: daemon.FullStatus.sshServerState:type_name -> daemon.SSHServerState 26, // 15: daemon.FullStatus.vncServerState:type_name -> daemon.VNCServerState 33, // 16: daemon.ListNetworksResponse.routes:type_name -> daemon.Network - 100, // 17: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry - 101, // 18: daemon.PortInfo.range:type_name -> daemon.PortInfo.Range + 102, // 17: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry + 103, // 18: daemon.PortInfo.range:type_name -> daemon.PortInfo.Range 34, // 19: daemon.ForwardingRule.destinationPort:type_name -> daemon.PortInfo 34, // 20: daemon.ForwardingRule.translatedPort:type_name -> daemon.PortInfo 35, // 21: daemon.ForwardingRulesResponse.rules:type_name -> daemon.ForwardingRule @@ -7139,15 +7284,15 @@ var file_daemon_proto_depIdxs = []int32{ 54, // 26: daemon.TracePacketResponse.stages:type_name -> daemon.TraceStage 2, // 27: daemon.SystemEvent.severity:type_name -> daemon.SystemEvent.Severity 3, // 28: daemon.SystemEvent.category:type_name -> daemon.SystemEvent.Category - 104, // 29: daemon.SystemEvent.timestamp:type_name -> google.protobuf.Timestamp - 102, // 30: daemon.SystemEvent.metadata:type_name -> daemon.SystemEvent.MetadataEntry + 106, // 29: daemon.SystemEvent.timestamp:type_name -> google.protobuf.Timestamp + 104, // 30: daemon.SystemEvent.metadata:type_name -> daemon.SystemEvent.MetadataEntry 57, // 31: daemon.GetEventsResponse.events:type_name -> daemon.SystemEvent - 103, // 32: daemon.SetConfigRequest.dnsRouteInterval:type_name -> google.protobuf.Duration + 105, // 32: daemon.SetConfigRequest.dnsRouteInterval:type_name -> google.protobuf.Duration 70, // 33: daemon.ListProfilesResponse.profiles:type_name -> daemon.Profile 1, // 34: daemon.ExposeServiceRequest.protocol:type_name -> daemon.ExposeProtocol 93, // 35: daemon.ExposeServiceEvent.ready:type_name -> daemon.ExposeServiceReady - 103, // 36: daemon.StartCaptureRequest.duration:type_name -> google.protobuf.Duration - 103, // 37: daemon.StartBundleCaptureRequest.timeout:type_name -> google.protobuf.Duration + 105, // 36: daemon.StartCaptureRequest.duration:type_name -> google.protobuf.Duration + 105, // 37: daemon.StartBundleCaptureRequest.timeout:type_name -> google.protobuf.Duration 32, // 38: daemon.Network.ResolvedIPsEntry.value:type_name -> daemon.IPList 5, // 39: daemon.DaemonService.Login:input_type -> daemon.LoginRequest 7, // 40: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest @@ -7188,47 +7333,49 @@ var file_daemon_proto_depIdxs = []int32{ 87, // 75: daemon.DaemonService.StopCPUProfile:input_type -> daemon.StopCPUProfileRequest 89, // 76: daemon.DaemonService.GetInstallerResult:input_type -> daemon.InstallerResultRequest 91, // 77: daemon.DaemonService.ExposeService:input_type -> daemon.ExposeServiceRequest - 6, // 78: daemon.DaemonService.Login:output_type -> daemon.LoginResponse - 8, // 79: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse - 10, // 80: daemon.DaemonService.Up:output_type -> daemon.UpResponse - 12, // 81: daemon.DaemonService.Status:output_type -> daemon.StatusResponse - 14, // 82: daemon.DaemonService.Down:output_type -> daemon.DownResponse - 16, // 83: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse - 29, // 84: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse - 31, // 85: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse - 31, // 86: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse - 36, // 87: daemon.DaemonService.ForwardingRules:output_type -> daemon.ForwardingRulesResponse - 38, // 88: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse - 40, // 89: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse - 42, // 90: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse - 45, // 91: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse - 47, // 92: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse - 49, // 93: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse - 51, // 94: daemon.DaemonService.SetSyncResponsePersistence:output_type -> daemon.SetSyncResponsePersistenceResponse - 55, // 95: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse - 95, // 96: daemon.DaemonService.StartCapture:output_type -> daemon.CapturePacket - 97, // 97: daemon.DaemonService.StartBundleCapture:output_type -> daemon.StartBundleCaptureResponse - 99, // 98: daemon.DaemonService.StopBundleCapture:output_type -> daemon.StopBundleCaptureResponse - 57, // 99: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent - 59, // 100: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse - 61, // 101: daemon.DaemonService.SwitchProfile:output_type -> daemon.SwitchProfileResponse - 63, // 102: daemon.DaemonService.SetConfig:output_type -> daemon.SetConfigResponse - 65, // 103: daemon.DaemonService.AddProfile:output_type -> daemon.AddProfileResponse - 67, // 104: daemon.DaemonService.RemoveProfile:output_type -> daemon.RemoveProfileResponse - 69, // 105: daemon.DaemonService.ListProfiles:output_type -> daemon.ListProfilesResponse - 72, // 106: daemon.DaemonService.GetActiveProfile:output_type -> daemon.GetActiveProfileResponse - 74, // 107: daemon.DaemonService.Logout:output_type -> daemon.LogoutResponse - 76, // 108: daemon.DaemonService.GetFeatures:output_type -> daemon.GetFeaturesResponse - 78, // 109: daemon.DaemonService.TriggerUpdate:output_type -> daemon.TriggerUpdateResponse - 80, // 110: daemon.DaemonService.GetPeerSSHHostKey:output_type -> daemon.GetPeerSSHHostKeyResponse - 82, // 111: daemon.DaemonService.RequestJWTAuth:output_type -> daemon.RequestJWTAuthResponse - 84, // 112: daemon.DaemonService.WaitJWTToken:output_type -> daemon.WaitJWTTokenResponse - 86, // 113: daemon.DaemonService.StartCPUProfile:output_type -> daemon.StartCPUProfileResponse - 88, // 114: daemon.DaemonService.StopCPUProfile:output_type -> daemon.StopCPUProfileResponse - 90, // 115: daemon.DaemonService.GetInstallerResult:output_type -> daemon.InstallerResultResponse - 92, // 116: daemon.DaemonService.ExposeService:output_type -> daemon.ExposeServiceEvent - 78, // [78:117] is the sub-list for method output_type - 39, // [39:78] is the sub-list for method input_type + 100, // 78: daemon.DaemonService.RespondApproval:input_type -> daemon.RespondApprovalRequest + 6, // 79: daemon.DaemonService.Login:output_type -> daemon.LoginResponse + 8, // 80: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse + 10, // 81: daemon.DaemonService.Up:output_type -> daemon.UpResponse + 12, // 82: daemon.DaemonService.Status:output_type -> daemon.StatusResponse + 14, // 83: daemon.DaemonService.Down:output_type -> daemon.DownResponse + 16, // 84: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse + 29, // 85: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse + 31, // 86: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse + 31, // 87: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse + 36, // 88: daemon.DaemonService.ForwardingRules:output_type -> daemon.ForwardingRulesResponse + 38, // 89: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse + 40, // 90: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse + 42, // 91: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse + 45, // 92: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse + 47, // 93: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse + 49, // 94: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse + 51, // 95: daemon.DaemonService.SetSyncResponsePersistence:output_type -> daemon.SetSyncResponsePersistenceResponse + 55, // 96: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse + 95, // 97: daemon.DaemonService.StartCapture:output_type -> daemon.CapturePacket + 97, // 98: daemon.DaemonService.StartBundleCapture:output_type -> daemon.StartBundleCaptureResponse + 99, // 99: daemon.DaemonService.StopBundleCapture:output_type -> daemon.StopBundleCaptureResponse + 57, // 100: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent + 59, // 101: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse + 61, // 102: daemon.DaemonService.SwitchProfile:output_type -> daemon.SwitchProfileResponse + 63, // 103: daemon.DaemonService.SetConfig:output_type -> daemon.SetConfigResponse + 65, // 104: daemon.DaemonService.AddProfile:output_type -> daemon.AddProfileResponse + 67, // 105: daemon.DaemonService.RemoveProfile:output_type -> daemon.RemoveProfileResponse + 69, // 106: daemon.DaemonService.ListProfiles:output_type -> daemon.ListProfilesResponse + 72, // 107: daemon.DaemonService.GetActiveProfile:output_type -> daemon.GetActiveProfileResponse + 74, // 108: daemon.DaemonService.Logout:output_type -> daemon.LogoutResponse + 76, // 109: daemon.DaemonService.GetFeatures:output_type -> daemon.GetFeaturesResponse + 78, // 110: daemon.DaemonService.TriggerUpdate:output_type -> daemon.TriggerUpdateResponse + 80, // 111: daemon.DaemonService.GetPeerSSHHostKey:output_type -> daemon.GetPeerSSHHostKeyResponse + 82, // 112: daemon.DaemonService.RequestJWTAuth:output_type -> daemon.RequestJWTAuthResponse + 84, // 113: daemon.DaemonService.WaitJWTToken:output_type -> daemon.WaitJWTTokenResponse + 86, // 114: daemon.DaemonService.StartCPUProfile:output_type -> daemon.StartCPUProfileResponse + 88, // 115: daemon.DaemonService.StopCPUProfile:output_type -> daemon.StopCPUProfileResponse + 90, // 116: daemon.DaemonService.GetInstallerResult:output_type -> daemon.InstallerResultResponse + 92, // 117: daemon.DaemonService.ExposeService:output_type -> daemon.ExposeServiceEvent + 101, // 118: daemon.DaemonService.RespondApproval:output_type -> daemon.RespondApprovalResponse + 79, // [79:119] is the sub-list for method output_type + 39, // [39:79] is the sub-list for method input_type 39, // [39:39] is the sub-list for extension type_name 39, // [39:39] is the sub-list for extension extendee 0, // [0:39] is the sub-list for field type_name @@ -7261,7 +7408,7 @@ func file_daemon_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_proto_rawDesc), len(file_daemon_proto_rawDesc)), NumEnums: 4, - NumMessages: 99, + NumMessages: 101, NumExtensions: 0, NumServices: 1, }, diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index d0e720210..9592e3a75 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -119,6 +119,14 @@ service DaemonService { // ExposeService exposes a local port via the NetBird reverse proxy rpc ExposeService(ExposeServiceRequest) returns (stream ExposeServiceEvent) {} + + // RespondApproval delivers the user's accept/deny decision for a + // pending user-approval prompt. The daemon pushes the prompt as a + // SystemEvent with category APPROVAL and metadata key "request_id"; + // the UI calls this RPC with the same request_id to unblock whichever + // subsystem (VNC, SSH, ...) is waiting. The "kind" metadata key tells + // the UI which subsystem the prompt belongs to. + rpc RespondApproval(RespondApprovalRequest) returns (RespondApprovalResponse) {} } @@ -207,6 +215,8 @@ message LoginRequest { optional bool disable_ipv6 = 40; optional bool serverVNCAllowed = 41; + + optional bool disableVNCApproval = 42; } message LoginResponse { @@ -318,6 +328,8 @@ message GetConfigResponse { bool disable_ipv6 = 27; bool serverVNCAllowed = 28; + + bool disableVNCApproval = 29; } // PeerState contains the latest state of a peer @@ -616,6 +628,7 @@ message SystemEvent { AUTHENTICATION = 2; CONNECTIVITY = 3; SYSTEM = 4; + APPROVAL = 5; } string id = 1; @@ -701,6 +714,8 @@ message SetConfigRequest { optional bool disable_ipv6 = 35; optional bool serverVNCAllowed = 36; + + optional bool disableVNCApproval = 37; } message SetConfigResponse{} @@ -895,3 +910,18 @@ message StartBundleCaptureRequest { message StartBundleCaptureResponse {} message StopBundleCaptureRequest {} message StopBundleCaptureResponse {} + +message RespondApprovalRequest { + // request_id matches the SystemEvent metadata key emitted by the daemon + // when a subsystem awaits user approval for an inbound connection. + string request_id = 1; + // accept is true if the user approved the request, false if they + // denied it. A missing or unknown request_id is treated as a no-op. + bool accept = 2; + // view_only signals that the user granted the connection but withheld + // input control. Only meaningful when accept is true; ignored when + // accept is false. + bool view_only = 3; +} + +message RespondApprovalResponse {} diff --git a/client/proto/daemon_grpc.pb.go b/client/proto/daemon_grpc.pb.go index 66a8efcc3..8a11948ab 100644 --- a/client/proto/daemon_grpc.pb.go +++ b/client/proto/daemon_grpc.pb.go @@ -58,6 +58,7 @@ const ( DaemonService_StopCPUProfile_FullMethodName = "/daemon.DaemonService/StopCPUProfile" DaemonService_GetInstallerResult_FullMethodName = "/daemon.DaemonService/GetInstallerResult" DaemonService_ExposeService_FullMethodName = "/daemon.DaemonService/ExposeService" + DaemonService_RespondApproval_FullMethodName = "/daemon.DaemonService/RespondApproval" ) // DaemonServiceClient is the client API for DaemonService service. @@ -134,6 +135,13 @@ type DaemonServiceClient interface { GetInstallerResult(ctx context.Context, in *InstallerResultRequest, opts ...grpc.CallOption) (*InstallerResultResponse, error) // ExposeService exposes a local port via the NetBird reverse proxy ExposeService(ctx context.Context, in *ExposeServiceRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ExposeServiceEvent], error) + // RespondApproval delivers the user's accept/deny decision for a + // pending user-approval prompt. The daemon pushes the prompt as a + // SystemEvent with category APPROVAL and metadata key "request_id"; + // the UI calls this RPC with the same request_id to unblock whichever + // subsystem (VNC, SSH, ...) is waiting. The "kind" metadata key tells + // the UI which subsystem the prompt belongs to. + RespondApproval(ctx context.Context, in *RespondApprovalRequest, opts ...grpc.CallOption) (*RespondApprovalResponse, error) } type daemonServiceClient struct { @@ -561,6 +569,16 @@ func (c *daemonServiceClient) ExposeService(ctx context.Context, in *ExposeServi // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type DaemonService_ExposeServiceClient = grpc.ServerStreamingClient[ExposeServiceEvent] +func (c *daemonServiceClient) RespondApproval(ctx context.Context, in *RespondApprovalRequest, opts ...grpc.CallOption) (*RespondApprovalResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RespondApprovalResponse) + err := c.cc.Invoke(ctx, DaemonService_RespondApproval_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // DaemonServiceServer is the server API for DaemonService service. // All implementations must embed UnimplementedDaemonServiceServer // for forward compatibility. @@ -635,6 +653,13 @@ type DaemonServiceServer interface { GetInstallerResult(context.Context, *InstallerResultRequest) (*InstallerResultResponse, error) // ExposeService exposes a local port via the NetBird reverse proxy ExposeService(*ExposeServiceRequest, grpc.ServerStreamingServer[ExposeServiceEvent]) error + // RespondApproval delivers the user's accept/deny decision for a + // pending user-approval prompt. The daemon pushes the prompt as a + // SystemEvent with category APPROVAL and metadata key "request_id"; + // the UI calls this RPC with the same request_id to unblock whichever + // subsystem (VNC, SSH, ...) is waiting. The "kind" metadata key tells + // the UI which subsystem the prompt belongs to. + RespondApproval(context.Context, *RespondApprovalRequest) (*RespondApprovalResponse, error) mustEmbedUnimplementedDaemonServiceServer() } @@ -762,6 +787,9 @@ func (UnimplementedDaemonServiceServer) GetInstallerResult(context.Context, *Ins func (UnimplementedDaemonServiceServer) ExposeService(*ExposeServiceRequest, grpc.ServerStreamingServer[ExposeServiceEvent]) error { return status.Error(codes.Unimplemented, "method ExposeService not implemented") } +func (UnimplementedDaemonServiceServer) RespondApproval(context.Context, *RespondApprovalRequest) (*RespondApprovalResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RespondApproval not implemented") +} func (UnimplementedDaemonServiceServer) mustEmbedUnimplementedDaemonServiceServer() {} func (UnimplementedDaemonServiceServer) testEmbeddedByValue() {} @@ -1464,6 +1492,24 @@ func _DaemonService_ExposeService_Handler(srv interface{}, stream grpc.ServerStr // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type DaemonService_ExposeServiceServer = grpc.ServerStreamingServer[ExposeServiceEvent] +func _DaemonService_RespondApproval_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RespondApprovalRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).RespondApproval(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DaemonService_RespondApproval_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).RespondApproval(ctx, req.(*RespondApprovalRequest)) + } + return interceptor(ctx, in, info, handler) +} + // DaemonService_ServiceDesc is the grpc.ServiceDesc for DaemonService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -1615,6 +1661,10 @@ var DaemonService_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetInstallerResult", Handler: _DaemonService_GetInstallerResult_Handler, }, + { + MethodName: "RespondApproval", + Handler: _DaemonService_RespondApproval_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/client/server/server.go b/client/server/server.go index ac6b14287..1784c91a8 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -377,6 +377,7 @@ func (s *Server) SetConfig(callerCtx context.Context, msg *proto.SetConfigReques config.DisableAutoConnect = msg.DisableAutoConnect config.ServerSSHAllowed = msg.ServerSSHAllowed config.ServerVNCAllowed = msg.ServerVNCAllowed + config.DisableVNCApproval = msg.DisableVNCApproval config.NetworkMonitor = msg.NetworkMonitor config.DisableClientRoutes = msg.DisableClientRoutes config.DisableServerRoutes = msg.DisableServerRoutes @@ -1448,6 +1449,27 @@ func (s *Server) ExposeService(req *proto.ExposeServiceRequest, srv proto.Daemon return nil } +// RespondApproval relays the user's accept/deny decision for a pending +// approval prompt to the engine's broker. Unknown or already-resolved +// request_ids are silently no-op'd so a slow UI cannot deny a prompt the +// user already handled (or that already timed out). +func (s *Server) RespondApproval(_ context.Context, msg *proto.RespondApprovalRequest) (*proto.RespondApprovalResponse, error) { + s.mutex.Lock() + connectClient := s.connectClient + s.mutex.Unlock() + if connectClient == nil { + return nil, gstatus.Errorf(codes.FailedPrecondition, "client not initialized") + } + engine := connectClient.Engine() + if engine == nil { + return nil, gstatus.Errorf(codes.FailedPrecondition, "engine not running") + } + if !engine.RespondApproval(msg.GetRequestId(), msg.GetAccept(), msg.GetViewOnly()) { + log.Debugf("approval response for unknown request_id %s", msg.GetRequestId()) + } + return &proto.RespondApprovalResponse{}, nil +} + func isUnixRunningDesktop() bool { if runtime.GOOS != "linux" && runtime.GOOS != "freebsd" { return false @@ -1565,6 +1587,7 @@ func (s *Server) GetConfig(ctx context.Context, req *proto.GetConfigRequest) (*p DisableAutoConnect: cfg.DisableAutoConnect, ServerSSHAllowed: *cfg.ServerSSHAllowed, ServerVNCAllowed: cfg.ServerVNCAllowed != nil && *cfg.ServerVNCAllowed, + DisableVNCApproval: cfg.DisableVNCApproval != nil && *cfg.DisableVNCApproval, RosenpassEnabled: cfg.RosenpassEnabled, RosenpassPermissive: cfg.RosenpassPermissive, LazyConnectionEnabled: cfg.LazyConnectionEnabled, diff --git a/client/server/setconfig_test.go b/client/server/setconfig_test.go index 01dbbed5a..8246c3243 100644 --- a/client/server/setconfig_test.go +++ b/client/server/setconfig_test.go @@ -59,6 +59,7 @@ func TestSetConfig_AllFieldsSaved(t *testing.T) { rosenpassPermissive := true serverSSHAllowed := true serverVNCAllowed := true + disableVNCApproval := true interfaceName := "utun100" wireguardPort := int64(51820) preSharedKey := "test-psk" @@ -85,6 +86,7 @@ func TestSetConfig_AllFieldsSaved(t *testing.T) { RosenpassPermissive: &rosenpassPermissive, ServerSSHAllowed: &serverSSHAllowed, ServerVNCAllowed: &serverVNCAllowed, + DisableVNCApproval: &disableVNCApproval, InterfaceName: &interfaceName, WireguardPort: &wireguardPort, OptionalPreSharedKey: &preSharedKey, @@ -131,6 +133,8 @@ func TestSetConfig_AllFieldsSaved(t *testing.T) { require.Equal(t, serverSSHAllowed, *cfg.ServerSSHAllowed) require.NotNil(t, cfg.ServerVNCAllowed) require.Equal(t, serverVNCAllowed, *cfg.ServerVNCAllowed) + require.NotNil(t, cfg.DisableVNCApproval) + require.Equal(t, disableVNCApproval, *cfg.DisableVNCApproval) require.Equal(t, interfaceName, cfg.WgIface) require.Equal(t, int(wireguardPort), cfg.WgPort) require.Equal(t, preSharedKey, cfg.PreSharedKey) @@ -184,6 +188,7 @@ func verifyAllFieldsCovered(t *testing.T, req *proto.SetConfigRequest) { "RosenpassPermissive": true, "ServerSSHAllowed": true, "ServerVNCAllowed": true, + "DisableVNCApproval": true, "InterfaceName": true, "WireguardPort": true, "OptionalPreSharedKey": true, @@ -246,6 +251,7 @@ func TestCLIFlags_MappedToSetConfig(t *testing.T) { "rosenpass-permissive": "RosenpassPermissive", "allow-server-ssh": "ServerSSHAllowed", "allow-server-vnc": "ServerVNCAllowed", + "disable-vnc-approval": "DisableVNCApproval", "interface-name": "InterfaceName", "wireguard-port": "WireguardPort", "preshared-key": "OptionalPreSharedKey", diff --git a/client/ssh/proxy/proxy_test.go b/client/ssh/proxy/proxy_test.go index b33d5f8f4..02cd1d58c 100644 --- a/client/ssh/proxy/proxy_test.go +++ b/client/ssh/proxy/proxy_test.go @@ -28,7 +28,7 @@ import ( "github.com/netbirdio/netbird/client/proto" nbssh "github.com/netbirdio/netbird/client/ssh" - sshauth "github.com/netbirdio/netbird/client/ssh/auth" + sshauth "github.com/netbirdio/netbird/shared/sessionauth" "github.com/netbirdio/netbird/client/ssh/server" "github.com/netbirdio/netbird/client/ssh/testutil" nbjwt "github.com/netbirdio/netbird/shared/auth/jwt" diff --git a/client/ssh/server/jwt_test.go b/client/ssh/server/jwt_test.go index b2f3ac6a0..def3658c9 100644 --- a/client/ssh/server/jwt_test.go +++ b/client/ssh/server/jwt_test.go @@ -23,7 +23,7 @@ import ( "github.com/stretchr/testify/require" nbssh "github.com/netbirdio/netbird/client/ssh" - sshauth "github.com/netbirdio/netbird/client/ssh/auth" + sshauth "github.com/netbirdio/netbird/shared/sessionauth" "github.com/netbirdio/netbird/client/ssh/client" "github.com/netbirdio/netbird/client/ssh/detection" "github.com/netbirdio/netbird/client/ssh/testutil" diff --git a/client/ssh/server/server.go b/client/ssh/server/server.go index 3d55de6dc..499743c66 100644 --- a/client/ssh/server/server.go +++ b/client/ssh/server/server.go @@ -23,7 +23,7 @@ import ( "golang.zx2c4.com/wireguard/tun/netstack" "github.com/netbirdio/netbird/client/iface/wgaddr" - sshauth "github.com/netbirdio/netbird/client/ssh/auth" + sshauth "github.com/netbirdio/netbird/shared/sessionauth" "github.com/netbirdio/netbird/client/ssh/detection" "github.com/netbirdio/netbird/shared/auth" "github.com/netbirdio/netbird/shared/auth/jwt" diff --git a/client/ui/approval.go b/client/ui/approval.go new file mode 100644 index 000000000..9e5beaf5c --- /dev/null +++ b/client/ui/approval.go @@ -0,0 +1,192 @@ +//go:build !(linux && 386) + +package main + +import ( + "context" + "fmt" + "strings" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/widget" + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/proto" +) + +// handleApprovalEvent forks a netbird-ui child process to render the +// dialog on its own fyne main loop. Top-level windows opened from a +// background goroutine of the tray process don't render reliably on +// Linux/GTK, so the rest of the UI (settings, login URL, update) uses +// the same fork pattern. +func (s *serviceClient) handleApprovalEvent(ev *proto.SystemEvent) { + if ev == nil || ev.Category != proto.SystemEvent_APPROVAL { + return + } + requestID := ev.Metadata["request_id"] + if requestID == "" { + log.Warnf("approval event missing request_id: %v", ev.Metadata) + return + } + args := []string{ + "--approval-request-id=" + requestID, + "--approval-kind=" + ev.Metadata["kind"], + "--approval-initiator=" + ev.Metadata["initiator"], + "--approval-peer-name=" + ev.Metadata["peer_name"], + "--approval-source-ip=" + ev.Metadata["source_ip"], + "--approval-username=" + ev.Metadata["username"], + "--approval-expires-at=" + ev.Metadata["expires_at"], + "--approval-subject=" + ev.UserMessage, + } + go s.eventHandler.runSelfCommand(s.ctx, "approval", args...) +} + +// showApprovalUI runs the dialog on the forked process's fyne main loop +// and forwards the user's decision to the daemon via RespondApproval. +func (s *serviceClient) showApprovalUI(req approvalRequest) { + w := s.app.NewWindow(approvalTitle(req.kind)) + w.Resize(fyne.NewSize(480, 260)) + w.CenterOnScreen() + w.RequestFocus() + + var rows []string + if req.initiator != "" { + rows = append(rows, "From user: "+req.initiator) + } + if req.peerName != "" { + rows = append(rows, "Via peer: "+req.peerName) + } + if req.sourceIP != "" && req.sourceIP != req.peerName { + rows = append(rows, "Source IP: "+req.sourceIP) + } + if req.username != "" { + rows = append(rows, "OS user: "+req.username) + } + if len(rows) == 0 { + rows = []string{"Remote: " + req.displayPeer()} + } + body := strings.Join(rows, "\n") + bodyLabel := widget.NewLabel(body) + bodyLabel.Wrapping = fyne.TextWrapWord + + countdown := widget.NewLabel("") + deadline := req.deadline() + updateCountdown := func() { + remaining := time.Until(deadline).Round(time.Second) + if remaining < 0 { + remaining = 0 + } + countdown.SetText(fmt.Sprintf("Auto-deny in %s", remaining)) + } + updateCountdown() + + type outcome struct { + accept bool + viewOnly bool + } + decided := make(chan outcome, 1) + decide := func(o outcome) { + select { + case decided <- o: + default: + } + } + + allow := widget.NewButton("Allow", func() { decide(outcome{accept: true}) }) + allow.Importance = widget.HighImportance + allowView := widget.NewButton("Allow (view only)", func() { decide(outcome{accept: true, viewOnly: true}) }) + deny := widget.NewButton("Deny", func() { decide(outcome{accept: false}) }) + + header := widget.NewLabelWithStyle(req.subject, fyne.TextAlignLeading, fyne.TextStyle{Bold: true}) + buttonRow := container.NewGridWithColumns(3, allow, allowView, deny) + info := container.NewVBox(header, widget.NewSeparator(), bodyLabel, widget.NewSeparator(), countdown) + w.SetContent(container.NewPadded(container.NewBorder(nil, buttonRow, nil, nil, info))) + w.SetCloseIntercept(func() { decide(outcome{}) }) + + go func() { + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + for range ticker.C { + if time.Until(deadline) <= 0 { + decide(outcome{}) + return + } + updateCountdown() + } + }() + + go func() { + o := <-decided + s.sendApprovalResponse(req.requestID, o.accept, o.viewOnly) + w.Close() + s.app.Quit() + }() + + w.Show() +} + +func (s *serviceClient) sendApprovalResponse(requestID string, accept, viewOnly bool) { + conn, err := s.getSrvClient(defaultFailTimeout) + if err != nil { + log.Warnf("approval response: get daemon client: %v", err) + return + } + ctx, cancel := context.WithTimeout(s.ctx, defaultFailTimeout) + defer cancel() + if _, err := conn.RespondApproval(ctx, &proto.RespondApprovalRequest{ + RequestId: requestID, + Accept: accept, + ViewOnly: viewOnly, + }); err != nil { + log.Warnf("approval response: %v", err) + } +} + +// approvalRequest is the parsed --approval-* CLI args that the forked +// dialog process consumes. +type approvalRequest struct { + requestID string + kind string + initiator string + peerName string + sourceIP string + username string + subject string + expiresAt string +} + +func (r approvalRequest) displayPeer() string { + switch { + case r.initiator != "": + return r.initiator + case r.peerName != "": + return r.peerName + case r.sourceIP != "": + return r.sourceIP + default: + return "unknown peer" + } +} + +// deadline returns the wall-clock auto-deny moment. Falls back to a short +// local window when the daemon's expires_at is missing/unparseable, so a +// stale value never leaves the dialog open indefinitely. +func (r approvalRequest) deadline() time.Time { + if t, err := time.Parse(time.RFC3339, r.expiresAt); err == nil { + return t + } + return time.Now().Add(13 * time.Second) +} + +func approvalTitle(kind string) string { + switch kind { + case "vnc": + return "Allow VNC Connection?" + case "ssh": + return "Allow SSH Connection?" + default: + return "Allow Incoming Connection?" + } +} diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index 863e8b291..f05e8dceb 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -97,13 +97,24 @@ func main() { showQuickActions: flags.showQuickActions, showUpdate: flags.showUpdate, showUpdateVersion: flags.showUpdateVersion, + showApproval: flags.showApproval, + approvalRequest: approvalRequest{ + requestID: flags.approvalRequestID, + kind: flags.approvalKind, + initiator: flags.approvalInitiator, + peerName: flags.approvalPeerName, + sourceIP: flags.approvalSourceIP, + username: flags.approvalUsername, + subject: flags.approvalSubject, + expiresAt: flags.approvalExpiresAt, + }, }) // Watch for theme/settings changes to update the icon. go watchSettingsChanges(a, client) // Run in window mode if any UI flag was set. - if flags.showSettings || flags.showNetworks || flags.showDebug || flags.showLoginURL || flags.showProfiles || flags.showQuickActions || flags.showUpdate { + if flags.showSettings || flags.showNetworks || flags.showDebug || flags.showLoginURL || flags.showProfiles || flags.showQuickActions || flags.showUpdate || flags.showApproval { a.Run() return } @@ -140,6 +151,16 @@ type cliFlags struct { saveLogsInFile bool showUpdate bool showUpdateVersion string + showApproval bool + + approvalRequestID string + approvalKind string + approvalInitiator string + approvalPeerName string + approvalSourceIP string + approvalUsername string + approvalSubject string + approvalExpiresAt string } // parseFlags reads and returns all needed command-line flags. @@ -161,6 +182,15 @@ func parseFlags() *cliFlags { flag.BoolVar(&flags.showLoginURL, "login-url", false, "show login URL in a popup window") flag.BoolVar(&flags.showUpdate, "update", false, "show update progress window") flag.StringVar(&flags.showUpdateVersion, "update-version", "", "version to update to") + flag.BoolVar(&flags.showApproval, "approval", false, "show inbound-connection approval prompt window") + flag.StringVar(&flags.approvalRequestID, "approval-request-id", "", "approval prompt: daemon-issued request id") + flag.StringVar(&flags.approvalKind, "approval-kind", "", "approval prompt: subsystem kind (vnc, ssh, ...)") + flag.StringVar(&flags.approvalInitiator, "approval-initiator", "", "approval prompt: display name of the user who initiated the connection") + flag.StringVar(&flags.approvalPeerName, "approval-peer-name", "", "approval prompt: remote peer FQDN") + flag.StringVar(&flags.approvalSourceIP, "approval-source-ip", "", "approval prompt: remote source IP") + flag.StringVar(&flags.approvalUsername, "approval-username", "", "approval prompt: requested OS username") + flag.StringVar(&flags.approvalSubject, "approval-subject", "", "approval prompt: human-readable subject line") + flag.StringVar(&flags.approvalExpiresAt, "approval-expires-at", "", "approval prompt: RFC3339 deadline at which the daemon auto-denies") flag.Parse() return &flags } @@ -288,6 +318,8 @@ type serviceClient struct { sEnableSSHRemotePortForward *widget.Check sDisableSSHAuth *widget.Check iSSHJWTCacheTTL *widget.Entry + sServerVNCAllowed *widget.Check + sDisableVNCApproval *widget.Check // observable settings over corresponding iMngURL and iPreSharedKey values. managementURL string @@ -309,6 +341,8 @@ type serviceClient struct { enableSSHRemotePortForward bool disableSSHAuth bool sshJWTCacheTTL int + serverVNCAllowed bool + disableVNCApproval bool connected bool daemonVersion string @@ -356,6 +390,8 @@ type newServiceClientArgs struct { showQuickActions bool showUpdate bool showUpdateVersion string + showApproval bool + approvalRequest approvalRequest } // newServiceClient instance constructor @@ -396,6 +432,8 @@ func newServiceClient(args *newServiceClientArgs) *serviceClient { s.showQuickActionsUI() case args.showUpdate: s.showUpdateProgress(ctx, args.showUpdateVersion) + case args.showApproval: + s.showApprovalUI(args.approvalRequest) } return s @@ -479,6 +517,8 @@ func (s *serviceClient) showSettingsUI() { s.sEnableSSHRemotePortForward = widget.NewCheck("Enable SSH Remote Port Forwarding", nil) s.sDisableSSHAuth = widget.NewCheck("Disable SSH Authentication", nil) s.iSSHJWTCacheTTL = widget.NewEntry() + s.sServerVNCAllowed = widget.NewCheck("Allow embedded VNC server on this peer", nil) + s.sDisableVNCApproval = widget.NewCheck("Skip per-connection approval prompt for VNC", nil) s.wSettings.SetContent(s.getSettingsForm()) s.wSettings.Resize(fyne.NewSize(600, 400)) @@ -591,7 +631,8 @@ func (s *serviceClient) hasSettingsChanged(iMngURL string, port, mtu int64) bool s.disableServerRoutes != s.sDisableServerRoutes.Checked || s.disableIPv6 != s.sDisableIPv6.Checked || s.blockLANAccess != s.sBlockLANAccess.Checked || - s.hasSSHChanges() + s.hasSSHChanges() || + s.hasVNCChanges() } func (s *serviceClient) applySettingsChanges(iMngURL string, port, mtu int64) error { @@ -650,6 +691,8 @@ func (s *serviceClient) buildSetConfigRequest(iMngURL string, port, mtu int64) ( req.EnableSSHLocalPortForwarding = &s.sEnableSSHLocalPortForward.Checked req.EnableSSHRemotePortForwarding = &s.sEnableSSHRemotePortForward.Checked req.DisableSSHAuth = &s.sDisableSSHAuth.Checked + req.ServerVNCAllowed = &s.sServerVNCAllowed.Checked + req.DisableVNCApproval = &s.sDisableVNCApproval.Checked sshJWTCacheTTLText := strings.TrimSpace(s.iSSHJWTCacheTTL.Text) if sshJWTCacheTTLText != "" { @@ -710,10 +753,12 @@ func (s *serviceClient) getSettingsForm() fyne.CanvasObject { connectionForm := s.getConnectionForm() networkForm := s.getNetworkForm() sshForm := s.getSSHForm() + vncForm := s.getVNCForm() tabs := container.NewAppTabs( container.NewTabItem("Connection", connectionForm), container.NewTabItem("Network", networkForm), container.NewTabItem("SSH", sshForm), + container.NewTabItem("VNC", vncForm), ) saveButton := widget.NewButtonWithIcon("Save", theme.ConfirmIcon(), s.saveSettings) saveButton.Importance = widget.HighImportance @@ -754,6 +799,15 @@ func (s *serviceClient) getSSHForm() *widget.Form { } } +func (s *serviceClient) getVNCForm() *widget.Form { + return &widget.Form{ + Items: []*widget.FormItem{ + {Text: "Allow VNC Server", Widget: s.sServerVNCAllowed}, + {Text: "Disable Connection Approval Prompt", Widget: s.sDisableVNCApproval}, + }, + } +} + func (s *serviceClient) hasSSHChanges() bool { currentSSHJWTCacheTTL := s.sshJWTCacheTTL if text := strings.TrimSpace(s.iSSHJWTCacheTTL.Text); text != "" { @@ -772,6 +826,11 @@ func (s *serviceClient) hasSSHChanges() bool { s.sshJWTCacheTTL != currentSSHJWTCacheTTL } +func (s *serviceClient) hasVNCChanges() bool { + return s.serverVNCAllowed != s.sServerVNCAllowed.Checked || + s.disableVNCApproval != s.sDisableVNCApproval.Checked +} + func (s *serviceClient) login(ctx context.Context, openURL bool) (*proto.LoginResponse, error) { conn, err := s.getSrvClient(defaultFailTimeout) if err != nil { @@ -1120,6 +1179,7 @@ func (s *serviceClient) onTrayReady() { s.eventManager = event.NewManager(s.notifier, s.addr) s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked()) + s.eventManager.AddHandler(s.handleApprovalEvent) s.eventManager.AddHandler(func(event *proto.SystemEvent) { if event.Category == proto.SystemEvent_SYSTEM { s.updateExitNodes() @@ -1355,6 +1415,12 @@ func (s *serviceClient) getSrvConfig() { if cfg.SSHJWTCacheTTL != nil { s.sshJWTCacheTTL = *cfg.SSHJWTCacheTTL } + if cfg.ServerVNCAllowed != nil { + s.serverVNCAllowed = *cfg.ServerVNCAllowed + } + if cfg.DisableVNCApproval != nil { + s.disableVNCApproval = *cfg.DisableVNCApproval + } if s.showAdvancedSettings { s.iMngURL.SetText(s.managementURL) @@ -1395,6 +1461,12 @@ func (s *serviceClient) getSrvConfig() { if cfg.SSHJWTCacheTTL != nil { s.iSSHJWTCacheTTL.SetText(strconv.Itoa(*cfg.SSHJWTCacheTTL)) } + if cfg.ServerVNCAllowed != nil { + s.sServerVNCAllowed.SetChecked(*cfg.ServerVNCAllowed) + } + if cfg.DisableVNCApproval != nil { + s.sDisableVNCApproval.SetChecked(*cfg.DisableVNCApproval) + } } if s.mNotifications == nil { @@ -1455,6 +1527,7 @@ func protoConfigToConfig(cfg *proto.GetConfigResponse) *profilemanager.Config { config.DisableAutoConnect = cfg.DisableAutoConnect config.ServerSSHAllowed = &cfg.ServerSSHAllowed config.ServerVNCAllowed = &cfg.ServerVNCAllowed + config.DisableVNCApproval = &cfg.DisableVNCApproval config.RosenpassEnabled = cfg.RosenpassEnabled config.RosenpassPermissive = cfg.RosenpassPermissive config.DisableNotifications = &cfg.DisableNotifications diff --git a/client/ui/event/event.go b/client/ui/event/event.go index 3b43fdc7f..06784d2de 100644 --- a/client/ui/event/event.go +++ b/client/ui/event/event.go @@ -112,7 +112,7 @@ func (e *Manager) handleEvent(event *proto.SystemEvent) { handlers := slices.Clone(e.handlers) e.mu.Unlock() - if event.UserMessage != "" && (enabled || event.Severity == proto.SystemEvent_CRITICAL) && !isV6DefaultRoutePartner(event) { + if event.UserMessage != "" && (enabled || event.Severity == proto.SystemEvent_CRITICAL) && !isV6DefaultRoutePartner(event) && event.Category != proto.SystemEvent_APPROVAL { title := e.getEventTitle(event) body := event.UserMessage id := event.Metadata["id"] diff --git a/client/vnc/server/agent_ipc.go b/client/vnc/server/agent_ipc.go index a9ef3a77a..dc2e96a47 100644 --- a/client/vnc/server/agent_ipc.go +++ b/client/vnc/server/agent_ipc.go @@ -72,6 +72,11 @@ func (s *Server) handleServiceConnection(conn net.Conn, sa sessionAgent) { } s.registerConnAuth(conn, header) + allow, decision := s.gateApproval(conn, header, authedLog) + if !allow { + return + } + socketPath, token, err := sa.Resolve(s.ctx) if err != nil { code := RejectCodeCapturerError @@ -87,7 +92,7 @@ func (s *Server) handleServiceConnection(conn net.Conn, sa sessionAgent) { Reader: io.MultiReader(&headerBuf, conn), Conn: conn, } - if err := proxyToAgent(s.ctx, replayConn, socketPath, token); err != nil { + if err := proxyToAgent(s.ctx, replayConn, socketPath, token, decision.ViewOnly); err != nil { rejectConnection(conn, codeMessage(RejectCodeCapturerError, err.Error())) authedLog.Warnf("VNC connection rejected: agent unreachable: %v", err) return @@ -124,12 +129,13 @@ func generateAuthToken() (string, error) { } // proxyToAgent dials the per-session agent's Unix socket, writes the -// raw token bytes, then copies bytes both ways until either side closes. -// The token must precede any RFB byte so the agent's verifyAgentToken -// can run first. Returns nil once a stream is established; the caller is -// responsible for sending an RFB-level rejection on error so the client -// sees a reason instead of a bare timeout. -func proxyToAgent(ctx context.Context, client net.Conn, socketPath, authToken string) error { +// raw token bytes plus a single view-only flag byte, then copies bytes +// both ways until either side closes. The token + flag prefix must +// precede any RFB byte so the agent's verifyAgentToken can run first. +// Returns nil once a stream is established; the caller is responsible +// for sending an RFB-level rejection on error so the client sees a +// reason instead of a bare timeout. +func proxyToAgent(ctx context.Context, client net.Conn, socketPath, authToken string, viewOnly bool) error { tokenBytes, err := hex.DecodeString(authToken) if err != nil || len(tokenBytes) != agentTokenLen { return fmt.Errorf("invalid auth token (len=%d): %w", len(tokenBytes), err) @@ -140,9 +146,14 @@ func proxyToAgent(ctx context.Context, client net.Conn, socketPath, authToken st return fmt.Errorf("dial agent at %s: %w", socketPath, err) } - if _, err := agentConn.Write(tokenBytes); err != nil { + preamble := make([]byte, len(tokenBytes)+1) + copy(preamble, tokenBytes) + if viewOnly { + preamble[len(tokenBytes)] = 1 + } + if _, err := agentConn.Write(preamble); err != nil { _ = agentConn.Close() - return fmt.Errorf("send auth token to agent: %w", err) + return fmt.Errorf("send auth preamble to agent: %w", err) } defer client.Close() diff --git a/client/vnc/server/noise_auth_test.go b/client/vnc/server/noise_auth_test.go index 34ec05490..08eae0839 100644 --- a/client/vnc/server/noise_auth_test.go +++ b/client/vnc/server/noise_auth_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/require" "golang.org/x/crypto/curve25519" - sshauth "github.com/netbirdio/netbird/client/ssh/auth" + sshauth "github.com/netbirdio/netbird/shared/sessionauth" sshuserhash "github.com/netbirdio/netbird/shared/sshauth" ) diff --git a/client/vnc/server/server.go b/client/vnc/server/server.go index eb384dcf2..d350be70f 100644 --- a/client/vnc/server/server.go +++ b/client/vnc/server/server.go @@ -23,7 +23,7 @@ import ( "golang.org/x/crypto/curve25519" "golang.zx2c4.com/wireguard/tun/netstack" - sshauth "github.com/netbirdio/netbird/client/ssh/auth" + sshauth "github.com/netbirdio/netbird/shared/sessionauth" ) // Connection modes sent by the client in the session header. @@ -36,12 +36,14 @@ const ( // stable so clients can branch on them without parsing free text. // Format: "CODE: human message". const ( - RejectCodeAuthForbidden = "AUTH_FORBIDDEN" - RejectCodeSessionError = "SESSION_ERROR" - RejectCodeCapturerError = "CAPTURER_ERROR" - RejectCodeUnsupportedOS = "UNSUPPORTED" - RejectCodeBadRequest = "BAD_REQUEST" - RejectCodeNoConsoleUser = "NO_CONSOLE_USER" + RejectCodeAuthForbidden = "AUTH_FORBIDDEN" + RejectCodeSessionError = "SESSION_ERROR" + RejectCodeCapturerError = "CAPTURER_ERROR" + RejectCodeUnsupportedOS = "UNSUPPORTED" + RejectCodeBadRequest = "BAD_REQUEST" + RejectCodeNoConsoleUser = "NO_CONSOLE_USER" + RejectCodeApprovalDenied = "APPROVAL_DENIED" + RejectCodeNoApprover = "NO_APPROVER" ) // EnvVNCDisableDownscale disables any platform-specific framebuffer @@ -173,11 +175,11 @@ type Server struct { network netip.Prefix log *log.Entry - mu sync.Mutex - listener net.Listener - ctx context.Context - cancel context.CancelFunc - vmgr virtualSessionManager + mu sync.Mutex + listener net.Listener + ctx context.Context + cancel context.CancelFunc + vmgr virtualSessionManager authorizer *sshauth.Authorizer netstackNet *netstack.Net // agentToken holds the raw token bytes for agent-mode auth. @@ -216,6 +218,14 @@ type Server struct { // this to its metrics framework. sessionRecorder func(SessionTick) + // requireApproval enables the per-connection user-accept gate. When + // true and approver is nil (or returns an error), the connection is + // rejected before any agent or session work. + requireApproval bool + // approver prompts the local user (via the daemon→UI event channel) + // to accept or deny each incoming connection. + approver Approver + // preListener, when non-nil, replaces the TCP listener Start would // open; addr/network args to Start are ignored. Used by the agent's // Unix-socket path. @@ -275,6 +285,40 @@ type Config struct { // addr/network args to Start are then ignored. The agent uses this to // listen on a Unix socket. Listener net.Listener + // RequireApproval gates each accepted connection on a user-side accept + // prompt before the proxy/session starts. Requires Approver to be set; + // otherwise the gate fails closed. + RequireApproval bool + // Approver brokers the per-connection prompt to the local user via the + // daemon→UI event channel. Nil disables the gate. + Approver Approver +} + +// Approver decouples the VNC server from the approval broker. A non-nil +// error means "do not proceed". +type Approver interface { + Request(ctx context.Context, info ApprovalInfo) (ApprovalDecision, error) +} + +// ApprovalDecision carries the parts of the user's response the VNC +// server acts on. Accept is implicit (errors signal deny). ViewOnly puts +// the session into read-only mode: the server drops input events. +type ApprovalDecision struct { + ViewOnly bool +} + +// ApprovalInfo describes the pending connection passed to the approver. +// Fields are best-effort; any may be empty. +type ApprovalInfo struct { + PeerName string + PeerPubKey string + SourceIP string + Mode string + Username string + // Initiator is the display name of the user who initiated the + // connection (typically the dashboard user). Resolved from the + // Noise-verified client static pubkey. + Initiator string } // New creates a VNC server from the provided Config. IdentityKey is the @@ -287,6 +331,8 @@ func New(cfg Config) *Server { identityKey: cfg.IdentityKey, serviceMode: cfg.ServiceMode, sessionRecorder: cfg.SessionRecorder, + requireApproval: cfg.RequireApproval, + approver: cfg.Approver, disableAuth: cfg.DisableAuth, netstackNet: cfg.NetstackNet, preListener: cfg.Listener, @@ -377,6 +423,59 @@ func (s *Server) untrackConn(c net.Conn) { s.sessionsMu.Unlock() } +// gateApproval prompts the local user to accept or deny conn before any +// session resources are allocated. On rejection the conn already received +// an RFB reject reason; the gate does not close it. +func (s *Server) gateApproval(conn net.Conn, header *connectionHeader, connLog *log.Entry) (bool, ApprovalDecision) { + if !s.requireApproval { + return true, ApprovalDecision{} + } + if s.approver == nil { + rejectConnection(conn, codeMessage(RejectCodeNoApprover, "approval required but no approver configured")) + connLog.Warn("VNC connection rejected: approval required but no approver") + return false, ApprovalDecision{} + } + info := ApprovalInfo{ + SourceIP: sourceIPString(conn.RemoteAddr()), + Mode: modeString(header.mode), + Username: header.username, + } + if len(header.clientStatic) == 32 { + info.PeerPubKey = hex.EncodeToString(header.clientStatic) + if s.authorizer != nil { + info.Initiator = s.authorizer.LookupSessionDisplayName(header.clientStatic) + } + } + decision, err := s.approver.Request(s.ctx, info) + if err != nil { + rejectConnection(conn, codeMessage(RejectCodeApprovalDenied, err.Error())) + connLog.Infof("VNC connection rejected: approval %v", err) + return false, ApprovalDecision{} + } + if decision.ViewOnly { + connLog.Info("VNC connection approved by user (view-only)") + } else { + connLog.Info("VNC connection approved by user") + } + return true, decision +} + +// sourceIPString returns the IP portion of a remote address, or the full +// string when no port is present (e.g. unix sockets). +func sourceIPString(addr net.Addr) string { + if addr == nil { + return "" + } + if ta, ok := addr.(*net.TCPAddr); ok && ta != nil { + return ta.IP.String() + } + host, _, err := net.SplitHostPort(addr.String()) + if err != nil { + return addr.String() + } + return host +} + // registerConnAuth records the verified Noise_IK identity for a live // connection so UpdateVNCAuth can later revoke it if policy changes. // No-op when auth is disabled (e.g. agent-mode loopback connections). @@ -673,7 +772,8 @@ func (s *Server) handleConnection(conn net.Conn) { _ = conn.Close() return } - if !s.verifyAgentToken(conn, connLog) { + ok, agentViewOnly := s.verifyAgentToken(conn, connLog) + if !ok { connLog.Info("VNC connection rejected: agent token check failed") return } @@ -683,13 +783,19 @@ func (s *Server) handleConnection(conn net.Conn) { _ = conn.Close() return } - connLog, sessionUserID, ok := s.authorizeSession(conn, header, connLog) + var sessionUserID string + connLog, sessionUserID, ok = s.authorizeSession(conn, header, connLog) if !ok { connLog.Info("VNC connection rejected: auth failed") return } s.registerConnAuth(conn, header) + allow, decision := s.gateApproval(conn, header, connLog) + if !allow { + return + } + capturer, injector, sessionCleanup, ok := s.acquireSessionResources(conn, header, &connLog) if !ok { connLog.Warn("VNC connection rejected: capturer/injector unavailable") @@ -726,6 +832,7 @@ func (s *Server) handleConnection(conn net.Conn) { serverW: w, serverH: h, log: connLog, + viewOnly: decision.ViewOnly || agentViewOnly, } sess.serve() connLog.Infof("VNC connection closed (%dms)", time.Since(start).Milliseconds()) @@ -791,8 +898,9 @@ func (s *Server) authenticateSession(header *connectionHeader) (string, error) { var vncIdentityMagic = []byte("NBV3") // Noise_IK_25519_ChaChaPoly_SHA256 message sizes (with empty payloads). -// msg1 = e(32) + s_AEAD(32+16) + payload_AEAD(0+16) = 96 bytes -// msg2 = e(32) + payload_AEAD(0+16) = 48 bytes +// +// msg1 = e(32) + s_AEAD(32+16) + payload_AEAD(0+16) = 96 bytes +// msg2 = e(32) + payload_AEAD(0+16) = 48 bytes const ( noiseInitiatorMsgLen = 96 noiseResponderMsgLen = 48 @@ -929,39 +1037,40 @@ func (s *Server) maybeRunNoiseHandshake(conn net.Conn, br *bufio.Reader) ([]byte return clientStatic, true, nil } -// verifyAgentToken validates the agent token prefix when configured. Returns -// false when the token is invalid or unreadable; the connection is closed. -func (s *Server) verifyAgentToken(conn net.Conn, connLog *log.Entry) bool { +// verifyAgentToken validates the agent token prefix when configured and +// reads the trailing view-only flag byte the daemon writes alongside it. +// Returns (ok, viewOnly). ok=false closes the connection. +func (s *Server) verifyAgentToken(conn net.Conn, connLog *log.Entry) (bool, bool) { if len(s.agentToken) == 0 { - return true + return true, false } - buf := make([]byte, len(s.agentToken)) + buf := make([]byte, len(s.agentToken)+1) if err := conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil { connLog.Debugf("set agent token deadline: %v", err) conn.Close() - return false + return false, false } if _, err := io.ReadFull(conn, buf); err != nil { if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { // Connect-then-close probes (port liveness checks) hit this // path on every dial; logging them would just flood the // daemon log without surfacing a real failure. - connLog.Tracef("agent auth: read token: %v", err) + connLog.Tracef("agent auth: read preamble: %v", err) } else { - connLog.Warnf("agent auth: read token: %v", err) + connLog.Warnf("agent auth: read preamble: %v", err) } conn.Close() - return false + return false, false } if err := conn.SetReadDeadline(time.Time{}); err != nil { connLog.Debugf("clear agent token deadline: %v", err) } - if subtle.ConstantTimeCompare(buf, s.agentToken) != 1 { + if subtle.ConstantTimeCompare(buf[:len(s.agentToken)], s.agentToken) != 1 { connLog.Warn("agent auth: invalid token, rejecting") conn.Close() - return false + return false, false } - return true + return true, buf[len(s.agentToken)] != 0 } // authorizeSession runs the Noise_IK handshake when auth is enabled. diff --git a/client/vnc/server/server_test.go b/client/vnc/server/server_test.go index 0a44de3e4..a8a6dffb8 100644 --- a/client/vnc/server/server_test.go +++ b/client/vnc/server/server_test.go @@ -3,15 +3,19 @@ package server import ( + "context" "encoding/binary" "encoding/hex" + "errors" "image" "io" "net" "net/netip" + "sync/atomic" "testing" "time" + log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -333,3 +337,180 @@ func TestSessionMode_RejectedWhenNoVMGR(t *testing.T) { require.NoError(t, err) assert.Contains(t, string(reason), RejectCodeUnsupportedOS) } + +// recordingApprover lets gate tests choose the outcome of the approval +// prompt and verify how often (and with what info) the gate calls it. +type recordingApprover struct { + calls atomic.Int32 + lastIn ApprovalInfo + decision ApprovalDecision + respond error +} + +func (r *recordingApprover) Request(_ context.Context, info ApprovalInfo) (ApprovalDecision, error) { + r.calls.Add(1) + r.lastIn = info + if r.respond != nil { + return ApprovalDecision{}, r.respond + } + return r.decision, nil +} + +// drainRejectClient simulates a remote VNC client just enough that +// rejectConnection's handshake-half completes promptly: it reads the +// server's "RFB 003.008\n", writes back a placeholder client version, and +// drains until EOF. Without this the rejectConnection path would block +// for up to two seconds on its SetReadDeadline. +func drainRejectClient(t *testing.T, c net.Conn) { + t.Helper() + go func() { + defer c.Close() + var srvVer [12]byte + if _, err := io.ReadFull(c, srvVer[:]); err != nil { + return + } + _, _ = c.Write([]byte("RFB 003.008\n")) + _, _ = io.Copy(io.Discard, c) + }() +} + +// newGateConn returns a server-side conn and a client-side conn linked by +// net.Pipe, with the client-side already draining so gateApproval's +// rejectConnection path completes without blocking the test. +func newGateConn(t *testing.T) net.Conn { + t.Helper() + srv, cli := net.Pipe() + drainRejectClient(t, cli) + t.Cleanup(func() { _ = srv.Close() }) + return srv +} + +func gateTestServer(requireApproval bool, approver Approver) *Server { + return &Server{ + log: log.WithField("test", "gate"), + requireApproval: requireApproval, + approver: approver, + } +} + +// TestGateApproval_Disabled_NoApproverCall: when the feature is off the +// gate must short-circuit before consulting any approver. A nil approver +// must NOT mean "deny" here — that would break upgrades for peers that +// haven't opted in yet. +func TestGateApproval_Disabled_NoApproverCall(t *testing.T) { + app := &recordingApprover{} + srv := gateTestServer(false, app) + + conn := newGateConn(t) + defer conn.Close() + header := &connectionHeader{mode: ModeAttach} + + allowed, _ := srv.gateApproval(conn, header, srv.log) + assert.True(t, allowed, "gate must pass through when requireApproval is false") + assert.Equal(t, int32(0), app.calls.Load(), "approver must not be called when disabled") +} + +// TestGateApproval_Enabled_NilApproverDenies is the most important +// regression test for "no silent bypass": if the feature is enabled but +// the broker wasn't wired (a misconfiguration), the gate must REJECT, +// not pass through. The reject code must be the dedicated NO_APPROVER so +// the failure is unambiguous in logs and on the client side. +func TestGateApproval_Enabled_NilApproverDenies(t *testing.T) { + srv := gateTestServer(true, nil) + + srvConn, cliConn := net.Pipe() + defer srvConn.Close() + defer cliConn.Close() + + // Capture the reject reason the gate sends. + rejectReason := make(chan string, 1) + go func() { + var srvVer [12]byte + _, _ = io.ReadFull(cliConn, srvVer[:]) + _, _ = cliConn.Write([]byte("RFB 003.008\n")) + // Server sends: 1 byte (numTypes=0), 4 bytes (reason len), reason. + var numTypes [1]byte + _, _ = io.ReadFull(cliConn, numTypes[:]) + var lenBuf [4]byte + _, _ = io.ReadFull(cliConn, lenBuf[:]) + reason := make([]byte, binary.BigEndian.Uint32(lenBuf[:])) + _, _ = io.ReadFull(cliConn, reason) + rejectReason <- string(reason) + }() + + header := &connectionHeader{mode: ModeAttach} + allowed, _ := srv.gateApproval(srvConn, header, srv.log) + assert.False(t, allowed, "missing approver MUST deny; never silently pass") + + select { + case reason := <-rejectReason: + assert.Contains(t, reason, RejectCodeNoApprover, "reject code must surface the misconfiguration cause") + case <-time.After(2 * time.Second): + t.Fatal("did not observe rejection reason") + } +} + +// TestGateApproval_ApproverDenies maps every approver error to a deny. +// We assert against every Err* the broker can produce so a future caller +// adding a new error doesn't accidentally fall into a default-allow. +func TestGateApproval_ApproverDenies(t *testing.T) { + cases := []struct { + name string + err error + }{ + {"denied", errors.New("user denied")}, + {"timeout", errors.New("approval timed out")}, + {"no_subscriber", errors.New("no UI subscriber connected for approval")}, + {"ctx_canceled", context.Canceled}, + {"misc", errors.New("anything else")}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + app := &recordingApprover{respond: tc.err} + srv := gateTestServer(true, app) + conn := newGateConn(t) + defer conn.Close() + + header := &connectionHeader{mode: ModeAttach} + allowed, _ := srv.gateApproval(conn, header, srv.log) + assert.False(t, allowed, "approver error %v must deny", tc.err) + assert.Equal(t, int32(1), app.calls.Load()) + }) + } +} + +// TestGateApproval_ApproverAccepts confirms the happy path actually +// returns true so we know the deny path is not the only outcome the +// gate can produce. +func TestGateApproval_ApproverAccepts(t *testing.T) { + app := &recordingApprover{respond: nil} + srv := gateTestServer(true, app) + conn := newGateConn(t) + defer conn.Close() + + header := &connectionHeader{mode: ModeAttach, username: "alice"} + allowed, _ := srv.gateApproval(conn, header, srv.log) + assert.True(t, allowed, "approver returning nil must let the gate pass") + assert.Equal(t, int32(1), app.calls.Load()) + assert.Equal(t, "alice", app.lastIn.Username, "header username must reach the approver") +} + +// TestGateApproval_PassesPubKeyHex confirms the gate hex-encodes the +// 32-byte client static key into ApprovalInfo.PeerPubKey so the prompt's +// metadata identifies which peer is connecting. A wrong-length key must +// NOT bypass the gate; it just won't populate the field. +func TestGateApproval_PassesPubKeyHex(t *testing.T) { + app := &recordingApprover{respond: nil} + srv := gateTestServer(true, app) + conn := newGateConn(t) + defer conn.Close() + + pub := make([]byte, 32) + for i := range pub { + pub[i] = byte(i) + } + header := &connectionHeader{mode: ModeAttach, clientStatic: pub} + allowed, _ := srv.gateApproval(conn, header, srv.log) + assert.True(t, allowed) + assert.Equal(t, hex.EncodeToString(pub), app.lastIn.PeerPubKey) +} diff --git a/client/vnc/server/session.go b/client/vnc/server/session.go index 50db02658..210e4379f 100644 --- a/client/vnc/server/session.go +++ b/client/vnc/server/session.go @@ -55,6 +55,11 @@ type session struct { serverH int desktopName string log *log.Entry + // viewOnly drops KeyEvent / PointerEvent (legacy + QEMU + extended) + // without invoking the injector when the user approved the + // connection in view-only mode. The bytes are still consumed off the + // wire so the protocol stays in sync. + viewOnly bool writeMu sync.Mutex // encMu guards the negotiated pixel format and encoding state below. @@ -161,6 +166,15 @@ func (s *session) serve() { } s.log.Infof("client connected: %s", s.addr()) + // View-only clients can't move the pointer, so default to compositing + // the host cursor into the framebuffer. The client can still send + // ShowRemoteCursor to turn it off. + if s.viewOnly { + s.encMu.Lock() + s.showRemoteCursor = true + s.encMu.Unlock() + } + // On any exit path (clean disconnect, transport error, panic) release // modifier keys and mouse buttons so the host doesn't end up with // Shift/Ctrl/Alt or a mouse button stuck because the client dropped @@ -226,8 +240,10 @@ func (s *session) handshake() error { // mode, username) that precedes the RFB handshake; the protocol-level // password scheme is not supported. func (s *session) sendSecurityTypes() error { - _, err := s.conn.Write([]byte{1, secNone}) - return err + if _, err := s.conn.Write([]byte{1, secNone}); err != nil { + return err + } + return nil } func (s *session) handleSecurity(secType byte) error { @@ -237,11 +253,20 @@ func (s *session) handleSecurity(secType byte) error { return binary.Write(s.conn, binary.BigEndian, uint32(0)) } +// ViewOnlyDesktopNamePrefix tags the RFB desktop name when the host +// approved the connection in view-only mode, so a NetBird-aware client +// can switch its UI into read-only state. NUL framing guarantees no +// collision with a user-set name. +const ViewOnlyDesktopNamePrefix = "\x00NB-VIEW-ONLY\x00" + func (s *session) sendServerInit() error { desktop := s.desktopName if desktop == "" { desktop = "NetBird VNC" } + if s.viewOnly { + desktop = ViewOnlyDesktopNamePrefix + desktop + } name := []byte(desktop) buf := make([]byte, 0, 4+16+4+len(name)) @@ -259,8 +284,10 @@ func (s *session) sendServerInit() error { ) buf = append(buf, name...) - _, err := s.conn.Write(buf) - return err + if _, err := s.conn.Write(buf); err != nil { + return err + } + return nil } func (s *session) messageLoop() error { @@ -536,8 +563,10 @@ func (s *session) SendDesktopName(name string) error { if _, err := s.conn.Write(header); err != nil { return err } - _, err := s.conn.Write(body) - return err + if _, err := s.conn.Write(body); err != nil { + return err + } + return nil } func (s *session) handleKeyEvent() error { @@ -545,6 +574,9 @@ func (s *session) handleKeyEvent() error { if _, err := io.ReadFull(s.conn, data[:]); err != nil { return fmt.Errorf("read KeyEvent: %w", err) } + if s.viewOnly { + return nil + } down := data[0] == 1 keysym := binary.BigEndian.Uint32(data[3:7]) s.injector.InjectKey(keysym, down) @@ -565,6 +597,9 @@ func (s *session) handleQEMUMessage() error { s.log.Tracef("ignoring QEMU subtype %d", subtype) return nil } + if s.viewOnly { + return nil + } down := binary.BigEndian.Uint16(data[1:3]) != 0 keysym := binary.BigEndian.Uint32(data[3:7]) scancode := binary.BigEndian.Uint32(data[7:11]) @@ -598,6 +633,9 @@ func (s *session) handlePointerEvent() error { s.lastPointerX = x s.lastPointerY = y s.pointerMu.Unlock() + if s.viewOnly { + return nil + } s.injector.InjectPointer(mask, x, y, s.serverW, s.serverH) return nil } diff --git a/client/wasm/internal/vnc/proxy.go b/client/wasm/internal/vnc/proxy.go index 60f03b212..2d57927ed 100644 --- a/client/wasm/internal/vnc/proxy.go +++ b/client/wasm/internal/vnc/proxy.go @@ -58,16 +58,14 @@ func NewSessionKey() (string, []byte, error) { return id, kp.Public, nil } -// consumeSessionKey atomically retrieves and removes the keypair for id. -// A session handle is single-use; combining lookup and delete under one -// critical section prevents concurrent callers from observing the same key. -func consumeSessionKey(id string) (noise.DHKey, bool) { +// lookupSessionKey returns the keypair for id. Keys stay live for the +// WASM lifetime so the same session handle can drive multiple VNC +// connections (reconnect, multiple peers, etc.). The handle is just an +// opaque map key; the private half never leaves wasm. +func lookupSessionKey(id string) (noise.DHKey, bool) { sessionKeyStore.mu.Lock() defer sessionKeyStore.mu.Unlock() kp, ok := sessionKeyStore.keys[id] - if ok { - delete(sessionKeyStore.keys, id) - } return kp, ok } @@ -187,7 +185,7 @@ func (p *VNCProxy) CreateProxy(req ProxyRequest) js.Value { height: height, } if req.KeySessionID != "" { - kp, ok := consumeSessionKey(req.KeySessionID) + kp, ok := lookupSessionKey(req.KeySessionID) if !ok { return rejectedPromise("unknown VNC session id") } @@ -217,11 +215,11 @@ func decodePeerPubKey(b64 string) ([]byte, error) { return raw, nil } -// rejectedPromise returns a resolved Promise carrying msg as an error -// string, mirroring how CreateProxy reports earlier validation failures. +// rejectedPromise returns a rejected Promise carrying msg as the +// reason. Callers in JS see this via `await ...` throwing. func rejectedPromise(msg string) js.Value { promise := js.Global().Get("Promise") - return promise.Call("resolve", js.ValueOf(msg)) + return promise.Call("reject", js.ValueOf(msg)) } // newProxyPromise wraps the JS Promise creation + executor lifecycle so diff --git a/management/internals/shared/grpc/conversion.go b/management/internals/shared/grpc/conversion.go index 1ecb7306b..66c4729da 100644 --- a/management/internals/shared/grpc/conversion.go +++ b/management/internals/shared/grpc/conversion.go @@ -13,7 +13,7 @@ import ( integrationsConfig "github.com/netbirdio/management-integrations/integrations/config" - "github.com/netbirdio/netbird/client/ssh/auth" + auth "github.com/netbirdio/netbird/shared/sessionauth" nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/internals/controllers/network_map/controller/cache" @@ -223,8 +223,9 @@ func buildSessionPubKeysProto(ctx context.Context, in []types.VNCSessionPubKey) continue } out = append(out, &proto.SessionPubKey{ - PubKey: pub, - UserIdHash: hash[:], + PubKey: pub, + UserIdHash: hash[:], + DisplayName: e.DisplayName, }) } return out diff --git a/management/server/http/handlers/peers/peers_handler.go b/management/server/http/handlers/peers/peers_handler.go index 45ec2556c..8e7385f7a 100644 --- a/management/server/http/handlers/peers/peers_handler.go +++ b/management/server/http/handlers/peers/peers_handler.go @@ -22,6 +22,7 @@ import ( "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" "github.com/netbirdio/netbird/management/server/types" + "github.com/netbirdio/netbird/shared/auth" "github.com/netbirdio/netbird/shared/management/http/api" "github.com/netbirdio/netbird/shared/management/http/util" "github.com/netbirdio/netbird/shared/management/status" @@ -525,6 +526,7 @@ func (h *Handler) CreateTemporaryAccess(w http.ResponseWriter, r *http.Request) return } policy.Rules[0].SessionPubKey = pubKey + policy.Rules[0].SessionDisplayName = h.displayNameForUser(r.Context(), userAuth) } _, err = h.accountManager.SavePolicy(r.Context(), userAuth.AccountId, userAuth.UserId, policy, true) @@ -744,3 +746,30 @@ func peerIPv6String(peer *nbpeer.Peer) *string { s := peer.IPv6.String() return &s } + +// displayNameForUser returns a human-readable label for the requesting +// user suitable for a VNC approval prompt. Tries the IdP-resolved +// UserInfo first (carries name / email management caches from the +// identity provider) and falls through to JWT claims, then user id. +// Errors from the lookup don't fail the request; we just degrade. +func (h *Handler) displayNameForUser(ctx context.Context, u auth.UserAuth) string { + if info, err := h.accountManager.GetCurrentUserInfo(ctx, u); err == nil && info != nil { + switch { + case info.UserInfo != nil && info.UserInfo.Name != "": + return info.UserInfo.Name + case info.UserInfo != nil && info.UserInfo.Email != "": + return info.UserInfo.Email + } + } else if err != nil { + log.WithContext(ctx).Debugf("display name: GetCurrentUserInfo: %v", err) + } + switch { + case u.PreferredName != "": + return u.PreferredName + case u.Name != "": + return u.Name + case u.Email != "": + return u.Email + } + return u.UserId +} diff --git a/management/server/types/account.go b/management/server/types/account.go index 5f749842e..8110216be 100644 --- a/management/server/types/account.go +++ b/management/server/types/account.go @@ -14,7 +14,7 @@ import ( "github.com/rs/xid" log "github.com/sirupsen/logrus" - "github.com/netbirdio/netbird/client/ssh/auth" + auth "github.com/netbirdio/netbird/shared/sessionauth" nbdns "github.com/netbirdio/netbird/dns" proxydomain "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/domain" "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service" diff --git a/management/server/types/networkmap_components.go b/management/server/types/networkmap_components.go index c4bc8aef6..0b9a2d505 100644 --- a/management/server/types/networkmap_components.go +++ b/management/server/types/networkmap_components.go @@ -9,7 +9,7 @@ import ( "strings" "time" - "github.com/netbirdio/netbird/client/ssh/auth" + auth "github.com/netbirdio/netbird/shared/sessionauth" nbdns "github.com/netbirdio/netbird/dns" resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" diff --git a/management/server/types/policy_authorized_users.go b/management/server/types/policy_authorized_users.go index cd5500bad..672dce3c6 100644 --- a/management/server/types/policy_authorized_users.go +++ b/management/server/types/policy_authorized_users.go @@ -6,7 +6,7 @@ import ( log "github.com/sirupsen/logrus" - "github.com/netbirdio/netbird/client/ssh/auth" + auth "github.com/netbirdio/netbird/shared/sessionauth" nbpeer "github.com/netbirdio/netbird/management/server/peer" ) @@ -28,6 +28,9 @@ type VNCSessionPubKey struct { PubKey string // UserID is the unhashed user identity the pubkey authenticates as. UserID string + // DisplayName is a human-readable label for UserID, used by the host + // peer's approval prompt. Empty when not provided. + DisplayName string } // ruleAuthCallbacks lets Account and NetworkMapComponents share the per-rule @@ -83,8 +86,9 @@ func (cb ruleAuthCallbacks) handleVNCRule(rule *PolicyRule, peerInSources, peerI cb.collectVNCUsers(rule, state.vncAuthorizedUsers) if peerInDestinations && rule.SessionPubKey != "" && rule.AuthorizedUser != "" { state.vncSessionPubKeys = append(state.vncSessionPubKeys, VNCSessionPubKey{ - PubKey: rule.SessionPubKey, - UserID: rule.AuthorizedUser, + PubKey: rule.SessionPubKey, + UserID: rule.AuthorizedUser, + DisplayName: rule.SessionDisplayName, }) } } diff --git a/management/server/types/policyrule.go b/management/server/types/policyrule.go index 054a08266..fdcbd5e6e 100644 --- a/management/server/types/policyrule.go +++ b/management/server/types/policyrule.go @@ -94,6 +94,13 @@ type PolicyRule struct { // AuthorizedUser when the rule was created via temporary-access for a // VNC scope; empty otherwise. SessionPubKey string + + // SessionDisplayName is a human-readable label for the user the + // SessionPubKey was issued to (typically display name, falling back + // to email or user id). The daemon surfaces it on the host's + // per-connection approval prompt so the user being asked can + // recognise who is requesting access. + SessionDisplayName string } // Copy returns a copy of a policy rule @@ -116,6 +123,7 @@ func (pm *PolicyRule) Copy() *PolicyRule { AuthorizedGroups: make(map[string][]string, len(pm.AuthorizedGroups)), AuthorizedUser: pm.AuthorizedUser, SessionPubKey: pm.SessionPubKey, + SessionDisplayName: pm.SessionDisplayName, } copy(rule.Destinations, pm.Destinations) copy(rule.Sources, pm.Sources) @@ -144,7 +152,8 @@ func (pm *PolicyRule) Equal(other *PolicyRule) bool { pm.SourceResource != other.SourceResource || pm.DestinationResource != other.DestinationResource || pm.AuthorizedUser != other.AuthorizedUser || - pm.SessionPubKey != other.SessionPubKey { + pm.SessionPubKey != other.SessionPubKey || + pm.SessionDisplayName != other.SessionDisplayName { return false } diff --git a/shared/management/proto/management.pb.go b/shared/management/proto/management.pb.go index a4a8d17d7..ef029f79f 100644 --- a/shared/management/proto/management.pb.go +++ b/shared/management/proto/management.pb.go @@ -2761,6 +2761,12 @@ type SessionPubKey struct { // UserIDHash is the BLAKE2b-128 hash of the user ID this session // belongs to, matching the entries in VNCAuth.AuthorizedUsers. UserIdHash []byte `protobuf:"bytes,2,opt,name=user_id_hash,json=userIdHash,proto3" json:"user_id_hash,omitempty"` + // DisplayName is a human-readable label for the user this session was + // issued to (typically the IDP display name, falling back to email). + // Used by the host peer's UI in the per-connection approval prompt so + // the user being asked can recognise the requester. May be empty when + // the management server has no user record for the session. + DisplayName string `protobuf:"bytes,3,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` } func (x *SessionPubKey) Reset() { @@ -2809,6 +2815,13 @@ func (x *SessionPubKey) GetUserIdHash() []byte { return nil } +func (x *SessionPubKey) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + // RemotePeerConfig represents a configuration of a remote peer. // The properties are used to configure WireGuard Peers sections type RemotePeerConfig struct { @@ -5091,346 +5104,348 @@ var file_management_proto_rawDesc = []byte{ 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, 0x4a, 0x0a, 0x0d, 0x53, 0x65, 0x73, 0x73, 0x69, + 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x6d, 0x0a, 0x0d, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x75, 0x62, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x20, 0x0a, 0x0c, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x48, - 0x61, 0x73, 0x68, 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, - 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, 0xfb, 0x02, 0x0a, 0x0c, 0x46, 0x69, - 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x1a, 0x0a, 0x06, 0x50, 0x65, - 0x65, 0x72, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 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, 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, - 0x26, 0x0a, 0x0e, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x65, - 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0e, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, - 0x72, 0x65, 0x66, 0x69, 0x78, 0x65, 0x73, 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, - 0x6c, 0x0a, 0x0e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, - 0x79, 0x12, 0x19, 0x0a, 0x15, 0x50, 0x65, 0x65, 0x72, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, - 0x69, 0x74, 0x79, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x10, 0x00, 0x12, 0x20, 0x0a, 0x1c, - 0x50, 0x65, 0x65, 0x72, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x53, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x65, 0x73, 0x10, 0x01, 0x12, 0x1d, - 0x0a, 0x19, 0x50, 0x65, 0x65, 0x72, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, - 0x49, 0x50, 0x76, 0x36, 0x4f, 0x76, 0x65, 0x72, 0x6c, 0x61, 0x79, 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, + 0x61, 0x73, 0x68, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, + 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 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, 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, 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, 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, + 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, 0xfb, 0x02, 0x0a, + 0x0c, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x1a, 0x0a, + 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, + 0x01, 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, 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, 0x26, 0x0a, 0x0e, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x72, 0x65, 0x66, + 0x69, 0x78, 0x65, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0e, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x65, 0x73, 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, 0x6c, 0x0a, 0x0e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, + 0x6c, 0x69, 0x74, 0x79, 0x12, 0x19, 0x0a, 0x15, 0x50, 0x65, 0x65, 0x72, 0x43, 0x61, 0x70, 0x61, + 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x10, 0x00, 0x12, + 0x20, 0x0a, 0x1c, 0x50, 0x65, 0x65, 0x72, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, + 0x79, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x65, 0x73, 0x10, + 0x01, 0x12, 0x1d, 0x0a, 0x19, 0x50, 0x65, 0x65, 0x72, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, + 0x69, 0x74, 0x79, 0x49, 0x50, 0x76, 0x36, 0x4f, 0x76, 0x65, 0x72, 0x6c, 0x61, 0x79, 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, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 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, 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, 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, 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 ( diff --git a/shared/management/proto/management.proto b/shared/management/proto/management.proto index 756cdcbbb..421712ccd 100644 --- a/shared/management/proto/management.proto +++ b/shared/management/proto/management.proto @@ -453,6 +453,13 @@ message SessionPubKey { // UserIDHash is the BLAKE2b-128 hash of the user ID this session // belongs to, matching the entries in VNCAuth.AuthorizedUsers. bytes user_id_hash = 2; + + // DisplayName is a human-readable label for the user this session was + // issued to (typically the IDP display name, falling back to email). + // Used by the host peer's UI in the per-connection approval prompt so + // the user being asked can recognise the requester. May be empty when + // the management server has no user record for the session. + string display_name = 3; } // RemotePeerConfig represents a configuration of a remote peer. diff --git a/client/ssh/auth/auth.go b/shared/sessionauth/auth.go similarity index 85% rename from client/ssh/auth/auth.go rename to shared/sessionauth/auth.go index 782c816af..e438d8831 100644 --- a/client/ssh/auth/auth.go +++ b/shared/sessionauth/auth.go @@ -1,4 +1,4 @@ -package auth +package sessionauth import ( "errors" @@ -43,6 +43,11 @@ type Authorizer struct { // Populated from management's temporary-access flow; used by VNC to // authenticate via the Noise_IK handshake. sessionPubKeys map[[sessionPubKeyLen]byte]sshuserhash.UserIDHash + // sessionDisplayNames mirrors sessionPubKeys with the optional + // human-readable display name management associated with each + // session key. Used by the per-connection UI approval prompt; not + // consulted by any authorization decision. + sessionDisplayNames map[[sessionPubKeyLen]byte]string // mu protects the list of users mu sync.RWMutex @@ -66,10 +71,13 @@ type Config struct { } // SessionPubKey is a single ephemeral-key entry: the 32-byte X25519 -// static public key plus the hashed user identity it authenticates as. +// static public key plus the hashed user identity it authenticates as, +// optionally plus a human-readable display name for the UI approval +// prompt to identify the requester. type SessionPubKey struct { - PubKey []byte - UserIDHash sshuserhash.UserIDHash + PubKey []byte + UserIDHash sshuserhash.UserIDHash + DisplayName string } // NewAuthorizer creates a new SSH authorizer with empty configuration @@ -77,7 +85,8 @@ func NewAuthorizer() *Authorizer { a := &Authorizer{ userIDClaim: DefaultUserIDClaim, machineUsers: make(map[string][]uint32), - sessionPubKeys: make(map[[sessionPubKeyLen]byte]sshuserhash.UserIDHash), + sessionPubKeys: make(map[[sessionPubKeyLen]byte]sshuserhash.UserIDHash), + sessionDisplayNames: make(map[[sessionPubKeyLen]byte]string), } return a @@ -94,6 +103,7 @@ func (a *Authorizer) Update(config *Config) { a.authorizedUsers = []sshuserhash.UserIDHash{} a.machineUsers = make(map[string][]uint32) a.sessionPubKeys = make(map[[sessionPubKeyLen]byte]sshuserhash.UserIDHash) + a.sessionDisplayNames = make(map[[sessionPubKeyLen]byte]string) log.Info("SSH authorization cleared") return } @@ -117,6 +127,7 @@ func (a *Authorizer) Update(config *Config) { a.machineUsers = machineUsers sessionPubKeys := make(map[[sessionPubKeyLen]byte]sshuserhash.UserIDHash, len(config.SessionPubKeys)) + sessionDisplayNames := make(map[[sessionPubKeyLen]byte]string, len(config.SessionPubKeys)) conflicted := make(map[[sessionPubKeyLen]byte]struct{}) for _, e := range config.SessionPubKeys { if len(e.PubKey) != sessionPubKeyLen { @@ -130,12 +141,17 @@ func (a *Authorizer) Update(config *Config) { if existing, ok := sessionPubKeys[key]; ok && existing != e.UserIDHash { log.Warnf("SSH auth: session pubkey bound to conflicting user hashes; dropping binding") delete(sessionPubKeys, key) + delete(sessionDisplayNames, key) conflicted[key] = struct{}{} continue } sessionPubKeys[key] = e.UserIDHash + if e.DisplayName != "" { + sessionDisplayNames[key] = e.DisplayName + } } a.sessionPubKeys = sessionPubKeys + a.sessionDisplayNames = sessionDisplayNames log.Debugf("SSH auth: updated with %d authorized users, %d machine user mappings, %d session pubkeys", len(config.AuthorizedUsers), len(machineUsers), len(sessionPubKeys)) @@ -217,6 +233,22 @@ func (a *Authorizer) LookupSessionKey(pubKey []byte) (sshuserhash.UserIDHash, er return hash, nil } +// LookupSessionDisplayName returns the human-readable display name +// management associated with a session pubkey, or empty string when none +// is recorded. Never returns an error: a missing/unknown key reports as +// "" and the caller falls back to other identifiers. +func (a *Authorizer) LookupSessionDisplayName(pubKey []byte) string { + if len(pubKey) != sessionPubKeyLen { + return "" + } + var key [sessionPubKeyLen]byte + copy(key[:], pubKey) + a.mu.RLock() + name := a.sessionDisplayNames[key] + a.mu.RUnlock() + return name +} + // AuthorizeOSUserBySessionKey resolves the OS-user mapping for a session // key. Mirrors Authorize but skips the JWT-hash step since the key has // already been verified and the user identity hash is in hand. diff --git a/client/ssh/auth/auth_test.go b/shared/sessionauth/auth_test.go similarity index 99% rename from client/ssh/auth/auth_test.go rename to shared/sessionauth/auth_test.go index 87047bb2b..6c5395f4e 100644 --- a/client/ssh/auth/auth_test.go +++ b/shared/sessionauth/auth_test.go @@ -1,4 +1,4 @@ -package auth +package sessionauth import ( "errors"