From 20e59e0797bc2a96190dd6f0d24c5210de634d8b Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 26 Jun 2024 16:22:24 +0100 Subject: [PATCH 01/14] ci: test with multiple postgres versions (#13665) - Tests now run on postgres 16 by default when run locally (can be specified with POSTGRES_VERSION) - Adds test-go-pg-16 to test against postgres version 16 - Updates Dogfood dockerfile / nix flake to postgres version 16 - Updates docker-compose.yaml postgres tag to 16 --- .github/workflows/ci.yaml | 44 ++++++++++++++++++++++++++++++++++++++- Makefile | 7 ++++--- docker-compose.yaml | 4 +++- dogfood/Dockerfile | 6 +++--- flake.nix | 4 ++-- 5 files changed, 55 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 59496b017d886..bcb58924e7cba 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -354,8 +354,50 @@ jobs: uses: ./.github/actions/setup-tf - name: Test with PostgreSQL Database + env: + POSTGRES_VERSION: "13" + TS_DEBUG_DISCO: "true" + run: | + make test-postgres + + - name: Upload test stats to Datadog + timeout-minutes: 1 + continue-on-error: true + uses: ./.github/actions/upload-datadog + if: success() || failure() + with: + api-key: ${{ secrets.DATADOG_API_KEY }} + + # NOTE: this could instead be defined as a matrix strategy, but we want to + # only block merging if tests on postgres 13 fail. Using a matrix strategy + # here makes the check in the above `required` job rather complicated. + test-go-pg-16: + runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }} + needs: + - changes + if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main' + # This timeout must be greater than the timeout set by `go test` in + # `make test-postgres` to ensure we receive a trace of running + # goroutines. Setting this to the timeout +5m should work quite well + # even if some of the preceding steps are slow. + timeout-minutes: 25 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Setup Go + uses: ./.github/actions/setup-go + + - name: Setup Terraform + uses: ./.github/actions/setup-tf + + - name: Test with PostgreSQL Database + env: + POSTGRES_VERSION: "16" + TS_DEBUG_DISCO: "true" run: | - export TS_DEBUG_DISCO=true make test-postgres - name: Upload test stats to Datadog diff --git a/Makefile b/Makefile index 75c5a945e113b..c3059800c7515 100644 --- a/Makefile +++ b/Makefile @@ -36,6 +36,7 @@ GOOS := $(shell go env GOOS) GOARCH := $(shell go env GOARCH) GOOS_BIN_EXT := $(if $(filter windows, $(GOOS)),.exe,) VERSION := $(shell ./scripts/version.sh) +POSTGRES_VERSION ?= 16 # Use the highest ZSTD compression level in CI. ifdef CI @@ -814,7 +815,7 @@ test-migrations: test-postgres-docker # NOTE: we set --memory to the same size as a GitHub runner. test-postgres-docker: - docker rm -f test-postgres-docker || true + docker rm -f test-postgres-docker-${POSTGRES_VERSION} || true docker run \ --env POSTGRES_PASSWORD=postgres \ --env POSTGRES_USER=postgres \ @@ -822,11 +823,11 @@ test-postgres-docker: --env PGDATA=/tmp \ --tmpfs /tmp \ --publish 5432:5432 \ - --name test-postgres-docker \ + --name test-postgres-docker-${POSTGRES_VERSION} \ --restart no \ --detach \ --memory 16GB \ - gcr.io/coder-dev-1/postgres:13 \ + gcr.io/coder-dev-1/postgres:${POSTGRES_VERSION} \ -c shared_buffers=1GB \ -c work_mem=1GB \ -c effective_cache_size=1GB \ diff --git a/docker-compose.yaml b/docker-compose.yaml index 9b41c5f47ae61..58692aa73e1f1 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -25,7 +25,9 @@ services: database: condition: service_healthy database: - image: "postgres:14.2" + # Minimum supported version is 13. + # More versions here: https://hub.docker.com/_/postgres + image: "postgres:16" ports: - "5432:5432" environment: diff --git a/dogfood/Dockerfile b/dogfood/Dockerfile index 8bfdb4d2bdb0b..57520ddef67a7 100644 --- a/dogfood/Dockerfile +++ b/dogfood/Dockerfile @@ -146,7 +146,7 @@ RUN apt-get update --quiet && apt-get install --yes \ openssl \ packer \ pkg-config \ - postgresql-13 \ + postgresql-16 \ python3 \ python3-pip \ rsync \ @@ -209,8 +209,8 @@ RUN apt-get update && \ npm cache clean --force # Ensure PostgreSQL binaries are in the users $PATH. -RUN update-alternatives --install /usr/local/bin/initdb initdb /usr/lib/postgresql/13/bin/initdb 100 && \ - update-alternatives --install /usr/local/bin/postgres postgres /usr/lib/postgresql/13/bin/postgres 100 +RUN update-alternatives --install /usr/local/bin/initdb initdb /usr/lib/postgresql/16/bin/initdb 100 && \ + update-alternatives --install /usr/local/bin/postgres postgres /usr/lib/postgresql/16/bin/postgres 100 # Create links for injected dependencies RUN ln --symbolic /var/tmp/coder/coder-cli/coder /usr/local/bin/coder && \ diff --git a/flake.nix b/flake.nix index ca31783e36045..ee6fbca7bd923 100644 --- a/flake.nix +++ b/flake.nix @@ -59,7 +59,7 @@ pixman pkg-config playwright-driver.browsers - postgresql_13 + postgresql_16 protobuf protoc-gen-go ripgrep @@ -97,7 +97,7 @@ name = "coder-${osArch}"; # Updated with ./scripts/update-flake.sh`. # This should be updated whenever go.mod changes! - vendorHash = "sha256-+K95kbYNMKiYk7obN2gjNCtBvaXBWPLKm12N6cF9ImQ="; + vendorHash = "sha256-e0L6osJwG0EF0M3TefxaAjDvN4jvQHxTGEUEECNO1Vw="; proxyVendor = true; src = ./.; nativeBuildInputs = with pkgs; [ getopt openssl zstd ]; From e8e0064ab758397bb25d2270be83e1e94ce87829 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Wed, 26 Jun 2024 15:26:07 +0000 Subject: [PATCH 02/14] chore: add DRPC tailnet & cli implementation --- cli/ping.go | 3 + cli/portforward.go | 3 + cli/root.go | 37 ++++++---- cli/speedtest.go | 3 + cli/ssh.go | 5 +- cli/testdata/coder_--help.golden | 3 + codersdk/workspacesdk/connector.go | 73 +++++++++++++------ .../workspacesdk/connector_internal_test.go | 3 +- codersdk/workspacesdk/workspacesdk.go | 63 +++++++++------- docs/cli.md | 9 +++ enterprise/cli/testdata/coder_--help.golden | 3 + tailnet/conn.go | 18 +++++ 12 files changed, 155 insertions(+), 68 deletions(-) diff --git a/cli/ping.go b/cli/ping.go index 82becb016bde7..ff6c318cc1b84 100644 --- a/cli/ping.go +++ b/cli/ping.go @@ -58,6 +58,9 @@ func (r *RootCmd) ping() *serpent.Command { _, _ = fmt.Fprintln(inv.Stderr, "Direct connections disabled.") opts.BlockEndpoints = true } + if r.noNetworkTelemetry { + opts.EnableTelemetry = true + } conn, err := workspacesdk.New(client).DialAgent(ctx, workspaceAgent.ID, opts) if err != nil { return err diff --git a/cli/portforward.go b/cli/portforward.go index 4c0b1d772eecc..bef0bb6f12be1 100644 --- a/cli/portforward.go +++ b/cli/portforward.go @@ -106,6 +106,9 @@ func (r *RootCmd) portForward() *serpent.Command { _, _ = fmt.Fprintln(inv.Stderr, "Direct connections disabled.") opts.BlockEndpoints = true } + if r.noNetworkTelemetry { + opts.EnableTelemetry = true + } conn, err := workspacesdk.New(client).DialAgent(ctx, workspaceAgent.ID, opts) if err != nil { return err diff --git a/cli/root.go b/cli/root.go index 073486c640744..ac4bd19d74568 100644 --- a/cli/root.go +++ b/cli/root.go @@ -52,20 +52,21 @@ var ( ) const ( - varURL = "url" - varToken = "token" - varAgentToken = "agent-token" - varAgentTokenFile = "agent-token-file" - varAgentURL = "agent-url" - varHeader = "header" - varHeaderCommand = "header-command" - varNoOpen = "no-open" - varNoVersionCheck = "no-version-warning" - varNoFeatureWarning = "no-feature-warning" - varForceTty = "force-tty" - varVerbose = "verbose" - varOrganizationSelect = "organization" - varDisableDirect = "disable-direct-connections" + varURL = "url" + varToken = "token" + varAgentToken = "agent-token" + varAgentTokenFile = "agent-token-file" + varAgentURL = "agent-url" + varHeader = "header" + varHeaderCommand = "header-command" + varNoOpen = "no-open" + varNoVersionCheck = "no-version-warning" + varNoFeatureWarning = "no-feature-warning" + varForceTty = "force-tty" + varVerbose = "verbose" + varOrganizationSelect = "organization" + varDisableDirect = "disable-direct-connections" + varDisableNetworkTelemetry = "disable-network-telemetry" notLoggedInMessage = "You are not logged in. Try logging in using 'coder login '." @@ -436,6 +437,13 @@ func (r *RootCmd) Command(subcommands []*serpent.Command) (*serpent.Command, err Value: serpent.BoolOf(&r.disableDirect), Group: globalGroup, }, + { + Flag: varDisableNetworkTelemetry, + Env: "CODER_DISABLE_NETWORK_TELEMETRY", + Description: "Disable network telemetry.", + Value: serpent.BoolOf(&r.noNetworkTelemetry), + Group: globalGroup, + }, { Flag: "debug-http", Description: "Debug codersdk HTTP requests.", @@ -491,6 +499,7 @@ type RootCmd struct { versionFlag bool disableDirect bool debugHTTP bool + noNetworkTelemetry bool noVersionCheck bool noFeatureWarning bool diff --git a/cli/speedtest.go b/cli/speedtest.go index 42fe7604c6dc4..3f34e5796f66f 100644 --- a/cli/speedtest.go +++ b/cli/speedtest.go @@ -102,6 +102,9 @@ func (r *RootCmd) speedtest() *serpent.Command { _, _ = fmt.Fprintln(inv.Stderr, "Direct connections disabled.") opts.BlockEndpoints = true } + if r.noNetworkTelemetry { + opts.EnableTelemetry = true + } if pcapFile != "" { s := capture.New() opts.CaptureHook = s.LogPacket diff --git a/cli/ssh.go b/cli/ssh.go index e4e9fadf5e8e8..050c7d7fe48c3 100644 --- a/cli/ssh.go +++ b/cli/ssh.go @@ -243,8 +243,9 @@ func (r *RootCmd) ssh() *serpent.Command { } conn, err := workspacesdk.New(client). DialAgent(ctx, workspaceAgent.ID, &workspacesdk.DialAgentOptions{ - Logger: logger, - BlockEndpoints: r.disableDirect, + Logger: logger, + BlockEndpoints: r.disableDirect, + EnableTelemetry: !r.noNetworkTelemetry, }) if err != nil { return xerrors.Errorf("dial agent: %w", err) diff --git a/cli/testdata/coder_--help.golden b/cli/testdata/coder_--help.golden index e970347890eb2..294010e7678e6 100644 --- a/cli/testdata/coder_--help.golden +++ b/cli/testdata/coder_--help.golden @@ -66,6 +66,9 @@ variables or flags. --disable-direct-connections bool, $CODER_DISABLE_DIRECT_CONNECTIONS Disable direct (P2P) connections to workspaces. + --disable-network-telemetry bool, $CODER_DISABLE_NETWORK_TELEMETRY + Disable network telemetry. + --global-config string, $CODER_CONFIG_DIR (default: ~/.config/coderv2) Path to the global `coder` config directory. diff --git a/codersdk/workspacesdk/connector.go b/codersdk/workspacesdk/connector.go index d6349adaf6b40..3b013120360bd 100644 --- a/codersdk/workspacesdk/connector.go +++ b/codersdk/workspacesdk/connector.go @@ -55,32 +55,28 @@ type tailnetAPIConnector struct { coordinateURL string dialOptions *websocket.DialOptions conn tailnetConn + customDialFn func() (proto.DRPCTailnetClient, error) + + clientMu sync.RWMutex + client proto.DRPCTailnetClient connected chan error isFirst bool closed chan struct{} } -// runTailnetAPIConnector creates and runs a tailnetAPIConnector -func runTailnetAPIConnector( - ctx context.Context, logger slog.Logger, - agentID uuid.UUID, coordinateURL string, dialOptions *websocket.DialOptions, - conn tailnetConn, -) *tailnetAPIConnector { - tac := &tailnetAPIConnector{ +// Create a new tailnetAPIConnector without running it +func newTailnetAPIConnector(ctx context.Context, logger slog.Logger, agentID uuid.UUID, coordinateURL string, dialOptions *websocket.DialOptions) *tailnetAPIConnector { + return &tailnetAPIConnector{ ctx: ctx, logger: logger, agentID: agentID, coordinateURL: coordinateURL, dialOptions: dialOptions, - conn: conn, + conn: nil, connected: make(chan error, 1), closed: make(chan struct{}), } - tac.gracefulCtx, tac.cancelGracefulCtx = context.WithCancel(context.Background()) - go tac.manageGracefulTimeout() - go tac.run() - return tac } // manageGracefulTimeout allows the gracefulContext to last 1 second longer than the main context @@ -96,21 +92,34 @@ func (tac *tailnetAPIConnector) manageGracefulTimeout() { } } -func (tac *tailnetAPIConnector) run() { - tac.isFirst = true - defer close(tac.closed) - for retrier := retry.New(50*time.Millisecond, 10*time.Second); retrier.Wait(tac.ctx); { - tailnetClient, err := tac.dial() - if err != nil { - continue +// Runs a tailnetAPIConnector using the provided connection +func (tac *tailnetAPIConnector) runConnector(conn tailnetConn) { + tac.conn = conn + tac.gracefulCtx, tac.cancelGracefulCtx = context.WithCancel(context.Background()) + go tac.manageGracefulTimeout() + go func() { + tac.isFirst = true + defer close(tac.closed) + for retrier := retry.New(50*time.Millisecond, 10*time.Second); retrier.Wait(tac.ctx); { + tailnetClient, err := tac.dial() + if err != nil { + continue + } + tac.clientMu.Lock() + tac.client = tailnetClient + tac.clientMu.Unlock() + tac.logger.Debug(tac.ctx, "obtained tailnet API v2+ client") + tac.coordinateAndDERPMap(tailnetClient) + tac.logger.Debug(tac.ctx, "tailnet API v2+ connection lost") } - tac.logger.Debug(tac.ctx, "obtained tailnet API v2+ client") - tac.coordinateAndDERPMap(tailnetClient) - tac.logger.Debug(tac.ctx, "tailnet API v2+ connection lost") - } + }() } func (tac *tailnetAPIConnector) dial() (proto.DRPCTailnetClient, error) { + if tac.customDialFn != nil { + return tac.customDialFn() + } + tac.logger.Debug(tac.ctx, "dialing Coder tailnet v2+ API") // nolint:bodyclose ws, res, err := websocket.Dial(tac.ctx, tac.coordinateURL, tac.dialOptions) @@ -172,7 +181,10 @@ func (tac *tailnetAPIConnector) coordinateAndDERPMap(client proto.DRPCTailnetCli // we do NOT want to gracefully disconnect on the coordinate() routine. So, we'll just // close the underlying connection. This will trigger a retry of the control plane in // run(). + tac.clientMu.Lock() client.DRPCConn().Close() + tac.client = nil + tac.clientMu.Unlock() // Note that derpMap() logs it own errors, we don't bother here. } }() @@ -236,3 +248,18 @@ func (tac *tailnetAPIConnector) derpMap(client proto.DRPCTailnetClient) error { tac.conn.SetDERPMap(dm) } } + +func (tac *tailnetAPIConnector) SendTelemetryEvent(event *proto.TelemetryEvent) { + tac.clientMu.RLock() + // We hold the lock for the entire telemetry request, but this would only block + // a coordinate retry, and closing the connection. + defer tac.clientMu.RUnlock() + if tac.client == nil { + return + } + ctx, cancel := context.WithTimeout(tac.ctx, 5*time.Second) + defer cancel() + _, _ = tac.client.PostTelemetry(ctx, &proto.TelemetryRequest{ + Events: []*proto.TelemetryEvent{event}, + }) +} diff --git a/codersdk/workspacesdk/connector_internal_test.go b/codersdk/workspacesdk/connector_internal_test.go index 0b75e460ad669..24d2204f7bcb7 100644 --- a/codersdk/workspacesdk/connector_internal_test.go +++ b/codersdk/workspacesdk/connector_internal_test.go @@ -70,7 +70,8 @@ func TestTailnetAPIConnector_Disconnects(t *testing.T) { fConn := newFakeTailnetConn() - uut := runTailnetAPIConnector(ctx, logger, agentID, svr.URL, &websocket.DialOptions{}, fConn) + uut := newTailnetAPIConnector(ctx, logger, agentID, svr.URL, &websocket.DialOptions{}) + uut.runConnector(fConn) call := testutil.RequireRecvCtx(ctx, t, fCoord.CoordinateCalls) reqTun := testutil.RequireRecvCtx(ctx, t, call.Reqs) diff --git a/codersdk/workspacesdk/workspacesdk.go b/codersdk/workspacesdk/workspacesdk.go index f1e3bd67ea3dc..ec159480d4a64 100644 --- a/codersdk/workspacesdk/workspacesdk.go +++ b/codersdk/workspacesdk/workspacesdk.go @@ -180,6 +180,9 @@ type DialAgentOptions struct { // CaptureHook is a callback that captures Disco packets and packets sent // into the tailnet tunnel. CaptureHook capture.Callback + // Whether the client will send network telemetry events + // Enable instead of Disable so it's initialized to false + EnableTelemetry bool } func (c *Client) DialAgent(dialCtx context.Context, agentID uuid.UUID, options *DialAgentOptions) (agentConn *AgentConn, err error) { @@ -195,29 +198,6 @@ func (c *Client) DialAgent(dialCtx context.Context, agentID uuid.UUID, options * options.BlockEndpoints = true } - ip := tailnet.IP() - var header http.Header - if headerTransport, ok := c.client.HTTPClient.Transport.(*codersdk.HeaderTransport); ok { - header = headerTransport.Header - } - conn, err := tailnet.NewConn(&tailnet.Options{ - Addresses: []netip.Prefix{netip.PrefixFrom(ip, 128)}, - DERPMap: connInfo.DERPMap, - DERPHeader: &header, - DERPForceWebSockets: connInfo.DERPForceWebSockets, - Logger: options.Logger, - BlockEndpoints: c.client.DisableDirectConnections || options.BlockEndpoints, - CaptureHook: options.CaptureHook, - }) - if err != nil { - return nil, xerrors.Errorf("create tailnet: %w", err) - } - defer func() { - if err != nil { - _ = conn.Close() - } - }() - headers := make(http.Header) tokenHeader := codersdk.SessionTokenHeader if c.client.SessionTokenHeader != "" { @@ -242,16 +222,43 @@ func (c *Client) DialAgent(dialCtx context.Context, agentID uuid.UUID, options * q.Add("version", proto.CurrentVersion.String()) coordinateURL.RawQuery = q.Encode() - connector := runTailnetAPIConnector(ctx, options.Logger, - agentID, coordinateURL.String(), + connector := newTailnetAPIConnector(ctx, options.Logger, agentID, coordinateURL.String(), &websocket.DialOptions{ HTTPClient: c.client.HTTPClient, HTTPHeader: headers, // Need to disable compression to avoid a data-race. CompressionMode: websocket.CompressionDisabled, - }, - conn, - ) + }) + + ip := tailnet.IP() + var header http.Header + if headerTransport, ok := c.client.HTTPClient.Transport.(*codersdk.HeaderTransport); ok { + header = headerTransport.Header + } + var telemetrySink tailnet.TelemetrySink + if options.EnableTelemetry { + telemetrySink = connector + } + conn, err := tailnet.NewConn(&tailnet.Options{ + Addresses: []netip.Prefix{netip.PrefixFrom(ip, 128)}, + DERPMap: connInfo.DERPMap, + DERPHeader: &header, + DERPForceWebSockets: connInfo.DERPForceWebSockets, + Logger: options.Logger, + BlockEndpoints: c.client.DisableDirectConnections || options.BlockEndpoints, + CaptureHook: options.CaptureHook, + TelemetrySink: telemetrySink, + }) + if err != nil { + return nil, xerrors.Errorf("create tailnet: %w", err) + } + defer func() { + if err != nil { + _ = conn.Close() + } + }() + connector.runConnector(conn) + options.Logger.Debug(ctx, "running tailnet API v2+ connector") select { diff --git a/docs/cli.md b/docs/cli.md index 70dd29e28b9da..2962425499040 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -149,6 +149,15 @@ Enable verbose output. Disable direct (P2P) connections to workspaces. +### --disable-network-telemetry + +| | | +| ----------- | --------------------------------------------- | +| Type | bool | +| Environment | $CODER_DISABLE_NETWORK_TELEMETRY | + +Disable network telemetry. + ### --global-config | | | diff --git a/enterprise/cli/testdata/coder_--help.golden b/enterprise/cli/testdata/coder_--help.golden index 7c2ff5c835dff..6895cdde3c9f4 100644 --- a/enterprise/cli/testdata/coder_--help.golden +++ b/enterprise/cli/testdata/coder_--help.golden @@ -30,6 +30,9 @@ variables or flags. --disable-direct-connections bool, $CODER_DISABLE_DIRECT_CONNECTIONS Disable direct (P2P) connections to workspaces. + --disable-network-telemetry bool, $CODER_DISABLE_NETWORK_TELEMETRY + Disable network telemetry. + --global-config string, $CODER_CONFIG_DIR (default: ~/.config/coderv2) Path to the global `coder` config directory. diff --git a/tailnet/conn.go b/tailnet/conn.go index 8b82c455e4788..3a2843539ed04 100644 --- a/tailnet/conn.go +++ b/tailnet/conn.go @@ -99,6 +99,15 @@ type Options struct { // ForceNetworkUp forces the network to be considered up. magicsock will not // do anything if it thinks it can't reach the internet. ForceNetworkUp bool + // TelemetrySink is optional. + TelemetrySink TelemetrySink +} + +// TelemetrySink allows tailnet.Conn to send network telemetry to the Coder +// server. +type TelemetrySink interface { + // SendTelemetryEvent sends a telemetry event to some external sink. + SendTelemetryEvent(event *proto.TelemetryEvent) } // NodeID creates a Tailscale NodeID from the last 8 bytes of a UUID. It ensures @@ -113,6 +122,13 @@ func NodeID(uid uuid.UUID) tailcfg.NodeID { return tailcfg.NodeID(id) } +func (c *Conn) sendTelemetryEvent(e *proto.TelemetryEvent) { + if c.telemetrySink == nil { + return + } + c.telemetrySink.SendTelemetryEvent(e) +} + // NewConn constructs a new Wireguard server that will accept connections from the addresses provided. func NewConn(options *Options) (conn *Conn, err error) { if options == nil { @@ -259,6 +275,7 @@ func NewConn(options *Options) (conn *Conn, err error) { wireguardEngine: wireguardEngine, configMaps: cfgMaps, nodeUpdater: nodeUp, + telemetrySink: options.TelemetrySink, } defer func() { if err != nil { @@ -316,6 +333,7 @@ type Conn struct { wireguardRouter *router.Config wireguardEngine wgengine.Engine listeners map[listenKey]*listener + telemetrySink TelemetrySink trafficStats *connstats.Statistics } From 381927422d185938ca286066386da35c19f2c7d6 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Wed, 26 Jun 2024 15:43:41 +0000 Subject: [PATCH 03/14] fixup --- codersdk/workspacesdk/connector.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/codersdk/workspacesdk/connector.go b/codersdk/workspacesdk/connector.go index 3b013120360bd..e29a0229902d6 100644 --- a/codersdk/workspacesdk/connector.go +++ b/codersdk/workspacesdk/connector.go @@ -35,6 +35,7 @@ type tailnetConn interface { // // 1) run the Coordinate API and pass node information back and forth // 2) stream DERPMap updates and program the Conn +// 3) Send network telemetry events // // These functions share the same websocket, and so are combined here so that if we hit a problem // we tear the whole thing down and start over with a new websocket. @@ -55,7 +56,6 @@ type tailnetAPIConnector struct { coordinateURL string dialOptions *websocket.DialOptions conn tailnetConn - customDialFn func() (proto.DRPCTailnetClient, error) clientMu sync.RWMutex client proto.DRPCTailnetClient @@ -116,10 +116,6 @@ func (tac *tailnetAPIConnector) runConnector(conn tailnetConn) { } func (tac *tailnetAPIConnector) dial() (proto.DRPCTailnetClient, error) { - if tac.customDialFn != nil { - return tac.customDialFn() - } - tac.logger.Debug(tac.ctx, "dialing Coder tailnet v2+ API") // nolint:bodyclose ws, res, err := websocket.Dial(tac.ctx, tac.coordinateURL, tac.dialOptions) From 5b1e1f9cb5d875c46ff9181593e4413497c72ba9 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Wed, 26 Jun 2024 15:49:34 +0000 Subject: [PATCH 04/14] fixup --- cli/ping.go | 2 +- cli/portforward.go | 2 +- cli/speedtest.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/ping.go b/cli/ping.go index ff6c318cc1b84..4d4e9308909dc 100644 --- a/cli/ping.go +++ b/cli/ping.go @@ -58,7 +58,7 @@ func (r *RootCmd) ping() *serpent.Command { _, _ = fmt.Fprintln(inv.Stderr, "Direct connections disabled.") opts.BlockEndpoints = true } - if r.noNetworkTelemetry { + if !r.noNetworkTelemetry { opts.EnableTelemetry = true } conn, err := workspacesdk.New(client).DialAgent(ctx, workspaceAgent.ID, opts) diff --git a/cli/portforward.go b/cli/portforward.go index bef0bb6f12be1..2a0126032e3bb 100644 --- a/cli/portforward.go +++ b/cli/portforward.go @@ -106,7 +106,7 @@ func (r *RootCmd) portForward() *serpent.Command { _, _ = fmt.Fprintln(inv.Stderr, "Direct connections disabled.") opts.BlockEndpoints = true } - if r.noNetworkTelemetry { + if !r.noNetworkTelemetry { opts.EnableTelemetry = true } conn, err := workspacesdk.New(client).DialAgent(ctx, workspaceAgent.ID, opts) diff --git a/cli/speedtest.go b/cli/speedtest.go index 3f34e5796f66f..9f23d0c58f780 100644 --- a/cli/speedtest.go +++ b/cli/speedtest.go @@ -102,7 +102,7 @@ func (r *RootCmd) speedtest() *serpent.Command { _, _ = fmt.Fprintln(inv.Stderr, "Direct connections disabled.") opts.BlockEndpoints = true } - if r.noNetworkTelemetry { + if !r.noNetworkTelemetry { opts.EnableTelemetry = true } if pcapFile != "" { From 08e728bcb2c9c708f4940fdc61c0eea73052990a Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 26 Jun 2024 07:38:46 -1000 Subject: [PATCH 05/14] chore: implement organization scoped audit log requests (#13663) * chore: add organization_id filter to audit logs * chore: implement organization scoped audit log requests --- coderd/apidoc/docs.go | 4 +++ coderd/apidoc/swagger.json | 4 +++ coderd/audit.go | 10 +++++- coderd/audit_test.go | 50 +++++++++++++++++++++++++++ coderd/database/dbauthz/dbauthz.go | 17 ++++++--- coderd/database/dbmem/dbmem.go | 3 ++ coderd/database/queries.sql.go | 40 ++++++++++++--------- coderd/database/queries/auditlogs.sql | 6 ++++ coderd/members.go | 9 ++--- coderd/searchquery/search.go | 1 + codersdk/audit.go | 1 + docs/api/schemas.md | 2 ++ site/src/api/typesGenerated.ts | 1 + 13 files changed, 123 insertions(+), 25 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 7ff97bba2968d..50f31bfaf6ba6 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -8719,6 +8719,10 @@ const docTemplate = `{ } ] }, + "organization_id": { + "type": "string", + "format": "uuid" + }, "resource_id": { "type": "string", "format": "uuid" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index bc6fcc19142a9..e04fc83f88653 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7762,6 +7762,10 @@ } ] }, + "organization_id": { + "type": "string", + "format": "uuid" + }, "resource_id": { "type": "string", "format": "uuid" diff --git a/coderd/audit.go b/coderd/audit.go index a818f681038ed..37966ead7e916 100644 --- a/coderd/audit.go +++ b/coderd/audit.go @@ -18,6 +18,7 @@ import ( "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/db2sdk" + "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/coderd/searchquery" @@ -61,6 +62,10 @@ func (api *API) auditLogs(rw http.ResponseWriter, r *http.Request) { } dblogs, err := api.Database.GetAuditLogsOffset(ctx, filter) + if dbauthz.IsNotAuthorizedError(err) { + httpapi.Forbidden(rw) + return + } if err != nil { httpapi.InternalServerError(rw, err) return @@ -139,6 +144,9 @@ func (api *API) generateFakeAuditLog(rw http.ResponseWriter, r *http.Request) { if len(params.AdditionalFields) == 0 { params.AdditionalFields = json.RawMessage("{}") } + if params.OrganizationID == uuid.Nil { + params.OrganizationID = uuid.New() + } _, err = api.Database.InsertAuditLog(ctx, database.InsertAuditLogParams{ ID: uuid.New(), @@ -155,7 +163,7 @@ func (api *API) generateFakeAuditLog(rw http.ResponseWriter, r *http.Request) { AdditionalFields: params.AdditionalFields, RequestID: uuid.Nil, // no request ID to attach this to ResourceIcon: "", - OrganizationID: uuid.New(), + OrganizationID: params.OrganizationID, }) if err != nil { httpapi.InternalServerError(rw, err) diff --git a/coderd/audit_test.go b/coderd/audit_test.go index 9de6f65071a02..198b792c44ba8 100644 --- a/coderd/audit_test.go +++ b/coderd/audit_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "net/http" "strconv" "testing" "time" @@ -11,6 +12,7 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/require" + "cdr.dev/slog/sloggers/slogtest" "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" @@ -135,6 +137,54 @@ func TestAuditLogs(t *testing.T) { require.Equal(t, auditLogs.AuditLogs[0].ResourceLink, fmt.Sprintf("/@%s/%s/builds/%s", workspace.OwnerName, workspace.Name, buildNumberString)) }) + + t.Run("Organization", func(t *testing.T) { + t.Parallel() + + logger := slogtest.Make(t, &slogtest.Options{ + IgnoreErrors: true, + }) + ctx := context.Background() + client := coderdtest.New(t, &coderdtest.Options{ + Logger: &logger, + }) + owner := coderdtest.CreateFirstUser(t, client) + orgAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.ScopedRoleOrgAdmin(owner.OrganizationID)) + + err := client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ + ResourceID: owner.UserID, + OrganizationID: owner.OrganizationID, + }) + require.NoError(t, err) + + // Add an extra audit log in another organization + err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ + ResourceID: owner.UserID, + OrganizationID: uuid.New(), + }) + require.NoError(t, err) + + // Fetching audit logs without an organization selector should fail + _, err = orgAdmin.AuditLogs(ctx, codersdk.AuditLogsRequest{ + Pagination: codersdk.Pagination{ + Limit: 5, + }, + }) + var sdkError *codersdk.Error + require.Error(t, err) + require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error") + require.Equal(t, http.StatusForbidden, sdkError.StatusCode()) + + // Using the organization selector allows the org admin to fetch audit logs + alogs, err := orgAdmin.AuditLogs(ctx, codersdk.AuditLogsRequest{ + SearchQuery: fmt.Sprintf("organization_id:%s", owner.OrganizationID.String()), + Pagination: codersdk.Pagination{ + Limit: 5, + }, + }) + require.NoError(t, err) + require.Len(t, alogs.AuditLogs, 1) + }) } func TestAuditLogsFilter(t *testing.T) { diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index b0b4adcf844c9..cc638cceb9dab 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1200,12 +1200,21 @@ func (q *querier) GetApplicationName(ctx context.Context) (string, error) { } func (q *querier) GetAuditLogsOffset(ctx context.Context, arg database.GetAuditLogsOffsetParams) ([]database.GetAuditLogsOffsetRow, error) { - // To optimize audit logs, we only check the global audit log permission once. - // This is because we expect a large unbounded set of audit logs, and applying a SQL - // filter would slow down the query for no benefit. - if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAuditLog); err != nil { + // To optimize the authz checks for audit logs, do not run an authorize + // check on each individual audit log row. In practice, audit logs are either + // fetched from a global or an organization scope. + // Applying a SQL filter would slow down the query for no benefit on how this query is + // actually used. + + object := rbac.ResourceAuditLog + if arg.OrganizationID != uuid.Nil { + object = object.InOrg(arg.OrganizationID) + } + + if err := q.authorizeContext(ctx, policy.ActionRead, object); err != nil { return nil, err } + return q.db.GetAuditLogsOffset(ctx, arg) } diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index d2d60c3bb6541..830ce9e7241f4 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -1928,6 +1928,9 @@ func (q *FakeQuerier) GetAuditLogsOffset(_ context.Context, arg database.GetAudi arg.Offset-- continue } + if arg.OrganizationID != uuid.Nil && arg.OrganizationID != alog.OrganizationID { + continue + } if arg.Action != "" && !strings.Contains(string(alog.Action), arg.Action) { continue } diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 2e1c1ad744978..3902b2e5f8461 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -500,52 +500,58 @@ WHERE resource_id = $4 ELSE true END + -- Filter organization_id + AND CASE + WHEN $5 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN + audit_logs.organization_id = $5 + ELSE true + END -- Filter by resource_target AND CASE - WHEN $5 :: text != '' THEN - resource_target = $5 + WHEN $6 :: text != '' THEN + resource_target = $6 ELSE true END -- Filter action AND CASE - WHEN $6 :: text != '' THEN - action = $6 :: audit_action + WHEN $7 :: text != '' THEN + action = $7 :: audit_action ELSE true END -- Filter by user_id AND CASE - WHEN $7 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN - user_id = $7 + WHEN $8 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN + user_id = $8 ELSE true END -- Filter by username AND CASE - WHEN $8 :: text != '' THEN - user_id = (SELECT id FROM users WHERE lower(username) = lower($8) AND deleted = false) + WHEN $9 :: text != '' THEN + user_id = (SELECT id FROM users WHERE lower(username) = lower($9) AND deleted = false) ELSE true END -- Filter by user_email AND CASE - WHEN $9 :: text != '' THEN - users.email = $9 + WHEN $10 :: text != '' THEN + users.email = $10 ELSE true END -- Filter by date_from AND CASE - WHEN $10 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN - "time" >= $10 + WHEN $11 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN + "time" >= $11 ELSE true END -- Filter by date_to AND CASE - WHEN $11 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN - "time" <= $11 + WHEN $12 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN + "time" <= $12 ELSE true END -- Filter by build_reason AND CASE - WHEN $12::text != '' THEN - workspace_builds.reason::text = $12 + WHEN $13::text != '' THEN + workspace_builds.reason::text = $13 ELSE true END ORDER BY @@ -561,6 +567,7 @@ type GetAuditLogsOffsetParams struct { Offset int32 `db:"offset" json:"offset"` ResourceType string `db:"resource_type" json:"resource_type"` ResourceID uuid.UUID `db:"resource_id" json:"resource_id"` + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` ResourceTarget string `db:"resource_target" json:"resource_target"` Action string `db:"action" json:"action"` UserID uuid.UUID `db:"user_id" json:"user_id"` @@ -611,6 +618,7 @@ func (q *sqlQuerier) GetAuditLogsOffset(ctx context.Context, arg GetAuditLogsOff arg.Offset, arg.ResourceType, arg.ResourceID, + arg.OrganizationID, arg.ResourceTarget, arg.Action, arg.UserID, diff --git a/coderd/database/queries/auditlogs.sql b/coderd/database/queries/auditlogs.sql index d05b5bbe371e0..653082bd69fee 100644 --- a/coderd/database/queries/auditlogs.sql +++ b/coderd/database/queries/auditlogs.sql @@ -59,6 +59,12 @@ WHERE resource_id = @resource_id ELSE true END + -- Filter organization_id + AND CASE + WHEN @organization_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN + audit_logs.organization_id = @organization_id + ELSE true + END -- Filter by resource_target AND CASE WHEN @resource_target :: text != '' THEN diff --git a/coderd/members.go b/coderd/members.go index e15aa9d4821f9..24f712b8154c7 100644 --- a/coderd/members.go +++ b/coderd/members.go @@ -176,10 +176,11 @@ func (api *API) putMemberRoles(rw http.ResponseWriter, r *http.Request) { apiKey = httpmw.APIKey(r) auditor = api.Auditor.Load() aReq, commitAudit = audit.InitRequest[database.AuditableOrganizationMember](rw, &audit.RequestParams{ - Audit: *auditor, - Log: api.Logger, - Request: r, - Action: database.AuditActionWrite, + OrganizationID: organization.ID, + Audit: *auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionWrite, }) ) aReq.Old = member.OrganizationMember.Auditable(member.Username) diff --git a/coderd/searchquery/search.go b/coderd/searchquery/search.go index cef971a731cbd..ab44530cc7c4c 100644 --- a/coderd/searchquery/search.go +++ b/coderd/searchquery/search.go @@ -30,6 +30,7 @@ func AuditLogs(query string) (database.GetAuditLogsOffsetParams, []codersdk.Vali const dateLayout = "2006-01-02" parser := httpapi.NewQueryParamParser() filter := database.GetAuditLogsOffsetParams{ + OrganizationID: parser.UUID(values, uuid.Nil, "organization_id"), ResourceID: parser.UUID(values, uuid.Nil, "resource_id"), ResourceTarget: parser.String(values, "", "resource_target"), Username: parser.String(values, "", "username"), diff --git a/codersdk/audit.go b/codersdk/audit.go index b1d525d69179f..683db5406c13f 100644 --- a/codersdk/audit.go +++ b/codersdk/audit.go @@ -161,6 +161,7 @@ type CreateTestAuditLogRequest struct { AdditionalFields json.RawMessage `json:"additional_fields,omitempty"` Time time.Time `json:"time,omitempty" format:"date-time"` BuildReason BuildReason `json:"build_reason,omitempty" enums:"autostart,autostop,initiator"` + OrganizationID uuid.UUID `json:"organization_id,omitempty" format:"uuid"` } // AuditLogs retrieves audit logs from the given page. diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 161c5bc41213a..305b3c0e733f6 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -1181,6 +1181,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "action": "create", "additional_fields": [0], "build_reason": "autostart", + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", "resource_type": "template", "time": "2019-08-24T14:15:22Z" @@ -1194,6 +1195,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `action` | [codersdk.AuditAction](#codersdkauditaction) | false | | | | `additional_fields` | array of integer | false | | | | `build_reason` | [codersdk.BuildReason](#codersdkbuildreason) | false | | | +| `organization_id` | string | false | | | | `resource_id` | string | false | | | | `resource_type` | [codersdk.ResourceType](#codersdkresourcetype) | false | | | | `time` | string | false | | | diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index cdcacf1823edb..4f4b4c8333304 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -282,6 +282,7 @@ export interface CreateTestAuditLogRequest { readonly additional_fields?: Record; readonly time?: string; readonly build_reason?: BuildReason; + readonly organization_id?: string; } // From codersdk/apikey.go From 30c4b4db5c48a27d248b8a20977d53d663546825 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 26 Jun 2024 07:50:32 -1000 Subject: [PATCH 06/14] chore: implement fetch all authorized templates api (#13678) --- coderd/apidoc/docs.go | 28 +++++++++ coderd/apidoc/swagger.json | 24 +++++++ coderd/coderd.go | 29 +++++---- coderd/templates.go | 107 ++++++++++++++++++------------- coderd/templates_test.go | 36 +++++++++++ codersdk/organizations.go | 19 ++++++ docs/api/templates.md | 126 +++++++++++++++++++++++++++++++++++++ 7 files changed, 315 insertions(+), 54 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 50f31bfaf6ba6..0d923db69d8fc 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -3101,6 +3101,34 @@ const docTemplate = `{ } } }, + "/templates": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Templates" + ], + "summary": "Get all templates", + "operationId": "get-all-templates", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.Template" + } + } + } + } + } + }, "/templates/{template}": { "get": { "security": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index e04fc83f88653..46caa7d6146da 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -2725,6 +2725,30 @@ } } }, + "/templates": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Templates"], + "summary": "Get all templates", + "operationId": "get-all-templates", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.Template" + } + } + } + } + } + }, "/templates/{template}": { "get": { "security": [ diff --git a/coderd/coderd.go b/coderd/coderd.go index f92e3008604c3..288eca9a4dbaf 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -827,7 +827,7 @@ func New(options *Options) *API { r.Post("/templateversions", api.postTemplateVersionsByOrganization) r.Route("/templates", func(r chi.Router) { r.Post("/", api.postTemplateByOrganization) - r.Get("/", api.templatesByOrganization) + r.Get("/", api.templatesByOrganization()) r.Get("/examples", api.templateExamples) r.Route("/{templatename}", func(r chi.Router) { r.Get("/", api.templateByOrganizationAndName) @@ -869,20 +869,25 @@ func New(options *Options) *API { }) }) }) - r.Route("/templates/{template}", func(r chi.Router) { + r.Route("/templates", func(r chi.Router) { r.Use( apiKeyMiddleware, - httpmw.ExtractTemplateParam(options.Database), ) - r.Get("/daus", api.templateDAUs) - r.Get("/", api.template) - r.Delete("/", api.deleteTemplate) - r.Patch("/", api.patchTemplateMeta) - r.Route("/versions", func(r chi.Router) { - r.Post("/archive", api.postArchiveTemplateVersions) - r.Get("/", api.templateVersionsByTemplate) - r.Patch("/", api.patchActiveTemplateVersion) - r.Get("/{templateversionname}", api.templateVersionByName) + r.Get("/", api.fetchTemplates(nil)) + r.Route("/{template}", func(r chi.Router) { + r.Use( + httpmw.ExtractTemplateParam(options.Database), + ) + r.Get("/daus", api.templateDAUs) + r.Get("/", api.template) + r.Delete("/", api.deleteTemplate) + r.Patch("/", api.patchTemplateMeta) + r.Route("/versions", func(r chi.Router) { + r.Post("/archive", api.postArchiveTemplateVersions) + r.Get("/", api.templateVersionsByTemplate) + r.Patch("/", api.patchActiveTemplateVersion) + r.Get("/{templateversionname}", api.templateVersionByName) + }) }) }) r.Route("/templateversions/{templateversion}", func(r chi.Router) { diff --git a/coderd/templates.go b/coderd/templates.go index b4c546814737e..3027321fdbba2 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -435,55 +435,78 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque // @Param organization path string true "Organization ID" format(uuid) // @Success 200 {array} codersdk.Template // @Router /organizations/{organization}/templates [get] -func (api *API) templatesByOrganization(rw http.ResponseWriter, r *http.Request) { - ctx := r.Context() - organization := httpmw.OrganizationParam(r) +func (api *API) templatesByOrganization() http.HandlerFunc { + // TODO: Should deprecate this endpoint and make it akin to /workspaces with + // a filter. There isn't a need to make the organization filter argument + // part of the query url. + // mutate the filter to only include templates from the given organization. + return api.fetchTemplates(func(r *http.Request, arg *database.GetTemplatesWithFilterParams) { + organization := httpmw.OrganizationParam(r) + arg.OrganizationID = organization.ID + }) +} - p := httpapi.NewQueryParamParser() - values := r.URL.Query() +// @Summary Get all templates +// @ID get-all-templates +// @Security CoderSessionToken +// @Produce json +// @Tags Templates +// @Success 200 {array} codersdk.Template +// @Router /templates [get] +func (api *API) fetchTemplates(mutate func(r *http.Request, arg *database.GetTemplatesWithFilterParams)) http.HandlerFunc { + return func(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + p := httpapi.NewQueryParamParser() + values := r.URL.Query() + + deprecated := sql.NullBool{} + if values.Has("deprecated") { + deprecated = sql.NullBool{ + Bool: p.Boolean(values, false, "deprecated"), + Valid: true, + } + } + if len(p.Errors) > 0 { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Invalid query params.", + Validations: p.Errors, + }) + return + } - deprecated := sql.NullBool{} - if values.Has("deprecated") { - deprecated = sql.NullBool{ - Bool: p.Boolean(values, false, "deprecated"), - Valid: true, + prepared, err := api.HTTPAuth.AuthorizeSQLFilter(r, policy.ActionRead, rbac.ResourceTemplate.Type) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error preparing sql filter.", + Detail: err.Error(), + }) + return } - } - if len(p.Errors) > 0 { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Invalid query params.", - Validations: p.Errors, - }) - return - } - prepared, err := api.HTTPAuth.AuthorizeSQLFilter(r, policy.ActionRead, rbac.ResourceTemplate.Type) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error preparing sql filter.", - Detail: err.Error(), - }) - return - } + args := database.GetTemplatesWithFilterParams{ + Deprecated: deprecated, + } + if mutate != nil { + mutate(r, &args) + } - // Filter templates based on rbac permissions - templates, err := api.Database.GetAuthorizedTemplates(ctx, database.GetTemplatesWithFilterParams{ - OrganizationID: organization.ID, - Deprecated: deprecated, - }, prepared) - if errors.Is(err, sql.ErrNoRows) { - err = nil - } + // Filter templates based on rbac permissions + templates, err := api.Database.GetAuthorizedTemplates(ctx, args, prepared) + if errors.Is(err, sql.ErrNoRows) { + err = nil + } - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching templates in organization.", - Detail: err.Error(), - }) - return - } + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching templates in organization.", + Detail: err.Error(), + }) + return + } - httpapi.Write(ctx, rw, http.StatusOK, api.convertTemplates(templates)) + httpapi.Write(ctx, rw, http.StatusOK, api.convertTemplates(templates)) + } } // @Summary Get templates by organization and template name diff --git a/coderd/templates_test.go b/coderd/templates_test.go index 7aebaf41b1e1b..2813f713f5ea2 100644 --- a/coderd/templates_test.go +++ b/coderd/templates_test.go @@ -438,6 +438,42 @@ func TestTemplatesByOrganization(t *testing.T) { templates, err := client.TemplatesByOrganization(ctx, user.OrganizationID) require.NoError(t, err) require.Len(t, templates, 2) + + // Listing all should match + templates, err = client.Templates(ctx) + require.NoError(t, err) + require.Len(t, templates, 2) + }) + t.Run("MultipleOrganizations", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + owner := coderdtest.CreateFirstUser(t, client) + org2 := coderdtest.CreateOrganization(t, client, coderdtest.CreateOrganizationOptions{}) + user, _ := coderdtest.CreateAnotherUser(t, client, org2.ID) + + // 2 templates in first organization + version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) + version2 := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) + coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID) + coderdtest.CreateTemplate(t, client, owner.OrganizationID, version2.ID) + + // 2 in the second organization + version3 := coderdtest.CreateTemplateVersion(t, client, org2.ID, nil) + version4 := coderdtest.CreateTemplateVersion(t, client, org2.ID, nil) + coderdtest.CreateTemplate(t, client, org2.ID, version3.ID) + coderdtest.CreateTemplate(t, client, org2.ID, version4.ID) + + ctx := testutil.Context(t, testutil.WaitLong) + + // All 4 are viewable by the owner + templates, err := client.Templates(ctx) + require.NoError(t, err) + require.Len(t, templates, 4) + + // Only 2 are viewable by the org user + templates, err = user.Templates(ctx) + require.NoError(t, err) + require.Len(t, templates, 2) }) } diff --git a/codersdk/organizations.go b/codersdk/organizations.go index bc9e2514b2c15..e494018258e48 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -362,6 +362,25 @@ func (c *Client) TemplatesByOrganization(ctx context.Context, organizationID uui return templates, json.NewDecoder(res.Body).Decode(&templates) } +// Templates lists all viewable templates +func (c *Client) Templates(ctx context.Context) ([]Template, error) { + res, err := c.Request(ctx, http.MethodGet, + "/api/v2/templates", + nil, + ) + if err != nil { + return nil, xerrors.Errorf("execute request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return nil, ReadBodyAsError(res) + } + + var templates []Template + return templates, json.NewDecoder(res.Body).Decode(&templates) +} + // TemplateByName finds a template inside the organization provided with a case-insensitive name. func (c *Client) TemplateByName(ctx context.Context, organizationID uuid.UUID, name string) (Template, error) { if name == "" { diff --git a/docs/api/templates.md b/docs/api/templates.md index de0498c3de87b..b85811f41d0b8 100644 --- a/docs/api/templates.md +++ b/docs/api/templates.md @@ -617,6 +617,132 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Get all templates + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/templates \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /templates` + +### Example responses + +> 200 Response + +```json +[ + { + "active_user_count": 0, + "active_version_id": "eae64611-bd53-4a80-bb77-df1e432c0fbc", + "activity_bump_ms": 0, + "allow_user_autostart": true, + "allow_user_autostop": true, + "allow_user_cancel_workspace_jobs": true, + "autostart_requirement": { + "days_of_week": ["monday"] + }, + "autostop_requirement": { + "days_of_week": ["monday"], + "weeks": 0 + }, + "build_time_stats": { + "property1": { + "p50": 123, + "p95": 146 + }, + "property2": { + "p50": 123, + "p95": 146 + } + }, + "created_at": "2019-08-24T14:15:22Z", + "created_by_id": "9377d689-01fb-4abf-8450-3368d2c1924f", + "created_by_name": "string", + "default_ttl_ms": 0, + "deprecated": true, + "deprecation_message": "string", + "description": "string", + "display_name": "string", + "failure_ttl_ms": 0, + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "max_port_share_level": "owner", + "name": "string", + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", + "provisioner": "terraform", + "require_active_version": true, + "time_til_dormant_autodelete_ms": 0, + "time_til_dormant_ms": 0, + "updated_at": "2019-08-24T14:15:22Z" + } +] +``` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | --------------------------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.Template](schemas.md#codersdktemplate) | + +

Response Schema

+ +Status Code **200** + +| Name | Type | Required | Restrictions | Description | +| ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | -------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `[array item]` | array | false | | | +| `» active_user_count` | integer | false | | Active user count is set to -1 when loading. | +| `» active_version_id` | string(uuid) | false | | | +| `» activity_bump_ms` | integer | false | | | +| `» allow_user_autostart` | boolean | false | | Allow user autostart and AllowUserAutostop are enterprise-only. Their values are only used if your license is entitled to use the advanced template scheduling feature. | +| `» allow_user_autostop` | boolean | false | | | +| `» allow_user_cancel_workspace_jobs` | boolean | false | | | +| `» autostart_requirement` | [codersdk.TemplateAutostartRequirement](schemas.md#codersdktemplateautostartrequirement) | false | | | +| `»» days_of_week` | array | false | | Days of week is a list of days of the week in which autostart is allowed to happen. If no days are specified, autostart is not allowed. | +| `» autostop_requirement` | [codersdk.TemplateAutostopRequirement](schemas.md#codersdktemplateautostoprequirement) | false | | Autostop requirement and AutostartRequirement are enterprise features. Its value is only used if your license is entitled to use the advanced template scheduling feature. | +| `»» days_of_week` | array | false | | Days of week is a list of days of the week on which restarts are required. Restarts happen within the user's quiet hours (in their configured timezone). If no days are specified, restarts are not required. Weekdays cannot be specified twice. | +| Restarts will only happen on weekdays in this list on weeks which line up with Weeks. | +| `»» weeks` | integer | false | | Weeks is the number of weeks between required restarts. Weeks are synced across all workspaces (and Coder deployments) using modulo math on a hardcoded epoch week of January 2nd, 2023 (the first Monday of 2023). Values of 0 or 1 indicate weekly restarts. Values of 2 indicate fortnightly restarts, etc. | +| `» build_time_stats` | [codersdk.TemplateBuildTimeStats](schemas.md#codersdktemplatebuildtimestats) | false | | | +| `»» [any property]` | [codersdk.TransitionStats](schemas.md#codersdktransitionstats) | false | | | +| `»»» p50` | integer | false | | | +| `»»» p95` | integer | false | | | +| `» created_at` | string(date-time) | false | | | +| `» created_by_id` | string(uuid) | false | | | +| `» created_by_name` | string | false | | | +| `» default_ttl_ms` | integer | false | | | +| `» deprecated` | boolean | false | | | +| `» deprecation_message` | string | false | | | +| `» description` | string | false | | | +| `» display_name` | string | false | | | +| `» failure_ttl_ms` | integer | false | | Failure ttl ms TimeTilDormantMillis, and TimeTilDormantAutoDeleteMillis are enterprise-only. Their values are used if your license is entitled to use the advanced template scheduling feature. | +| `» icon` | string | false | | | +| `» id` | string(uuid) | false | | | +| `» max_port_share_level` | [codersdk.WorkspaceAgentPortShareLevel](schemas.md#codersdkworkspaceagentportsharelevel) | false | | | +| `» name` | string | false | | | +| `» organization_id` | string(uuid) | false | | | +| `» provisioner` | string | false | | | +| `» require_active_version` | boolean | false | | Require active version mandates that workspaces are built with the active template version. | +| `» time_til_dormant_autodelete_ms` | integer | false | | | +| `» time_til_dormant_ms` | integer | false | | | +| `» updated_at` | string(date-time) | false | | | + +#### Enumerated Values + +| Property | Value | +| ---------------------- | --------------- | +| `max_port_share_level` | `owner` | +| `max_port_share_level` | `authenticated` | +| `max_port_share_level` | `public` | +| `provisioner` | `terraform` | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Get template metadata by ID ### Code samples From c4f1676055de1b8da4ad10defb49f2edd107ac06 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 27 Jun 2024 10:07:30 +0200 Subject: [PATCH 07/14] feat: expose workspace build ID to terraform-plugin-coder (#13680) --- .../provisionerdserver/provisionerdserver.go | 1 + .../provisionerdserver_test.go | 1 + provisioner/terraform/provision.go | 1 + provisionersdk/proto/provisioner.pb.go | 283 +++++++++--------- provisionersdk/proto/provisioner.proto | 1 + site/e2e/provisionerGenerated.ts | 4 + 6 files changed, 155 insertions(+), 136 deletions(-) diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 8c8a3f64beed1..413ed999aa6a6 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -598,6 +598,7 @@ func (s *server) acquireProtoJob(ctx context.Context, job database.ProvisionerJo WorkspaceOwnerSessionToken: sessionToken, WorkspaceOwnerSshPublicKey: ownerSSHPublicKey, WorkspaceOwnerSshPrivateKey: ownerSSHPrivateKey, + WorkspaceBuildId: workspaceBuild.ID.String(), }, LogLevel: input.LogLevel, }, diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index e0403b7c7db2d..36f2ac5f601ce 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -365,6 +365,7 @@ func TestAcquireJob(t *testing.T) { WorkspaceOwnerSessionToken: sessionToken, WorkspaceOwnerSshPublicKey: sshKey.PublicKey, WorkspaceOwnerSshPrivateKey: sshKey.PrivateKey, + WorkspaceBuildId: build.ID.String(), }, }, }) diff --git a/provisioner/terraform/provision.go b/provisioner/terraform/provision.go index 20135c6034818..cd28f93854c0f 100644 --- a/provisioner/terraform/provision.go +++ b/provisioner/terraform/provision.go @@ -210,6 +210,7 @@ func provisionEnv( "CODER_WORKSPACE_TEMPLATE_ID="+metadata.GetTemplateId(), "CODER_WORKSPACE_TEMPLATE_NAME="+metadata.GetTemplateName(), "CODER_WORKSPACE_TEMPLATE_VERSION="+metadata.GetTemplateVersion(), + "CODER_WORKSPACE_BUILD_ID="+metadata.GetWorkspaceBuildId(), ) for key, value := range provisionersdk.AgentScriptEnv() { env = append(env, key+"="+value) diff --git a/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index 8e8b2ca085eb3..39fe557ae43e1 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -1640,6 +1640,7 @@ type Metadata struct { WorkspaceOwnerGroups []string `protobuf:"bytes,14,rep,name=workspace_owner_groups,json=workspaceOwnerGroups,proto3" json:"workspace_owner_groups,omitempty"` WorkspaceOwnerSshPublicKey string `protobuf:"bytes,15,opt,name=workspace_owner_ssh_public_key,json=workspaceOwnerSshPublicKey,proto3" json:"workspace_owner_ssh_public_key,omitempty"` WorkspaceOwnerSshPrivateKey string `protobuf:"bytes,16,opt,name=workspace_owner_ssh_private_key,json=workspaceOwnerSshPrivateKey,proto3" json:"workspace_owner_ssh_private_key,omitempty"` + WorkspaceBuildId string `protobuf:"bytes,17,opt,name=workspace_build_id,json=workspaceBuildId,proto3" json:"workspace_build_id,omitempty"` } func (x *Metadata) Reset() { @@ -1786,6 +1787,13 @@ func (x *Metadata) GetWorkspaceOwnerSshPrivateKey() string { return "" } +func (x *Metadata) GetWorkspaceBuildId() string { + if x != nil { + return x.WorkspaceBuildId + } + return "" +} + // Config represents execution configuration shared by all subsequent requests in the Session type Config struct { state protoimpl.MessageState @@ -2900,7 +2908,7 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x69, 0x73, 0x5f, 0x6e, 0x75, 0x6c, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x69, - 0x73, 0x4e, 0x75, 0x6c, 0x6c, 0x22, 0xc1, 0x06, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x73, 0x4e, 0x75, 0x6c, 0x6c, 0x22, 0xef, 0x06, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x53, 0x0a, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x74, 0x72, 0x61, @@ -2952,143 +2960,146 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x73, 0x73, 0x68, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x53, 0x73, 0x68, 0x50, - 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x22, 0x8a, 0x01, 0x0a, 0x06, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x17, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, - 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x15, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x53, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x12, 0x14, 0x0a, 0x05, - 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, - 0x74, 0x65, 0x12, 0x32, 0x0a, 0x15, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, - 0x72, 0x5f, 0x6c, 0x6f, 0x67, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x13, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x4c, 0x6f, - 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x0e, 0x0a, 0x0c, 0x50, 0x61, 0x72, 0x73, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xa3, 0x02, 0x0a, 0x0d, 0x50, 0x61, 0x72, 0x73, 0x65, - 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, - 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x4c, - 0x0a, 0x12, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, - 0x62, 0x6c, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, - 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, - 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x11, 0x74, 0x65, 0x6d, 0x70, 0x6c, - 0x61, 0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, - 0x72, 0x65, 0x61, 0x64, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x72, 0x65, - 0x61, 0x64, 0x6d, 0x65, 0x12, 0x54, 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x70, - 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, - 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0d, 0x77, 0x6f, 0x72, - 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x61, 0x67, 0x73, 0x1a, 0x40, 0x0a, 0x12, 0x57, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, - 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xb5, 0x02, 0x0a, - 0x0b, 0x50, 0x6c, 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, - 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, - 0x53, 0x0a, 0x15, 0x72, 0x69, 0x63, 0x68, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, - 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, - 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, - 0x13, 0x72, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x73, 0x12, 0x43, 0x0a, 0x0f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, - 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, - 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, - 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0e, 0x76, 0x61, 0x72, 0x69, 0x61, - 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x59, 0x0a, 0x17, 0x65, 0x78, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, - 0x64, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x70, 0x72, 0x6f, - 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x15, 0x65, - 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, - 0x64, 0x65, 0x72, 0x73, 0x22, 0xf8, 0x01, 0x0a, 0x0c, 0x50, 0x6c, 0x61, 0x6e, 0x43, 0x6f, 0x6d, - 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x33, 0x0a, 0x09, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, - 0x12, 0x3a, 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x03, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, - 0x52, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x61, 0x0a, 0x17, - 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, - 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x45, 0x78, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x15, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, - 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x22, - 0x41, 0x0a, 0x0c, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x31, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x2c, 0x0a, 0x12, 0x77, 0x6f, 0x72, + 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x18, + 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x42, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x64, 0x22, 0x8a, 0x01, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x12, 0x36, 0x0a, 0x17, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x15, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x53, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, + 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, + 0x12, 0x32, 0x0a, 0x15, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x5f, + 0x6c, 0x6f, 0x67, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x13, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x4c, + 0x65, 0x76, 0x65, 0x6c, 0x22, 0x0e, 0x0a, 0x0c, 0x50, 0x61, 0x72, 0x73, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x22, 0xa3, 0x02, 0x0a, 0x0d, 0x50, 0x61, 0x72, 0x73, 0x65, 0x43, 0x6f, + 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x4c, 0x0a, 0x12, + 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, + 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, + 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x11, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, + 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, + 0x61, 0x64, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x72, 0x65, 0x61, 0x64, + 0x6d, 0x65, 0x12, 0x54, 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, + 0x74, 0x61, 0x67, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x43, 0x6f, + 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x54, 0x61, 0x67, 0x73, 0x1a, 0x40, 0x0a, 0x12, 0x57, 0x6f, 0x72, 0x6b, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xb5, 0x02, 0x0a, 0x0b, 0x50, + 0x6c, 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x53, 0x0a, + 0x15, 0x72, 0x69, 0x63, 0x68, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, + 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x13, 0x72, + 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x73, 0x12, 0x43, 0x0a, 0x0f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, + 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0e, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, + 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x59, 0x0a, 0x17, 0x65, 0x78, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, + 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x15, 0x65, 0x78, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x73, 0x22, 0xf8, 0x01, 0x0a, 0x0c, 0x50, 0x6c, 0x61, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, + 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x3a, + 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, + 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x52, 0x0a, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x61, 0x0a, 0x17, 0x65, 0x78, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x15, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x22, 0x41, 0x0a, + 0x0c, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, + 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x22, 0x8f, 0x02, 0x0a, 0x0d, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, + 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x33, + 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, - 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x22, 0x8f, 0x02, 0x0a, 0x0d, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x43, 0x6f, 0x6d, 0x70, - 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, - 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, - 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x3a, 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, - 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, - 0x6d, 0x65, 0x74, 0x65, 0x72, 0x52, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, - 0x73, 0x12, 0x61, 0x0a, 0x17, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x75, - 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x2e, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, - 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x15, 0x65, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x73, 0x12, 0x3a, 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, + 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, + 0x74, 0x65, 0x72, 0x52, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, + 0x61, 0x0a, 0x17, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x75, 0x74, 0x68, + 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x29, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, - 0x64, 0x65, 0x72, 0x73, 0x22, 0x0f, 0x0a, 0x0d, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x8c, 0x02, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x2d, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x00, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x12, 0x31, 0x0a, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, - 0x72, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x05, 0x70, 0x61, - 0x72, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, - 0x50, 0x6c, 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x04, 0x70, - 0x6c, 0x61, 0x6e, 0x12, 0x31, 0x0a, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, - 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x12, 0x34, 0x0a, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, - 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x48, 0x00, 0x52, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x42, 0x06, 0x0a, 0x04, - 0x74, 0x79, 0x70, 0x65, 0x22, 0xd1, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x24, 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, - 0x48, 0x00, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x32, 0x0a, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, - 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, - 0x74, 0x65, 0x48, 0x00, 0x52, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x04, 0x70, - 0x6c, 0x61, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x43, 0x6f, 0x6d, 0x70, - 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x12, 0x32, 0x0a, 0x05, - 0x61, 0x70, 0x70, 0x6c, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x43, - 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, - 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x2a, 0x3f, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, - 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x00, 0x12, - 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, - 0x46, 0x4f, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x03, 0x12, 0x09, - 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x2a, 0x3b, 0x0a, 0x0f, 0x41, 0x70, 0x70, - 0x53, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, - 0x4f, 0x57, 0x4e, 0x45, 0x52, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x41, 0x55, 0x54, 0x48, 0x45, - 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x55, - 0x42, 0x4c, 0x49, 0x43, 0x10, 0x02, 0x2a, 0x37, 0x0a, 0x13, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x09, 0x0a, - 0x05, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x54, 0x4f, 0x50, - 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, 0x02, 0x32, - 0x49, 0x0a, 0x0b, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x12, 0x3a, - 0x0a, 0x07, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x30, 0x5a, 0x2e, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, - 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x65, 0x72, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x15, 0x65, 0x78, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x73, 0x22, 0x0f, 0x0a, 0x0d, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x22, 0x8c, 0x02, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x2d, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x48, 0x00, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x31, + 0x0a, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x05, 0x70, 0x61, 0x72, 0x73, + 0x65, 0x12, 0x2e, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x6c, + 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6c, 0x61, + 0x6e, 0x12, 0x31, 0x0a, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, + 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x05, 0x61, + 0x70, 0x70, 0x6c, 0x79, 0x12, 0x34, 0x0a, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x65, 0x72, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x48, 0x00, 0x52, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, + 0x70, 0x65, 0x22, 0xd1, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x24, 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x48, 0x00, + 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x32, 0x0a, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, + 0x48, 0x00, 0x52, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x04, 0x70, 0x6c, 0x61, + 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, + 0x74, 0x65, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x12, 0x32, 0x0a, 0x05, 0x61, 0x70, + 0x70, 0x6c, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x43, 0x6f, 0x6d, + 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x42, 0x06, + 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x2a, 0x3f, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, + 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x00, 0x12, 0x09, 0x0a, + 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, + 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, + 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x2a, 0x3b, 0x0a, 0x0f, 0x41, 0x70, 0x70, 0x53, 0x68, + 0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x4f, 0x57, + 0x4e, 0x45, 0x52, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, + 0x49, 0x43, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x55, 0x42, 0x4c, + 0x49, 0x43, 0x10, 0x02, 0x2a, 0x37, 0x0a, 0x13, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x09, 0x0a, 0x05, 0x53, + 0x54, 0x41, 0x52, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x54, 0x4f, 0x50, 0x10, 0x01, + 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, 0x02, 0x32, 0x49, 0x0a, + 0x0b, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x12, 0x3a, 0x0a, 0x07, + 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x30, 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, + 0x72, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( diff --git a/provisionersdk/proto/provisioner.proto b/provisionersdk/proto/provisioner.proto index a17b78bcc3ba4..71d8bd12e8d1e 100644 --- a/provisionersdk/proto/provisioner.proto +++ b/provisionersdk/proto/provisioner.proto @@ -228,6 +228,7 @@ message Metadata { repeated string workspace_owner_groups = 14; string workspace_owner_ssh_public_key = 15; string workspace_owner_ssh_private_key = 16; + string workspace_build_id = 17; } // Config represents execution configuration shared by all subsequent requests in the Session diff --git a/site/e2e/provisionerGenerated.ts b/site/e2e/provisionerGenerated.ts index 5c3d7ebc53512..fa98b21b7fd37 100644 --- a/site/e2e/provisionerGenerated.ts +++ b/site/e2e/provisionerGenerated.ts @@ -233,6 +233,7 @@ export interface Metadata { workspaceOwnerGroups: string[]; workspaceOwnerSshPublicKey: string; workspaceOwnerSshPrivateKey: string; + workspaceBuildId: string; } /** Config represents execution configuration shared by all subsequent requests in the Session */ @@ -850,6 +851,9 @@ export const Metadata = { if (message.workspaceOwnerSshPrivateKey !== "") { writer.uint32(130).string(message.workspaceOwnerSshPrivateKey); } + if (message.workspaceBuildId !== "") { + writer.uint32(138).string(message.workspaceBuildId); + } return writer; }, }; From 5b59f2880f0fb178b1182717459aef2c5750636d Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Thu, 27 Jun 2024 15:02:43 +0400 Subject: [PATCH 08/14] fix: fix workspacesdk to return error on API mismatch (#13683) --- codersdk/workspacesdk/connector.go | 24 +++++++++++- .../workspacesdk/connector_internal_test.go | 37 +++++++++++++++++++ codersdk/workspacesdk/workspacesdk.go | 2 + enterprise/coderd/workspaceagents_test.go | 6 ++- 4 files changed, 66 insertions(+), 3 deletions(-) diff --git a/codersdk/workspacesdk/connector.go b/codersdk/workspacesdk/connector.go index d6349adaf6b40..5ac009af15091 100644 --- a/codersdk/workspacesdk/connector.go +++ b/codersdk/workspacesdk/connector.go @@ -3,8 +3,10 @@ package workspacesdk import ( "context" "errors" + "fmt" "io" "net/http" + "slices" "sync" "time" @@ -14,6 +16,7 @@ import ( "tailscale.com/tailcfg" "cdr.dev/slog" + "github.com/coder/coder/v2/buildinfo" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/tailnet" "github.com/coder/coder/v2/tailnet/proto" @@ -101,6 +104,9 @@ func (tac *tailnetAPIConnector) run() { defer close(tac.closed) for retrier := retry.New(50*time.Millisecond, 10*time.Second); retrier.Wait(tac.ctx); { tailnetClient, err := tac.dial() + if xerrors.Is(err, &codersdk.Error{}) { + return + } if err != nil { continue } @@ -110,13 +116,29 @@ func (tac *tailnetAPIConnector) run() { } } +var permanentErrorStatuses = []int{ + http.StatusConflict, // returned if client/agent connections disabled (browser only) + http.StatusBadRequest, // returned if API mismatch + http.StatusNotFound, // returned if user doesn't have permission or agent doesn't exist +} + func (tac *tailnetAPIConnector) dial() (proto.DRPCTailnetClient, error) { tac.logger.Debug(tac.ctx, "dialing Coder tailnet v2+ API") // nolint:bodyclose ws, res, err := websocket.Dial(tac.ctx, tac.coordinateURL, tac.dialOptions) if tac.isFirst { - if res != nil && res.StatusCode == http.StatusConflict { + if res != nil && slices.Contains(permanentErrorStatuses, res.StatusCode) { err = codersdk.ReadBodyAsError(res) + // A bit more human-readable help in the case the API version was rejected + var sdkErr *codersdk.Error + if xerrors.As(err, &sdkErr) { + if sdkErr.Message == AgentAPIMismatchMessage && + sdkErr.StatusCode() == http.StatusBadRequest { + sdkErr.Helper = fmt.Sprintf( + "Ensure your client release version (%s, different than the API version) matches the server release version", + buildinfo.Version()) + } + } tac.connected <- err return nil, err } diff --git a/codersdk/workspacesdk/connector_internal_test.go b/codersdk/workspacesdk/connector_internal_test.go index 0b75e460ad669..c7fc036ffa2a1 100644 --- a/codersdk/workspacesdk/connector_internal_test.go +++ b/codersdk/workspacesdk/connector_internal_test.go @@ -18,6 +18,8 @@ import ( "cdr.dev/slog" "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/apiversion" + "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/tailnet" "github.com/coder/coder/v2/tailnet/proto" @@ -97,6 +99,41 @@ func TestTailnetAPIConnector_Disconnects(t *testing.T) { require.NotNil(t, reqDisc.Disconnect) } +func TestTailnetAPIConnector_UplevelVersion(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) + logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) + agentID := uuid.UUID{0x55} + + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + sVer := apiversion.New(proto.CurrentMajor, proto.CurrentMinor-1) + + // the following matches what Coderd does; + // c.f. coderd/workspaceagents.go: workspaceAgentClientCoordinate + cVer := r.URL.Query().Get("version") + if err := sVer.Validate(cVer); err != nil { + httpapi.Write(ctx, w, http.StatusBadRequest, codersdk.Response{ + Message: AgentAPIMismatchMessage, + Validations: []codersdk.ValidationError{ + {Field: "version", Detail: err.Error()}, + }, + }) + return + } + })) + + fConn := newFakeTailnetConn() + + uut := runTailnetAPIConnector(ctx, logger, agentID, svr.URL, &websocket.DialOptions{}, fConn) + + err := testutil.RequireRecvCtx(ctx, t, uut.connected) + var sdkErr *codersdk.Error + require.ErrorAs(t, err, &sdkErr) + require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode()) + require.Equal(t, AgentAPIMismatchMessage, sdkErr.Message) + require.NotEmpty(t, sdkErr.Helper) +} + type fakeTailnetConn struct{} func (*fakeTailnetConn) UpdatePeers([]*proto.CoordinateResponse_PeerUpdate) error { diff --git a/codersdk/workspacesdk/workspacesdk.go b/codersdk/workspacesdk/workspacesdk.go index f1e3bd67ea3dc..cb17150e8072e 100644 --- a/codersdk/workspacesdk/workspacesdk.go +++ b/codersdk/workspacesdk/workspacesdk.go @@ -55,6 +55,8 @@ const ( AgentMinimumListeningPort = 9 ) +const AgentAPIMismatchMessage = "Unknown or unsupported API version" + // AgentIgnoredListeningPorts contains a list of ports to ignore when looking for // running applications inside a workspace. We want to ignore non-HTTP servers, // so we pre-populate this list with common ports that are not HTTP servers. diff --git a/enterprise/coderd/workspaceagents_test.go b/enterprise/coderd/workspaceagents_test.go index 17b38a0332570..12c308987fd14 100644 --- a/enterprise/coderd/workspaceagents_test.go +++ b/enterprise/coderd/workspaceagents_test.go @@ -46,8 +46,9 @@ func TestBlockNonBrowser(t *testing.T) { }, }) r := setupWorkspaceAgent(t, client, user, 0) + ctx := testutil.Context(t, testutil.WaitShort) //nolint:gocritic // Testing that even the owner gets blocked. - _, err := workspacesdk.New(client).DialAgent(context.Background(), r.sdkAgent.ID, nil) + _, err := workspacesdk.New(client).DialAgent(ctx, r.sdkAgent.ID, nil) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusConflict, apiErr.StatusCode()) @@ -65,8 +66,9 @@ func TestBlockNonBrowser(t *testing.T) { }, }) r := setupWorkspaceAgent(t, client, user, 0) + ctx := testutil.Context(t, testutil.WaitShort) //nolint:gocritic // Testing RBAC is not the point of this test. - conn, err := workspacesdk.New(client).DialAgent(context.Background(), r.sdkAgent.ID, nil) + conn, err := workspacesdk.New(client).DialAgent(ctx, r.sdkAgent.ID, nil) require.NoError(t, err) _ = conn.Close() }) From c94b5188bd2a48dba26032efe23e5f5530a1ebcc Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Thu, 27 Jun 2024 15:38:21 +0400 Subject: [PATCH 09/14] fix: modify workspacesdk to ask for tailnet API 2.0 (#13684) #13617 bumped the Agent/Tailnet API minor version because it adds telemetry features. However, we don't actually use the protocol features yet, so it's a bit obnoxious for our CLI client to ask for the newest API version. This is particularly true of the CLI client, since that's distributed separately, so if an end user installs the latest CLI client and their organization hasn't fully upgraded, then it will fail to connect. Since we have a release coming up and the telemetry stuff won't make it, I think we should roll back to version 2.0 until we actually implement the telemetry stuff. That way the newest release (2.13) will work with Coder servers all the way back to 2.9. --- codersdk/workspacesdk/workspacesdk.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/codersdk/workspacesdk/workspacesdk.go b/codersdk/workspacesdk/workspacesdk.go index cb17150e8072e..04765c13d9877 100644 --- a/codersdk/workspacesdk/workspacesdk.go +++ b/codersdk/workspacesdk/workspacesdk.go @@ -21,7 +21,6 @@ import ( "cdr.dev/slog" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/tailnet" - "github.com/coder/coder/v2/tailnet/proto" ) // AgentIP is a static IPv6 address with the Tailscale prefix that is used to route @@ -241,7 +240,15 @@ func (c *Client) DialAgent(dialCtx context.Context, agentID uuid.UUID, options * return nil, xerrors.Errorf("parse url: %w", err) } q := coordinateURL.Query() - q.Add("version", proto.CurrentVersion.String()) + // TODO (ethanndickson) - the current version includes 2 additions we don't currently use: + // + // 2.1 GetAnnouncementBanners on the Agent API (version locked to Tailnet API) + // 2.2 PostTelemetry on the Tailnet API + // + // So, asking for API 2.2 just makes us incompatible back level servers, for no real benefit. + // As a temporary measure, we'll specifically ask for API version 2.0 until we implement sending + // telemetry. + q.Add("version", "2.0") coordinateURL.RawQuery = q.Encode() connector := runTailnetAPIConnector(ctx, options.Logger, From 6cd8c4371eed9312289c2865aef74e67a57360db Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Wed, 26 Jun 2024 14:05:07 +0000 Subject: [PATCH 10/14] chore: add DRPC server implementation for network telemetry --- coderd/agentapi/api.go | 40 +- coderd/coderd.go | 24 +- coderd/telemetry/telemetry.go | 305 +++++++++++ coderd/workspaceagentsrpc.go | 24 +- tailnet/proto/tailnet.pb.go | 979 ++++++++++++++++++---------------- tailnet/proto/tailnet.proto | 37 +- tailnet/service.go | 95 +++- 7 files changed, 1001 insertions(+), 503 deletions(-) diff --git a/coderd/agentapi/api.go b/coderd/agentapi/api.go index 4e5e30ad9c761..37c84a966f4cb 100644 --- a/coderd/agentapi/api.go +++ b/coderd/agentapi/api.go @@ -23,6 +23,7 @@ import ( "github.com/coder/coder/v2/coderd/externalauth" "github.com/coder/coder/v2/coderd/prometheusmetrics" "github.com/coder/coder/v2/coderd/schedule" + "github.com/coder/coder/v2/coderd/telemetry" "github.com/coder/coder/v2/coderd/tracing" "github.com/coder/coder/v2/coderd/workspacestats" "github.com/coder/coder/v2/codersdk" @@ -47,6 +48,7 @@ type API struct { mu sync.Mutex cachedWorkspaceID uuid.UUID + drpcServiceClose func() } var _ agentproto.DRPCAgentServer = &API{} @@ -65,15 +67,18 @@ type Options struct { AppearanceFetcher *atomic.Pointer[appearance.Fetcher] PublishWorkspaceUpdateFn func(ctx context.Context, workspaceID uuid.UUID) PublishWorkspaceAgentLogsUpdateFn func(ctx context.Context, workspaceAgentID uuid.UUID, msg agentsdk.LogsNotifyMessage) - - AccessURL *url.URL - AppHostname string - AgentStatsRefreshInterval time.Duration - DisableDirectConnections bool - DerpForceWebSockets bool - DerpMapUpdateFrequency time.Duration - ExternalAuthConfigs []*externalauth.Config - Experiments codersdk.Experiments + NetworkTelemetryBatchFn func(batch []telemetry.NetworkEvent) + + AccessURL *url.URL + AppHostname string + AgentStatsRefreshInterval time.Duration + DisableDirectConnections bool + DerpForceWebSockets bool + DerpMapUpdateFrequency time.Duration + NetworkTelemetryBatchFrequency time.Duration + NetworkTelemetryBatchMaxSize int + ExternalAuthConfigs []*externalauth.Config + Experiments codersdk.Experiments // Optional: // WorkspaceID avoids a future lookup to find the workspace ID by setting @@ -154,15 +159,24 @@ func New(opts Options) *API { } api.DRPCService = &tailnet.DRPCService{ - CoordPtr: opts.TailnetCoordinator, - Logger: opts.Log, - DerpMapUpdateFrequency: opts.DerpMapUpdateFrequency, - DerpMapFn: opts.DerpMapFn, + CoordPtr: opts.TailnetCoordinator, + Logger: opts.Log, + DerpMapUpdateFrequency: opts.DerpMapUpdateFrequency, + DerpMapFn: opts.DerpMapFn, + NetworkTelemetryBatchFrequency: opts.NetworkTelemetryBatchFrequency, + NetworkTelemetryBatchMaxSize: opts.NetworkTelemetryBatchMaxSize, + NetworkTelemetryBatchFn: opts.NetworkTelemetryBatchFn, } + api.drpcServiceClose = api.DRPCService.PeriodicTelemetryBatcher() return api } +func (a *API) Close() error { + a.drpcServiceClose() + return nil +} + func (a *API) Server(ctx context.Context) (*drpcserver.Server, error) { mux := drpcmux.New() err := agentproto.DRPCRegisterAgent(mux, a) diff --git a/coderd/coderd.go b/coderd/coderd.go index 288eca9a4dbaf..292b0cb171574 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -142,14 +142,16 @@ type Options struct { DERPServer *derp.Server // BaseDERPMap is used as the base DERP map for all clients and agents. // Proxies are added to this list. - BaseDERPMap *tailcfg.DERPMap - DERPMapUpdateFrequency time.Duration - SwaggerEndpoint bool - SetUserGroups func(ctx context.Context, logger slog.Logger, tx database.Store, userID uuid.UUID, orgGroupNames map[uuid.UUID][]string, createMissingGroups bool) error - SetUserSiteRoles func(ctx context.Context, logger slog.Logger, tx database.Store, userID uuid.UUID, roles []string) error - TemplateScheduleStore *atomic.Pointer[schedule.TemplateScheduleStore] - UserQuietHoursScheduleStore *atomic.Pointer[schedule.UserQuietHoursScheduleStore] - AccessControlStore *atomic.Pointer[dbauthz.AccessControlStore] + BaseDERPMap *tailcfg.DERPMap + DERPMapUpdateFrequency time.Duration + NetworkTelemetryBatchFrequency time.Duration + NetworkTelemetryBatchMaxSize int + SwaggerEndpoint bool + SetUserGroups func(ctx context.Context, logger slog.Logger, tx database.Store, userID uuid.UUID, orgGroupNames map[uuid.UUID][]string, createMissingGroups bool) error + SetUserSiteRoles func(ctx context.Context, logger slog.Logger, tx database.Store, userID uuid.UUID, roles []string) error + TemplateScheduleStore *atomic.Pointer[schedule.TemplateScheduleStore] + UserQuietHoursScheduleStore *atomic.Pointer[schedule.UserQuietHoursScheduleStore] + AccessControlStore *atomic.Pointer[dbauthz.AccessControlStore] // AppSecurityKey is the crypto key used to sign and encrypt tokens related to // workspace applications. It consists of both a signing and encryption key. AppSecurityKey workspaceapps.SecurityKey @@ -305,6 +307,12 @@ func New(options *Options) *API { if options.DERPMapUpdateFrequency == 0 { options.DERPMapUpdateFrequency = 5 * time.Second } + if options.NetworkTelemetryBatchFrequency == 0 { + options.NetworkTelemetryBatchFrequency = 1 * time.Minute + } + if options.NetworkTelemetryBatchMaxSize == 0 { + options.NetworkTelemetryBatchMaxSize = 1_000 + } if options.TailnetCoordinator == nil { options.TailnetCoordinator = tailnet.NewCoordinator(options.Logger) } diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index 91251053663f5..d2dfbaba44204 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -20,6 +20,8 @@ import ( "github.com/google/uuid" "golang.org/x/sync/errgroup" "golang.org/x/xerrors" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/wrapperspb" "cdr.dev/slog" "github.com/coder/coder/v2/buildinfo" @@ -27,6 +29,7 @@ import ( "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/codersdk" + tailnetproto "github.com/coder/coder/v2/tailnet/proto" ) const ( @@ -795,6 +798,7 @@ type Snapshot struct { WorkspaceResourceMetadata []WorkspaceResourceMetadata `json:"workspace_resource_metadata"` WorkspaceResources []WorkspaceResource `json:"workspace_resources"` Workspaces []Workspace `json:"workspaces"` + NetworkEvents []NetworkEvent `json:"network_events"` } // Deployment contains information about the host running Coder. @@ -1006,6 +1010,307 @@ type ExternalProvisioner struct { ShutdownAt *time.Time `json:"shutdown_at"` } +type NetworkEventIPFields struct { + Version int32 `json:"version"` // 4 or 6 + Class string `json:"class"` // public, private, link_local, unique_local, loopback +} + +func ipFieldsFromProto(proto *tailnetproto.IPFields) NetworkEventIPFields { + if proto == nil { + return NetworkEventIPFields{} + } + return NetworkEventIPFields{ + Version: proto.Version, + Class: strings.ToLower(proto.Class.String()), + } +} + +type NetworkEventP2PEndpoint struct { + Hash string `json:"hash"` + Port int `json:"port"` + Fields NetworkEventIPFields `json:"fields"` +} + +func p2pEndpointFromProto(proto *tailnetproto.TelemetryEvent_P2PEndpoint) NetworkEventP2PEndpoint { + if proto == nil { + return NetworkEventP2PEndpoint{} + } + return NetworkEventP2PEndpoint{ + Hash: proto.Hash, + Port: int(proto.Port), + Fields: ipFieldsFromProto(proto.Fields), + } +} + +type DERPMapHomeParams struct { + RegionScore map[int64]float64 `json:"region_score"` +} + +func derpMapHomeParamsFromProto(proto *tailnetproto.DERPMap_HomeParams) DERPMapHomeParams { + if proto == nil { + return DERPMapHomeParams{} + } + out := DERPMapHomeParams{ + RegionScore: make(map[int64]float64, len(proto.RegionScore)), + } + for k, v := range proto.RegionScore { + out.RegionScore[k] = v + } + return out +} + +type DERPRegion struct { + RegionID int64 `json:"region_id"` + EmbeddedRelay bool `json:"embedded_relay"` + RegionCode string + RegionName string + Avoid bool + Nodes []DERPNode `json:"nodes"` +} + +func derpRegionFromProto(proto *tailnetproto.DERPMap_Region) DERPRegion { + if proto == nil { + return DERPRegion{} + } + nodes := make([]DERPNode, 0, len(proto.Nodes)) + for _, node := range proto.Nodes { + nodes = append(nodes, derpNodeFromProto(node)) + } + return DERPRegion{ + RegionID: proto.RegionId, + EmbeddedRelay: proto.EmbeddedRelay, + RegionCode: proto.RegionCode, + RegionName: proto.RegionName, + Avoid: proto.Avoid, + Nodes: nodes, + } +} + +type DERPNode struct { + Name string `json:"name"` + RegionID int64 `json:"region_id"` + HostName string `json:"host_name"` + CertName string `json:"cert_name"` + IPv4 string `json:"ipv4"` + IPv6 string `json:"ipv6"` + STUNPort int32 `json:"stun_port"` + STUNOnly bool `json:"stun_only"` + DERPPort int32 `json:"derp_port"` + InsecureForTests bool `json:"insecure_for_tests"` + ForceHTTP bool `json:"force_http"` + STUNTestIP string `json:"stun_test_ip"` + CanPort80 bool `json:"can_port_80"` +} + +func derpNodeFromProto(proto *tailnetproto.DERPMap_Region_Node) DERPNode { + if proto == nil { + return DERPNode{} + } + return DERPNode{ + Name: proto.Name, + RegionID: proto.RegionId, + HostName: proto.HostName, + CertName: proto.CertName, + IPv4: proto.Ipv4, + IPv6: proto.Ipv6, + STUNPort: proto.StunPort, + STUNOnly: proto.StunOnly, + DERPPort: proto.DerpPort, + InsecureForTests: proto.InsecureForTests, + ForceHTTP: proto.ForceHttp, + STUNTestIP: proto.StunTestIp, + CanPort80: proto.CanPort_80, + } +} + +type DERPMap struct { + HomeParams DERPMapHomeParams `json:"home_params"` + Regions map[int64]DERPRegion +} + +func derpMapFromProto(proto *tailnetproto.DERPMap) DERPMap { + if proto == nil { + return DERPMap{} + } + regionMap := make(map[int64]DERPRegion, len(proto.Regions)) + for k, v := range proto.Regions { + regionMap[k] = derpRegionFromProto(v) + } + return DERPMap{ + HomeParams: derpMapHomeParamsFromProto(proto.HomeParams), + Regions: regionMap, + } +} + +type NetcheckIP struct { + Hash string `json:"hash"` + Fields NetworkEventIPFields `json:"fields"` +} + +func netcheckIPFromProto(proto *tailnetproto.Netcheck_NetcheckIP) NetcheckIP { + if proto == nil { + return NetcheckIP{} + } + return NetcheckIP{ + Hash: proto.Hash, + Fields: ipFieldsFromProto(proto.Fields), + } +} + +type Netcheck struct { + UDP bool `json:"udp"` + IPv6 bool `json:"ipv6"` + IPv4 bool `json:"ipv4"` + IPv6CanSend bool `json:"ipv6_can_send"` + IPv4CanSend bool `json:"ipv4_can_send"` + OSHasIPv6 bool `json:"os_has_ipv6"` + ICMPv4 bool `json:"icmpv4"` + + MappingVariesByDestIP *bool `json:"mapping_varies_by_dest_ip"` + HairPinning *bool `json:"hair_pinning"` + UPnP *bool `json:"upnp"` + PMP *bool `json:"pmp"` + PCP *bool `json:"pcp"` + + PreferredDERP int64 `json:"preferred_derp"` + + RegionLatency map[int64]time.Duration `json:"region_latency"` + RegionV4Latency map[int64]time.Duration `json:"region_v4_latency"` + RegionV6Latency map[int64]time.Duration `json:"region_v6_latency"` + + GlobalV4 NetcheckIP `json:"global_v4"` + GlobalV6 NetcheckIP `json:"global_v6"` + + CaptivePortal *bool `json:"captive_portal"` +} + +func protoBool(b *wrapperspb.BoolValue) *bool { + if b == nil { + return nil + } + return &b.Value +} + +func netcheckFromProto(proto *tailnetproto.Netcheck) Netcheck { + if proto == nil { + return Netcheck{} + } + + durationMapFromProto := func(m map[int64]*durationpb.Duration) map[int64]time.Duration { + out := make(map[int64]time.Duration, len(m)) + for k, v := range m { + out[k] = v.AsDuration() + } + return out + } + + return Netcheck{ + UDP: proto.UDP, + IPv6: proto.IPv6, + IPv4: proto.IPv4, + IPv6CanSend: proto.IPv6CanSend, + IPv4CanSend: proto.IPv4CanSend, + OSHasIPv6: proto.OSHasIPv6, + ICMPv4: proto.ICMPv4, + + MappingVariesByDestIP: protoBool(proto.MappingVariesByDestIP), + HairPinning: protoBool(proto.HairPinning), + UPnP: protoBool(proto.UPnP), + PMP: protoBool(proto.PMP), + PCP: protoBool(proto.PCP), + + PreferredDERP: proto.PreferredDERP, + + RegionLatency: durationMapFromProto(proto.RegionLatency), + RegionV4Latency: durationMapFromProto(proto.RegionV4Latency), + RegionV6Latency: durationMapFromProto(proto.RegionV6Latency), + + GlobalV4: netcheckIPFromProto(proto.GlobalV4), + GlobalV6: netcheckIPFromProto(proto.GlobalV6), + + CaptivePortal: protoBool(proto.CaptivePortal), + } +} + +// NetworkEvent and all related structs come from tailnet.proto. +type NetworkEvent struct { + ID uuid.UUID `json:"id"` + Time time.Time `json:"time"` + Application string `json:"application"` + Status string `json:"status"` // connected, disconnected + DisconnectionReason string `json:"disconnection_reason"` + ClientType string `json:"client_type"` // cli, agent, coderd, wsproxy + NodeIDSelf uuid.UUID `json:"node_id_self"` + NodeIDRemote uuid.UUID `json:"node_id_remote"` + P2PEndpoint NetworkEventP2PEndpoint `json:"p2p_endpoint"` + LogIPHashes map[string]NetworkEventIPFields `json:"log_ip_hashes"` + HomeDERP string `json:"home_derp"` + Logs []string `json:"logs"` + DERPMap DERPMap `json:"derp_map"` + LatestNetcheck Netcheck `json:"latest_netcheck"` + + ConnectionAge time.Duration `json:"connection_age"` + ConnectionSetup time.Duration `json:"connection_setup"` + P2PSetup time.Duration `json:"p2p_setup"` + DERPLatency time.Duration `json:"derp_latency"` + P2PLatency time.Duration `json:"p2p_latency"` + ThroughputMbits *float32 `json:"throughput_mbits"` +} + +func protoFloat(f *wrapperspb.FloatValue) *float32 { + if f == nil { + return nil + } + return &f.Value +} + +func NetworkEventFromProto(proto *tailnetproto.TelemetryEvent) (NetworkEvent, error) { + if proto == nil { + return NetworkEvent{}, xerrors.New("nil event") + } + id, err := uuid.ParseBytes(proto.Id) + if err != nil { + return NetworkEvent{}, xerrors.Errorf("parse id %q: %w", proto.Id, err) + } + nodeIDSelf, err := uuid.ParseBytes(proto.NodeIdSelf) + if err != nil { + return NetworkEvent{}, xerrors.Errorf("parse node_id_self %q: %w", proto.NodeIdSelf, err) + } + nodeIDRemote, err := uuid.ParseBytes(proto.NodeIdRemote) + if err != nil { + return NetworkEvent{}, xerrors.Errorf("parse node_id_remote %q: %w", proto.NodeIdRemote, err) + } + + logIPHashes := make(map[string]NetworkEventIPFields, len(proto.LogIpHashes)) + for k, v := range proto.LogIpHashes { + logIPHashes[k] = ipFieldsFromProto(v) + } + + return NetworkEvent{ + ID: id, + Time: proto.Time.AsTime(), + Application: proto.Application, + Status: strings.ToLower(proto.Status.String()), + DisconnectionReason: proto.DisconnectionReason, + ClientType: strings.ToLower(proto.ClientType.String()), + NodeIDSelf: nodeIDSelf, + NodeIDRemote: nodeIDRemote, + P2PEndpoint: p2pEndpointFromProto(proto.P2PEndpoint), + LogIPHashes: logIPHashes, + HomeDERP: proto.HomeDerp, + Logs: proto.Logs, + DERPMap: derpMapFromProto(proto.DerpMap), + LatestNetcheck: netcheckFromProto(proto.LatestNetcheck), + + ConnectionAge: proto.ConnectionAge.AsDuration(), + ConnectionSetup: proto.ConnectionSetup.AsDuration(), + P2PSetup: proto.P2PSetup.AsDuration(), + DERPLatency: proto.DerpLatency.AsDuration(), + P2PLatency: proto.P2PLatency.AsDuration(), + ThroughputMbits: protoFloat(proto.ThroughputMbits), + }, nil +} + type noopReporter struct{} func (*noopReporter) Report(_ *Snapshot) {} diff --git a/coderd/workspaceagentsrpc.go b/coderd/workspaceagentsrpc.go index b413db264feac..9853d170eff7c 100644 --- a/coderd/workspaceagentsrpc.go +++ b/coderd/workspaceagentsrpc.go @@ -24,6 +24,7 @@ import ( "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/telemetry" "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/tailnet" @@ -135,15 +136,22 @@ func (api *API) workspaceAgentRPC(rw http.ResponseWriter, r *http.Request) { StatsReporter: api.statsReporter, PublishWorkspaceUpdateFn: api.publishWorkspaceUpdate, PublishWorkspaceAgentLogsUpdateFn: api.publishWorkspaceAgentLogsUpdate, + NetworkTelemetryBatchFn: func(batch []telemetry.NetworkEvent) { + api.Telemetry.Report(&telemetry.Snapshot{ + NetworkEvents: batch, + }) + }, - AccessURL: api.AccessURL, - AppHostname: api.AppHostname, - AgentStatsRefreshInterval: api.AgentStatsRefreshInterval, - DisableDirectConnections: api.DeploymentValues.DERP.Config.BlockDirect.Value(), - DerpForceWebSockets: api.DeploymentValues.DERP.Config.ForceWebSockets.Value(), - DerpMapUpdateFrequency: api.Options.DERPMapUpdateFrequency, - ExternalAuthConfigs: api.ExternalAuthConfigs, - Experiments: api.Experiments, + AccessURL: api.AccessURL, + AppHostname: api.AppHostname, + AgentStatsRefreshInterval: api.AgentStatsRefreshInterval, + DisableDirectConnections: api.DeploymentValues.DERP.Config.BlockDirect.Value(), + DerpForceWebSockets: api.DeploymentValues.DERP.Config.ForceWebSockets.Value(), + DerpMapUpdateFrequency: api.Options.DERPMapUpdateFrequency, + NetworkTelemetryBatchFrequency: api.Options.NetworkTelemetryBatchFrequency, + NetworkTelemetryBatchMaxSize: api.Options.NetworkTelemetryBatchMaxSize, + ExternalAuthConfigs: api.ExternalAuthConfigs, + Experiments: api.Experiments, // Optional: WorkspaceID: build.WorkspaceID, // saves the extra lookup later diff --git a/tailnet/proto/tailnet.pb.go b/tailnet/proto/tailnet.pb.go index c2dfd88a483ba..f293b7ba69f55 100644 --- a/tailnet/proto/tailnet.pb.go +++ b/tailnet/proto/tailnet.pb.go @@ -78,6 +78,61 @@ func (CoordinateResponse_PeerUpdate_Kind) EnumDescriptor() ([]byte, []int) { return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{4, 0, 0} } +type IPFields_IPClass int32 + +const ( + IPFields_PUBLIC IPFields_IPClass = 0 + IPFields_PRIVATE IPFields_IPClass = 1 + IPFields_LINK_LOCAL IPFields_IPClass = 2 + IPFields_UNIQUE_LOCAL IPFields_IPClass = 3 + IPFields_LOOPBACK IPFields_IPClass = 4 +) + +// Enum value maps for IPFields_IPClass. +var ( + IPFields_IPClass_name = map[int32]string{ + 0: "PUBLIC", + 1: "PRIVATE", + 2: "LINK_LOCAL", + 3: "UNIQUE_LOCAL", + 4: "LOOPBACK", + } + IPFields_IPClass_value = map[string]int32{ + "PUBLIC": 0, + "PRIVATE": 1, + "LINK_LOCAL": 2, + "UNIQUE_LOCAL": 3, + "LOOPBACK": 4, + } +) + +func (x IPFields_IPClass) Enum() *IPFields_IPClass { + p := new(IPFields_IPClass) + *p = x + return p +} + +func (x IPFields_IPClass) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (IPFields_IPClass) Descriptor() protoreflect.EnumDescriptor { + return file_tailnet_proto_tailnet_proto_enumTypes[1].Descriptor() +} + +func (IPFields_IPClass) Type() protoreflect.EnumType { + return &file_tailnet_proto_tailnet_proto_enumTypes[1] +} + +func (x IPFields_IPClass) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use IPFields_IPClass.Descriptor instead. +func (IPFields_IPClass) EnumDescriptor() ([]byte, []int) { + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{5, 0} +} + type TelemetryEvent_Status int32 const ( @@ -108,11 +163,11 @@ func (x TelemetryEvent_Status) String() string { } func (TelemetryEvent_Status) Descriptor() protoreflect.EnumDescriptor { - return file_tailnet_proto_tailnet_proto_enumTypes[1].Descriptor() + return file_tailnet_proto_tailnet_proto_enumTypes[2].Descriptor() } func (TelemetryEvent_Status) Type() protoreflect.EnumType { - return &file_tailnet_proto_tailnet_proto_enumTypes[1] + return &file_tailnet_proto_tailnet_proto_enumTypes[2] } func (x TelemetryEvent_Status) Number() protoreflect.EnumNumber { @@ -121,7 +176,7 @@ func (x TelemetryEvent_Status) Number() protoreflect.EnumNumber { // Deprecated: Use TelemetryEvent_Status.Descriptor instead. func (TelemetryEvent_Status) EnumDescriptor() ([]byte, []int) { - return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{6, 0} + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{7, 0} } type TelemetryEvent_ClientType int32 @@ -160,11 +215,11 @@ func (x TelemetryEvent_ClientType) String() string { } func (TelemetryEvent_ClientType) Descriptor() protoreflect.EnumDescriptor { - return file_tailnet_proto_tailnet_proto_enumTypes[2].Descriptor() + return file_tailnet_proto_tailnet_proto_enumTypes[3].Descriptor() } func (TelemetryEvent_ClientType) Type() protoreflect.EnumType { - return &file_tailnet_proto_tailnet_proto_enumTypes[2] + return &file_tailnet_proto_tailnet_proto_enumTypes[3] } func (x TelemetryEvent_ClientType) Number() protoreflect.EnumNumber { @@ -173,62 +228,7 @@ func (x TelemetryEvent_ClientType) Number() protoreflect.EnumNumber { // Deprecated: Use TelemetryEvent_ClientType.Descriptor instead. func (TelemetryEvent_ClientType) EnumDescriptor() ([]byte, []int) { - return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{6, 1} -} - -type TelemetryEvent_IPClass int32 - -const ( - TelemetryEvent_PUBLIC TelemetryEvent_IPClass = 0 - TelemetryEvent_PRIVATE TelemetryEvent_IPClass = 1 - TelemetryEvent_LINK_LOCAL TelemetryEvent_IPClass = 2 - TelemetryEvent_UNIQUE_LOCAL TelemetryEvent_IPClass = 3 - TelemetryEvent_LOOPBACK TelemetryEvent_IPClass = 4 -) - -// Enum value maps for TelemetryEvent_IPClass. -var ( - TelemetryEvent_IPClass_name = map[int32]string{ - 0: "PUBLIC", - 1: "PRIVATE", - 2: "LINK_LOCAL", - 3: "UNIQUE_LOCAL", - 4: "LOOPBACK", - } - TelemetryEvent_IPClass_value = map[string]int32{ - "PUBLIC": 0, - "PRIVATE": 1, - "LINK_LOCAL": 2, - "UNIQUE_LOCAL": 3, - "LOOPBACK": 4, - } -) - -func (x TelemetryEvent_IPClass) Enum() *TelemetryEvent_IPClass { - p := new(TelemetryEvent_IPClass) - *p = x - return p -} - -func (x TelemetryEvent_IPClass) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (TelemetryEvent_IPClass) Descriptor() protoreflect.EnumDescriptor { - return file_tailnet_proto_tailnet_proto_enumTypes[3].Descriptor() -} - -func (TelemetryEvent_IPClass) Type() protoreflect.EnumType { - return &file_tailnet_proto_tailnet_proto_enumTypes[3] -} - -func (x TelemetryEvent_IPClass) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use TelemetryEvent_IPClass.Descriptor instead. -func (TelemetryEvent_IPClass) EnumDescriptor() ([]byte, []int) { - return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{6, 2} + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{7, 1} } type DERPMap struct { @@ -578,6 +578,61 @@ func (x *CoordinateResponse) GetError() string { return "" } +type IPFields struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Version int32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` + Class IPFields_IPClass `protobuf:"varint,2,opt,name=class,proto3,enum=coder.tailnet.v2.IPFields_IPClass" json:"class,omitempty"` +} + +func (x *IPFields) Reset() { + *x = IPFields{} + if protoimpl.UnsafeEnabled { + mi := &file_tailnet_proto_tailnet_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *IPFields) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IPFields) ProtoMessage() {} + +func (x *IPFields) ProtoReflect() protoreflect.Message { + mi := &file_tailnet_proto_tailnet_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IPFields.ProtoReflect.Descriptor instead. +func (*IPFields) Descriptor() ([]byte, []int) { + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{5} +} + +func (x *IPFields) GetVersion() int32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *IPFields) GetClass() IPFields_IPClass { + if x != nil { + return x.Class + } + return IPFields_PUBLIC +} + type Netcheck struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -599,15 +654,15 @@ type Netcheck struct { RegionLatency map[int64]*durationpb.Duration `protobuf:"bytes,14,rep,name=RegionLatency,proto3" json:"RegionLatency,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` RegionV4Latency map[int64]*durationpb.Duration `protobuf:"bytes,15,rep,name=RegionV4Latency,proto3" json:"RegionV4Latency,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` RegionV6Latency map[int64]*durationpb.Duration `protobuf:"bytes,16,rep,name=RegionV6Latency,proto3" json:"RegionV6Latency,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` - GlobalV4 string `protobuf:"bytes,17,opt,name=GlobalV4,proto3" json:"GlobalV4,omitempty"` - GlobalV6 string `protobuf:"bytes,18,opt,name=GlobalV6,proto3" json:"GlobalV6,omitempty"` + GlobalV4 *Netcheck_NetcheckIP `protobuf:"bytes,17,opt,name=GlobalV4,proto3" json:"GlobalV4,omitempty"` + GlobalV6 *Netcheck_NetcheckIP `protobuf:"bytes,18,opt,name=GlobalV6,proto3" json:"GlobalV6,omitempty"` CaptivePortal *wrapperspb.BoolValue `protobuf:"bytes,19,opt,name=CaptivePortal,proto3" json:"CaptivePortal,omitempty"` } func (x *Netcheck) Reset() { *x = Netcheck{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[5] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -620,7 +675,7 @@ func (x *Netcheck) String() string { func (*Netcheck) ProtoMessage() {} func (x *Netcheck) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[5] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -633,7 +688,7 @@ func (x *Netcheck) ProtoReflect() protoreflect.Message { // Deprecated: Use Netcheck.ProtoReflect.Descriptor instead. func (*Netcheck) Descriptor() ([]byte, []int) { - return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{5} + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{6} } func (x *Netcheck) GetUDP() bool { @@ -748,18 +803,18 @@ func (x *Netcheck) GetRegionV6Latency() map[int64]*durationpb.Duration { return nil } -func (x *Netcheck) GetGlobalV4() string { +func (x *Netcheck) GetGlobalV4() *Netcheck_NetcheckIP { if x != nil { return x.GlobalV4 } - return "" + return nil } -func (x *Netcheck) GetGlobalV6() string { +func (x *Netcheck) GetGlobalV6() *Netcheck_NetcheckIP { if x != nil { return x.GlobalV6 } - return "" + return nil } func (x *Netcheck) GetCaptivePortal() *wrapperspb.BoolValue { @@ -774,32 +829,32 @@ type TelemetryEvent struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Id []byte `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Time *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=time,proto3" json:"time,omitempty"` - Application string `protobuf:"bytes,3,opt,name=application,proto3" json:"application,omitempty"` - Status TelemetryEvent_Status `protobuf:"varint,4,opt,name=status,proto3,enum=coder.tailnet.v2.TelemetryEvent_Status" json:"status,omitempty"` - DisconnectionReason string `protobuf:"bytes,5,opt,name=disconnection_reason,json=disconnectionReason,proto3" json:"disconnection_reason,omitempty"` - ClientType TelemetryEvent_ClientType `protobuf:"varint,6,opt,name=client_type,json=clientType,proto3,enum=coder.tailnet.v2.TelemetryEvent_ClientType" json:"client_type,omitempty"` - NodeIdSelf string `protobuf:"bytes,7,opt,name=node_id_self,json=nodeIdSelf,proto3" json:"node_id_self,omitempty"` - NodeIdRemote string `protobuf:"bytes,8,opt,name=node_id_remote,json=nodeIdRemote,proto3" json:"node_id_remote,omitempty"` - P2PEndpoint *TelemetryEvent_P2PEndpoint `protobuf:"bytes,9,opt,name=p2p_endpoint,json=p2pEndpoint,proto3" json:"p2p_endpoint,omitempty"` - LogIpHashes map[string]*TelemetryEvent_IPFields `protobuf:"bytes,10,rep,name=log_ip_hashes,json=logIpHashes,proto3" json:"log_ip_hashes,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` - HomeDerp string `protobuf:"bytes,11,opt,name=home_derp,json=homeDerp,proto3" json:"home_derp,omitempty"` - Logs []string `protobuf:"bytes,12,rep,name=logs,proto3" json:"logs,omitempty"` - DerpMap *DERPMap `protobuf:"bytes,13,opt,name=derp_map,json=derpMap,proto3" json:"derp_map,omitempty"` - LatestNetcheck *Netcheck `protobuf:"bytes,14,opt,name=latest_netcheck,json=latestNetcheck,proto3" json:"latest_netcheck,omitempty"` - ConnectionAge *durationpb.Duration `protobuf:"bytes,15,opt,name=connection_age,json=connectionAge,proto3" json:"connection_age,omitempty"` - ConnectionSetup *durationpb.Duration `protobuf:"bytes,16,opt,name=connection_setup,json=connectionSetup,proto3" json:"connection_setup,omitempty"` - P2PSetup *durationpb.Duration `protobuf:"bytes,17,opt,name=p2p_setup,json=p2pSetup,proto3" json:"p2p_setup,omitempty"` - DerpLatency *durationpb.Duration `protobuf:"bytes,18,opt,name=derp_latency,json=derpLatency,proto3" json:"derp_latency,omitempty"` - P2PLatency *durationpb.Duration `protobuf:"bytes,19,opt,name=p2p_latency,json=p2pLatency,proto3" json:"p2p_latency,omitempty"` - ThroughputMbits *wrapperspb.FloatValue `protobuf:"bytes,20,opt,name=throughput_mbits,json=throughputMbits,proto3" json:"throughput_mbits,omitempty"` + Id []byte `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Time *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=time,proto3" json:"time,omitempty"` + Application string `protobuf:"bytes,3,opt,name=application,proto3" json:"application,omitempty"` + Status TelemetryEvent_Status `protobuf:"varint,4,opt,name=status,proto3,enum=coder.tailnet.v2.TelemetryEvent_Status" json:"status,omitempty"` + DisconnectionReason string `protobuf:"bytes,5,opt,name=disconnection_reason,json=disconnectionReason,proto3" json:"disconnection_reason,omitempty"` + ClientType TelemetryEvent_ClientType `protobuf:"varint,6,opt,name=client_type,json=clientType,proto3,enum=coder.tailnet.v2.TelemetryEvent_ClientType" json:"client_type,omitempty"` + NodeIdSelf []byte `protobuf:"bytes,7,opt,name=node_id_self,json=nodeIdSelf,proto3" json:"node_id_self,omitempty"` + NodeIdRemote []byte `protobuf:"bytes,8,opt,name=node_id_remote,json=nodeIdRemote,proto3" json:"node_id_remote,omitempty"` + P2PEndpoint *TelemetryEvent_P2PEndpoint `protobuf:"bytes,9,opt,name=p2p_endpoint,json=p2pEndpoint,proto3" json:"p2p_endpoint,omitempty"` + LogIpHashes map[string]*IPFields `protobuf:"bytes,10,rep,name=log_ip_hashes,json=logIpHashes,proto3" json:"log_ip_hashes,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + HomeDerp string `protobuf:"bytes,11,opt,name=home_derp,json=homeDerp,proto3" json:"home_derp,omitempty"` + Logs []string `protobuf:"bytes,12,rep,name=logs,proto3" json:"logs,omitempty"` + DerpMap *DERPMap `protobuf:"bytes,13,opt,name=derp_map,json=derpMap,proto3" json:"derp_map,omitempty"` + LatestNetcheck *Netcheck `protobuf:"bytes,14,opt,name=latest_netcheck,json=latestNetcheck,proto3" json:"latest_netcheck,omitempty"` + ConnectionAge *durationpb.Duration `protobuf:"bytes,15,opt,name=connection_age,json=connectionAge,proto3" json:"connection_age,omitempty"` + ConnectionSetup *durationpb.Duration `protobuf:"bytes,16,opt,name=connection_setup,json=connectionSetup,proto3" json:"connection_setup,omitempty"` + P2PSetup *durationpb.Duration `protobuf:"bytes,17,opt,name=p2p_setup,json=p2pSetup,proto3" json:"p2p_setup,omitempty"` + DerpLatency *durationpb.Duration `protobuf:"bytes,18,opt,name=derp_latency,json=derpLatency,proto3" json:"derp_latency,omitempty"` + P2PLatency *durationpb.Duration `protobuf:"bytes,19,opt,name=p2p_latency,json=p2pLatency,proto3" json:"p2p_latency,omitempty"` + ThroughputMbits *wrapperspb.FloatValue `protobuf:"bytes,20,opt,name=throughput_mbits,json=throughputMbits,proto3" json:"throughput_mbits,omitempty"` } func (x *TelemetryEvent) Reset() { *x = TelemetryEvent{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[6] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -812,7 +867,7 @@ func (x *TelemetryEvent) String() string { func (*TelemetryEvent) ProtoMessage() {} func (x *TelemetryEvent) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[6] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -825,7 +880,7 @@ func (x *TelemetryEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use TelemetryEvent.ProtoReflect.Descriptor instead. func (*TelemetryEvent) Descriptor() ([]byte, []int) { - return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{6} + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{7} } func (x *TelemetryEvent) GetId() []byte { @@ -870,18 +925,18 @@ func (x *TelemetryEvent) GetClientType() TelemetryEvent_ClientType { return TelemetryEvent_CLI } -func (x *TelemetryEvent) GetNodeIdSelf() string { +func (x *TelemetryEvent) GetNodeIdSelf() []byte { if x != nil { return x.NodeIdSelf } - return "" + return nil } -func (x *TelemetryEvent) GetNodeIdRemote() string { +func (x *TelemetryEvent) GetNodeIdRemote() []byte { if x != nil { return x.NodeIdRemote } - return "" + return nil } func (x *TelemetryEvent) GetP2PEndpoint() *TelemetryEvent_P2PEndpoint { @@ -891,7 +946,7 @@ func (x *TelemetryEvent) GetP2PEndpoint() *TelemetryEvent_P2PEndpoint { return nil } -func (x *TelemetryEvent) GetLogIpHashes() map[string]*TelemetryEvent_IPFields { +func (x *TelemetryEvent) GetLogIpHashes() map[string]*IPFields { if x != nil { return x.LogIpHashes } @@ -979,7 +1034,7 @@ type TelemetryRequest struct { func (x *TelemetryRequest) Reset() { *x = TelemetryRequest{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[7] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -992,7 +1047,7 @@ func (x *TelemetryRequest) String() string { func (*TelemetryRequest) ProtoMessage() {} func (x *TelemetryRequest) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[7] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1005,7 +1060,7 @@ func (x *TelemetryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TelemetryRequest.ProtoReflect.Descriptor instead. func (*TelemetryRequest) Descriptor() ([]byte, []int) { - return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{7} + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{8} } func (x *TelemetryRequest) GetEvents() []*TelemetryEvent { @@ -1024,7 +1079,7 @@ type TelemetryResponse struct { func (x *TelemetryResponse) Reset() { *x = TelemetryResponse{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[8] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1037,7 +1092,7 @@ func (x *TelemetryResponse) String() string { func (*TelemetryResponse) ProtoMessage() {} func (x *TelemetryResponse) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[8] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1050,7 +1105,7 @@ func (x *TelemetryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use TelemetryResponse.ProtoReflect.Descriptor instead. func (*TelemetryResponse) Descriptor() ([]byte, []int) { - return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{8} + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{9} } type DERPMap_HomeParams struct { @@ -1064,7 +1119,7 @@ type DERPMap_HomeParams struct { func (x *DERPMap_HomeParams) Reset() { *x = DERPMap_HomeParams{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[9] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1077,7 +1132,7 @@ func (x *DERPMap_HomeParams) String() string { func (*DERPMap_HomeParams) ProtoMessage() {} func (x *DERPMap_HomeParams) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[9] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1116,7 +1171,7 @@ type DERPMap_Region struct { func (x *DERPMap_Region) Reset() { *x = DERPMap_Region{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[10] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1129,7 +1184,7 @@ func (x *DERPMap_Region) String() string { func (*DERPMap_Region) ProtoMessage() {} func (x *DERPMap_Region) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[10] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1210,7 +1265,7 @@ type DERPMap_Region_Node struct { func (x *DERPMap_Region_Node) Reset() { *x = DERPMap_Region_Node{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[13] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1223,7 +1278,7 @@ func (x *DERPMap_Region_Node) String() string { func (*DERPMap_Region_Node) ProtoMessage() {} func (x *DERPMap_Region_Node) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[13] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1341,7 +1396,7 @@ type CoordinateRequest_UpdateSelf struct { func (x *CoordinateRequest_UpdateSelf) Reset() { *x = CoordinateRequest_UpdateSelf{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[16] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1354,7 +1409,7 @@ func (x *CoordinateRequest_UpdateSelf) String() string { func (*CoordinateRequest_UpdateSelf) ProtoMessage() {} func (x *CoordinateRequest_UpdateSelf) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[16] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1386,7 +1441,7 @@ type CoordinateRequest_Disconnect struct { func (x *CoordinateRequest_Disconnect) Reset() { *x = CoordinateRequest_Disconnect{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[17] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1399,7 +1454,7 @@ func (x *CoordinateRequest_Disconnect) String() string { func (*CoordinateRequest_Disconnect) ProtoMessage() {} func (x *CoordinateRequest_Disconnect) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[17] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1426,7 +1481,7 @@ type CoordinateRequest_Tunnel struct { func (x *CoordinateRequest_Tunnel) Reset() { *x = CoordinateRequest_Tunnel{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[18] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1439,7 +1494,7 @@ func (x *CoordinateRequest_Tunnel) String() string { func (*CoordinateRequest_Tunnel) ProtoMessage() {} func (x *CoordinateRequest_Tunnel) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[18] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1477,7 +1532,7 @@ type CoordinateRequest_ReadyForHandshake struct { func (x *CoordinateRequest_ReadyForHandshake) Reset() { *x = CoordinateRequest_ReadyForHandshake{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[19] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1490,7 +1545,7 @@ func (x *CoordinateRequest_ReadyForHandshake) String() string { func (*CoordinateRequest_ReadyForHandshake) ProtoMessage() {} func (x *CoordinateRequest_ReadyForHandshake) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[19] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1527,7 +1582,7 @@ type CoordinateResponse_PeerUpdate struct { func (x *CoordinateResponse_PeerUpdate) Reset() { *x = CoordinateResponse_PeerUpdate{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[20] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1540,7 +1595,7 @@ func (x *CoordinateResponse_PeerUpdate) String() string { func (*CoordinateResponse_PeerUpdate) ProtoMessage() {} func (x *CoordinateResponse_PeerUpdate) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[20] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1584,32 +1639,32 @@ func (x *CoordinateResponse_PeerUpdate) GetReason() string { return "" } -type TelemetryEvent_IPFields struct { +type Netcheck_NetcheckIP struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Version int32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` - Class TelemetryEvent_IPClass `protobuf:"varint,2,opt,name=class,proto3,enum=coder.tailnet.v2.TelemetryEvent_IPClass" json:"class,omitempty"` + Hash string `protobuf:"bytes,1,opt,name=hash,proto3" json:"hash,omitempty"` + Fields *IPFields `protobuf:"bytes,2,opt,name=fields,proto3" json:"fields,omitempty"` } -func (x *TelemetryEvent_IPFields) Reset() { - *x = TelemetryEvent_IPFields{} +func (x *Netcheck_NetcheckIP) Reset() { + *x = Netcheck_NetcheckIP{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[24] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *TelemetryEvent_IPFields) String() string { +func (x *Netcheck_NetcheckIP) String() string { return protoimpl.X.MessageStringOf(x) } -func (*TelemetryEvent_IPFields) ProtoMessage() {} +func (*Netcheck_NetcheckIP) ProtoMessage() {} -func (x *TelemetryEvent_IPFields) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[24] +func (x *Netcheck_NetcheckIP) ProtoReflect() protoreflect.Message { + mi := &file_tailnet_proto_tailnet_proto_msgTypes[25] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1620,23 +1675,23 @@ func (x *TelemetryEvent_IPFields) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use TelemetryEvent_IPFields.ProtoReflect.Descriptor instead. -func (*TelemetryEvent_IPFields) Descriptor() ([]byte, []int) { - return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{6, 0} +// Deprecated: Use Netcheck_NetcheckIP.ProtoReflect.Descriptor instead. +func (*Netcheck_NetcheckIP) Descriptor() ([]byte, []int) { + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{6, 3} } -func (x *TelemetryEvent_IPFields) GetVersion() int32 { +func (x *Netcheck_NetcheckIP) GetHash() string { if x != nil { - return x.Version + return x.Hash } - return 0 + return "" } -func (x *TelemetryEvent_IPFields) GetClass() TelemetryEvent_IPClass { +func (x *Netcheck_NetcheckIP) GetFields() *IPFields { if x != nil { - return x.Class + return x.Fields } - return TelemetryEvent_PUBLIC + return nil } type TelemetryEvent_P2PEndpoint struct { @@ -1644,15 +1699,15 @@ type TelemetryEvent_P2PEndpoint struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Hash string `protobuf:"bytes,1,opt,name=hash,proto3" json:"hash,omitempty"` - Port int32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` - Fields *TelemetryEvent_IPFields `protobuf:"bytes,3,opt,name=fields,proto3" json:"fields,omitempty"` + Hash string `protobuf:"bytes,1,opt,name=hash,proto3" json:"hash,omitempty"` + Port int32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` + Fields *IPFields `protobuf:"bytes,3,opt,name=fields,proto3" json:"fields,omitempty"` } func (x *TelemetryEvent_P2PEndpoint) Reset() { *x = TelemetryEvent_P2PEndpoint{} if protoimpl.UnsafeEnabled { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[25] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1665,7 +1720,7 @@ func (x *TelemetryEvent_P2PEndpoint) String() string { func (*TelemetryEvent_P2PEndpoint) ProtoMessage() {} func (x *TelemetryEvent_P2PEndpoint) ProtoReflect() protoreflect.Message { - mi := &file_tailnet_proto_tailnet_proto_msgTypes[25] + mi := &file_tailnet_proto_tailnet_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1678,7 +1733,7 @@ func (x *TelemetryEvent_P2PEndpoint) ProtoReflect() protoreflect.Message { // Deprecated: Use TelemetryEvent_P2PEndpoint.ProtoReflect.Descriptor instead. func (*TelemetryEvent_P2PEndpoint) Descriptor() ([]byte, []int) { - return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{6, 1} + return file_tailnet_proto_tailnet_proto_rawDescGZIP(), []int{7, 0} } func (x *TelemetryEvent_P2PEndpoint) GetHash() string { @@ -1695,7 +1750,7 @@ func (x *TelemetryEvent_P2PEndpoint) GetPort() int32 { return 0 } -func (x *TelemetryEvent_P2PEndpoint) GetFields() *TelemetryEvent_IPFields { +func (x *TelemetryEvent_P2PEndpoint) GetFields() *IPFields { if x != nil { return x.Fields } @@ -1875,210 +1930,218 @@ var file_tailnet_proto_tailnet_proto_rawDesc = []byte{ 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x44, 0x49, 0x53, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x4c, 0x4f, 0x53, 0x54, 0x10, 0x03, 0x12, 0x17, 0x0a, 0x13, 0x52, 0x45, 0x41, 0x44, 0x59, 0x5f, 0x46, 0x4f, 0x52, 0x5f, 0x48, 0x41, 0x4e, 0x44, 0x53, - 0x48, 0x41, 0x4b, 0x45, 0x10, 0x04, 0x22, 0xa0, 0x09, 0x0a, 0x08, 0x4e, 0x65, 0x74, 0x63, 0x68, - 0x65, 0x63, 0x6b, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x03, 0x55, 0x44, 0x50, 0x12, 0x12, 0x0a, 0x04, 0x49, 0x50, 0x76, 0x36, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x04, 0x49, 0x50, 0x76, 0x36, 0x12, 0x12, 0x0a, 0x04, 0x49, 0x50, 0x76, - 0x34, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x49, 0x50, 0x76, 0x34, 0x12, 0x20, 0x0a, - 0x0b, 0x49, 0x50, 0x76, 0x36, 0x43, 0x61, 0x6e, 0x53, 0x65, 0x6e, 0x64, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x0b, 0x49, 0x50, 0x76, 0x36, 0x43, 0x61, 0x6e, 0x53, 0x65, 0x6e, 0x64, 0x12, - 0x20, 0x0a, 0x0b, 0x49, 0x50, 0x76, 0x34, 0x43, 0x61, 0x6e, 0x53, 0x65, 0x6e, 0x64, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x49, 0x50, 0x76, 0x34, 0x43, 0x61, 0x6e, 0x53, 0x65, 0x6e, - 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x4f, 0x53, 0x48, 0x61, 0x73, 0x49, 0x50, 0x76, 0x36, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x4f, 0x53, 0x48, 0x61, 0x73, 0x49, 0x50, 0x76, 0x36, 0x12, - 0x16, 0x0a, 0x06, 0x49, 0x43, 0x4d, 0x50, 0x76, 0x34, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x06, 0x49, 0x43, 0x4d, 0x50, 0x76, 0x34, 0x12, 0x50, 0x0a, 0x15, 0x4d, 0x61, 0x70, 0x70, 0x69, - 0x6e, 0x67, 0x56, 0x61, 0x72, 0x69, 0x65, 0x73, 0x42, 0x79, 0x44, 0x65, 0x73, 0x74, 0x49, 0x50, - 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x52, 0x15, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x72, 0x69, 0x65, - 0x73, 0x42, 0x79, 0x44, 0x65, 0x73, 0x74, 0x49, 0x50, 0x12, 0x3c, 0x0a, 0x0b, 0x48, 0x61, 0x69, - 0x72, 0x50, 0x69, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0b, 0x48, 0x61, 0x69, 0x72, - 0x50, 0x69, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x12, 0x2e, 0x0a, 0x04, 0x55, 0x50, 0x6e, 0x50, 0x18, - 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x52, 0x04, 0x55, 0x50, 0x6e, 0x50, 0x12, 0x2c, 0x0a, 0x03, 0x50, 0x4d, 0x50, 0x18, 0x0b, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x52, 0x03, 0x50, 0x4d, 0x50, 0x12, 0x2c, 0x0a, 0x03, 0x50, 0x43, 0x50, 0x18, 0x0c, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x03, - 0x50, 0x43, 0x50, 0x12, 0x24, 0x0a, 0x0d, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, - 0x44, 0x45, 0x52, 0x50, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x50, 0x72, 0x65, 0x66, - 0x65, 0x72, 0x72, 0x65, 0x64, 0x44, 0x45, 0x52, 0x50, 0x12, 0x53, 0x0a, 0x0d, 0x52, 0x65, 0x67, - 0x69, 0x6f, 0x6e, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x0e, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, - 0x2e, 0x76, 0x32, 0x2e, 0x4e, 0x65, 0x74, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x2e, 0x52, 0x65, 0x67, - 0x69, 0x6f, 0x6e, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, - 0x0d, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x59, - 0x0a, 0x0f, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x56, 0x34, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, - 0x79, 0x18, 0x0f, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, - 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4e, 0x65, 0x74, 0x63, 0x68, - 0x65, 0x63, 0x6b, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x56, 0x34, 0x4c, 0x61, 0x74, 0x65, - 0x6e, 0x63, 0x79, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0f, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, - 0x56, 0x34, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x59, 0x0a, 0x0f, 0x52, 0x65, 0x67, - 0x69, 0x6f, 0x6e, 0x56, 0x36, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x10, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, - 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4e, 0x65, 0x74, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x2e, 0x52, - 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x56, 0x36, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x52, 0x0f, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x56, 0x36, 0x4c, 0x61, 0x74, - 0x65, 0x6e, 0x63, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x56, 0x34, - 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x56, 0x34, - 0x12, 0x1a, 0x0a, 0x08, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x56, 0x36, 0x18, 0x12, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x56, 0x36, 0x12, 0x40, 0x0a, 0x0d, - 0x43, 0x61, 0x70, 0x74, 0x69, 0x76, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x61, 0x6c, 0x18, 0x13, 0x20, + 0x48, 0x41, 0x4b, 0x45, 0x10, 0x04, 0x22, 0xb2, 0x01, 0x0a, 0x08, 0x49, 0x50, 0x46, 0x69, 0x65, + 0x6c, 0x64, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x38, 0x0a, + 0x05, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, + 0x49, 0x50, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x2e, 0x49, 0x50, 0x43, 0x6c, 0x61, 0x73, 0x73, + 0x52, 0x05, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x22, 0x52, 0x0a, 0x07, 0x49, 0x50, 0x43, 0x6c, 0x61, + 0x73, 0x73, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x10, 0x00, 0x12, 0x0b, + 0x0a, 0x07, 0x50, 0x52, 0x49, 0x56, 0x41, 0x54, 0x45, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x4c, + 0x49, 0x4e, 0x4b, 0x5f, 0x4c, 0x4f, 0x43, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x55, + 0x4e, 0x49, 0x51, 0x55, 0x45, 0x5f, 0x4c, 0x4f, 0x43, 0x41, 0x4c, 0x10, 0x03, 0x12, 0x0c, 0x0a, + 0x08, 0x4c, 0x4f, 0x4f, 0x50, 0x42, 0x41, 0x43, 0x4b, 0x10, 0x04, 0x22, 0xc4, 0x0a, 0x0a, 0x08, + 0x4e, 0x65, 0x74, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x55, 0x44, 0x50, 0x12, 0x12, 0x0a, 0x04, 0x49, 0x50, + 0x76, 0x36, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x49, 0x50, 0x76, 0x36, 0x12, 0x12, + 0x0a, 0x04, 0x49, 0x50, 0x76, 0x34, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x49, 0x50, + 0x76, 0x34, 0x12, 0x20, 0x0a, 0x0b, 0x49, 0x50, 0x76, 0x36, 0x43, 0x61, 0x6e, 0x53, 0x65, 0x6e, + 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x49, 0x50, 0x76, 0x36, 0x43, 0x61, 0x6e, + 0x53, 0x65, 0x6e, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x49, 0x50, 0x76, 0x34, 0x43, 0x61, 0x6e, 0x53, + 0x65, 0x6e, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x49, 0x50, 0x76, 0x34, 0x43, + 0x61, 0x6e, 0x53, 0x65, 0x6e, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x4f, 0x53, 0x48, 0x61, 0x73, 0x49, + 0x50, 0x76, 0x36, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x4f, 0x53, 0x48, 0x61, 0x73, + 0x49, 0x50, 0x76, 0x36, 0x12, 0x16, 0x0a, 0x06, 0x49, 0x43, 0x4d, 0x50, 0x76, 0x34, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x49, 0x43, 0x4d, 0x50, 0x76, 0x34, 0x12, 0x50, 0x0a, 0x15, + 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x72, 0x69, 0x65, 0x73, 0x42, 0x79, 0x44, + 0x65, 0x73, 0x74, 0x49, 0x50, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, + 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x15, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, + 0x56, 0x61, 0x72, 0x69, 0x65, 0x73, 0x42, 0x79, 0x44, 0x65, 0x73, 0x74, 0x49, 0x50, 0x12, 0x3c, + 0x0a, 0x0b, 0x48, 0x61, 0x69, 0x72, 0x50, 0x69, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, - 0x0d, 0x43, 0x61, 0x70, 0x74, 0x69, 0x76, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x61, 0x6c, 0x1a, 0x5b, - 0x0a, 0x12, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2f, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x5d, 0x0a, 0x14, 0x52, - 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x56, 0x34, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2f, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x5d, 0x0a, 0x14, 0x52, 0x65, - 0x67, 0x69, 0x6f, 0x6e, 0x56, 0x36, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2f, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xd7, 0x0c, 0x0a, 0x0e, 0x54, 0x65, - 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x12, 0x2e, 0x0a, 0x04, - 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, - 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0b, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3f, - 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x27, - 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, - 0x32, 0x2e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x45, 0x76, 0x65, 0x6e, 0x74, - 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, - 0x31, 0x0a, 0x14, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x64, - 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x61, 0x73, - 0x6f, 0x6e, 0x12, 0x4c, 0x0a, 0x0b, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, - 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, - 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x65, 0x6c, 0x65, 0x6d, - 0x65, 0x74, 0x72, 0x79, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, - 0x54, 0x79, 0x70, 0x65, 0x52, 0x0a, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, - 0x12, 0x20, 0x0a, 0x0c, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x5f, 0x73, 0x65, 0x6c, 0x66, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x53, 0x65, - 0x6c, 0x66, 0x12, 0x24, 0x0a, 0x0e, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x5f, 0x72, 0x65, - 0x6d, 0x6f, 0x74, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6e, 0x6f, 0x64, 0x65, - 0x49, 0x64, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x12, 0x4f, 0x0a, 0x0c, 0x70, 0x32, 0x70, 0x5f, - 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, + 0x0b, 0x48, 0x61, 0x69, 0x72, 0x50, 0x69, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x12, 0x2e, 0x0a, 0x04, + 0x55, 0x50, 0x6e, 0x50, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, + 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x04, 0x55, 0x50, 0x6e, 0x50, 0x12, 0x2c, 0x0a, 0x03, + 0x50, 0x4d, 0x50, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x03, 0x50, 0x4d, 0x50, 0x12, 0x2c, 0x0a, 0x03, 0x50, 0x43, + 0x50, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x52, 0x03, 0x50, 0x43, 0x50, 0x12, 0x24, 0x0a, 0x0d, 0x50, 0x72, 0x65, 0x66, + 0x65, 0x72, 0x72, 0x65, 0x64, 0x44, 0x45, 0x52, 0x50, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0d, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x44, 0x45, 0x52, 0x50, 0x12, 0x53, + 0x0a, 0x0d, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x18, + 0x0e, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, + 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4e, 0x65, 0x74, 0x63, 0x68, 0x65, 0x63, + 0x6b, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0d, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x4c, 0x61, 0x74, 0x65, + 0x6e, 0x63, 0x79, 0x12, 0x59, 0x0a, 0x0f, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x56, 0x34, 0x4c, + 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x0f, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, + 0x4e, 0x65, 0x74, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x56, + 0x34, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0f, 0x52, + 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x56, 0x34, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x59, + 0x0a, 0x0f, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x56, 0x36, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, + 0x79, 0x18, 0x10, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4e, 0x65, 0x74, 0x63, 0x68, + 0x65, 0x63, 0x6b, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x56, 0x36, 0x4c, 0x61, 0x74, 0x65, + 0x6e, 0x63, 0x79, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0f, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, + 0x56, 0x36, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x41, 0x0a, 0x08, 0x47, 0x6c, 0x6f, + 0x62, 0x61, 0x6c, 0x56, 0x34, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x63, 0x6f, + 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4e, + 0x65, 0x74, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x2e, 0x4e, 0x65, 0x74, 0x63, 0x68, 0x65, 0x63, 0x6b, + 0x49, 0x50, 0x52, 0x08, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x56, 0x34, 0x12, 0x41, 0x0a, 0x08, + 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x56, 0x36, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, - 0x32, 0x2e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x45, 0x76, 0x65, 0x6e, 0x74, - 0x2e, 0x50, 0x32, 0x50, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x0b, 0x70, 0x32, - 0x70, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x55, 0x0a, 0x0d, 0x6c, 0x6f, 0x67, - 0x5f, 0x69, 0x70, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x31, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, - 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x45, 0x76, 0x65, - 0x6e, 0x74, 0x2e, 0x4c, 0x6f, 0x67, 0x49, 0x70, 0x48, 0x61, 0x73, 0x68, 0x65, 0x73, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x52, 0x0b, 0x6c, 0x6f, 0x67, 0x49, 0x70, 0x48, 0x61, 0x73, 0x68, 0x65, 0x73, - 0x12, 0x1b, 0x0a, 0x09, 0x68, 0x6f, 0x6d, 0x65, 0x5f, 0x64, 0x65, 0x72, 0x70, 0x18, 0x0b, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x6d, 0x65, 0x44, 0x65, 0x72, 0x70, 0x12, 0x12, 0x0a, - 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x6c, 0x6f, 0x67, - 0x73, 0x12, 0x34, 0x0a, 0x08, 0x64, 0x65, 0x72, 0x70, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x0d, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, - 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x44, 0x45, 0x52, 0x50, 0x4d, 0x61, 0x70, 0x52, 0x07, - 0x64, 0x65, 0x72, 0x70, 0x4d, 0x61, 0x70, 0x12, 0x43, 0x0a, 0x0f, 0x6c, 0x61, 0x74, 0x65, 0x73, - 0x74, 0x5f, 0x6e, 0x65, 0x74, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, - 0x2e, 0x76, 0x32, 0x2e, 0x4e, 0x65, 0x74, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x0e, 0x6c, 0x61, - 0x74, 0x65, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x40, 0x0a, 0x0e, - 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x61, 0x67, 0x65, 0x18, 0x0f, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, - 0x0d, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x67, 0x65, 0x12, 0x44, - 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x65, 0x74, - 0x75, 0x70, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x32, 0x2e, 0x4e, 0x65, 0x74, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x2e, 0x4e, 0x65, 0x74, 0x63, 0x68, + 0x65, 0x63, 0x6b, 0x49, 0x50, 0x52, 0x08, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x56, 0x36, 0x12, + 0x40, 0x0a, 0x0d, 0x43, 0x61, 0x70, 0x74, 0x69, 0x76, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x61, 0x6c, + 0x18, 0x13, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x52, 0x0d, 0x43, 0x61, 0x70, 0x74, 0x69, 0x76, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x61, + 0x6c, 0x1a, 0x5b, 0x0a, 0x12, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x4c, 0x61, 0x74, 0x65, 0x6e, + 0x63, 0x79, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2f, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, - 0x65, 0x74, 0x75, 0x70, 0x12, 0x36, 0x0a, 0x09, 0x70, 0x32, 0x70, 0x5f, 0x73, 0x65, 0x74, 0x75, - 0x70, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x69, 0x6f, 0x6e, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x5d, + 0x0a, 0x14, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x56, 0x34, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, + 0x79, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2f, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x08, 0x70, 0x32, 0x70, 0x53, 0x65, 0x74, 0x75, 0x70, 0x12, 0x3c, 0x0a, 0x0c, - 0x64, 0x65, 0x72, 0x70, 0x5f, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x12, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, - 0x65, 0x72, 0x70, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x3a, 0x0a, 0x0b, 0x70, 0x32, - 0x70, 0x5f, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x13, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x70, 0x32, 0x70, 0x4c, - 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x46, 0x0a, 0x10, 0x74, 0x68, 0x72, 0x6f, 0x75, 0x67, - 0x68, 0x70, 0x75, 0x74, 0x5f, 0x6d, 0x62, 0x69, 0x74, 0x73, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x46, 0x6c, 0x6f, 0x61, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0f, 0x74, - 0x68, 0x72, 0x6f, 0x75, 0x67, 0x68, 0x70, 0x75, 0x74, 0x4d, 0x62, 0x69, 0x74, 0x73, 0x1a, 0x64, - 0x0a, 0x08, 0x49, 0x50, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x76, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x3e, 0x0a, 0x05, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x28, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, - 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, - 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x49, 0x50, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x52, 0x05, 0x63, - 0x6c, 0x61, 0x73, 0x73, 0x1a, 0x78, 0x0a, 0x0b, 0x50, 0x32, 0x50, 0x45, 0x6e, 0x64, 0x70, 0x6f, - 0x69, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x41, 0x0a, 0x06, 0x66, - 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x63, 0x6f, - 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, - 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x49, 0x50, - 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x1a, 0x69, - 0x0a, 0x10, 0x4c, 0x6f, 0x67, 0x49, 0x70, 0x48, 0x61, 0x73, 0x68, 0x65, 0x73, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x12, 0x3f, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, - 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, - 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x49, 0x50, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x52, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x29, 0x0a, 0x06, 0x53, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x12, 0x0d, 0x0a, 0x09, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x45, 0x44, - 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x44, 0x49, 0x53, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, - 0x45, 0x44, 0x10, 0x01, 0x22, 0x39, 0x0a, 0x0a, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x79, - 0x70, 0x65, 0x12, 0x07, 0x0a, 0x03, 0x43, 0x4c, 0x49, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x41, - 0x47, 0x45, 0x4e, 0x54, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x4f, 0x44, 0x45, 0x52, 0x44, - 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x57, 0x53, 0x50, 0x52, 0x4f, 0x58, 0x59, 0x10, 0x03, 0x22, - 0x52, 0x0a, 0x07, 0x49, 0x50, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x55, - 0x42, 0x4c, 0x49, 0x43, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x52, 0x49, 0x56, 0x41, 0x54, - 0x45, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x4c, 0x49, 0x4e, 0x4b, 0x5f, 0x4c, 0x4f, 0x43, 0x41, - 0x4c, 0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x55, 0x4e, 0x49, 0x51, 0x55, 0x45, 0x5f, 0x4c, 0x4f, - 0x43, 0x41, 0x4c, 0x10, 0x03, 0x12, 0x0c, 0x0a, 0x08, 0x4c, 0x4f, 0x4f, 0x50, 0x42, 0x41, 0x43, - 0x4b, 0x10, 0x04, 0x22, 0x4c, 0x0a, 0x10, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x6f, 0x6e, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x5d, 0x0a, + 0x14, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x56, 0x36, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2f, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x54, 0x0a, 0x0a, + 0x4e, 0x65, 0x74, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x49, 0x50, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, + 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x32, + 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, + 0x32, 0x2e, 0x49, 0x50, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, + 0x64, 0x73, 0x22, 0xff, 0x0a, 0x0a, 0x0e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, + 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x02, 0x69, 0x64, 0x12, 0x2e, 0x0a, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x04, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x70, 0x70, 0x6c, + 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3f, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x65, 0x6c, 0x65, 0x6d, - 0x65, 0x74, 0x72, 0x79, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, - 0x73, 0x22, 0x13, 0x0a, 0x11, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x98, 0x02, 0x0a, 0x07, 0x54, 0x61, 0x69, 0x6c, 0x6e, - 0x65, 0x74, 0x12, 0x58, 0x0a, 0x0d, 0x50, 0x6f, 0x73, 0x74, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, - 0x74, 0x72, 0x79, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, - 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x65, 0x74, 0x72, 0x79, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x31, 0x0a, 0x14, 0x64, 0x69, 0x73, 0x63, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x4c, 0x0a, 0x0b, 0x63, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, + 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x45, 0x76, 0x65, + 0x6e, 0x74, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0a, 0x63, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x20, 0x0a, 0x0c, 0x6e, 0x6f, 0x64, + 0x65, 0x5f, 0x69, 0x64, 0x5f, 0x73, 0x65, 0x6c, 0x66, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x0a, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x53, 0x65, 0x6c, 0x66, 0x12, 0x24, 0x0a, 0x0e, 0x6e, + 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x5f, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x52, 0x65, 0x6d, 0x6f, 0x74, + 0x65, 0x12, 0x4f, 0x0a, 0x0c, 0x70, 0x32, 0x70, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, + 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x65, 0x6c, 0x65, 0x6d, - 0x65, 0x74, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, 0x0e, - 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x44, 0x45, 0x52, 0x50, 0x4d, 0x61, 0x70, 0x73, 0x12, 0x27, + 0x65, 0x74, 0x72, 0x79, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x32, 0x50, 0x45, 0x6e, 0x64, + 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x0b, 0x70, 0x32, 0x70, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, + 0x6e, 0x74, 0x12, 0x55, 0x0a, 0x0d, 0x6c, 0x6f, 0x67, 0x5f, 0x69, 0x70, 0x5f, 0x68, 0x61, 0x73, + 0x68, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x63, 0x6f, 0x64, 0x65, + 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x65, 0x6c, + 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x4c, 0x6f, 0x67, 0x49, + 0x70, 0x48, 0x61, 0x73, 0x68, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x6c, 0x6f, + 0x67, 0x49, 0x70, 0x48, 0x61, 0x73, 0x68, 0x65, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x68, 0x6f, 0x6d, + 0x65, 0x5f, 0x64, 0x65, 0x72, 0x70, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, + 0x6d, 0x65, 0x44, 0x65, 0x72, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x18, 0x0c, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x12, 0x34, 0x0a, 0x08, 0x64, 0x65, + 0x72, 0x70, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, + 0x44, 0x45, 0x52, 0x50, 0x4d, 0x61, 0x70, 0x52, 0x07, 0x64, 0x65, 0x72, 0x70, 0x4d, 0x61, 0x70, + 0x12, 0x43, 0x0a, 0x0f, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x6e, 0x65, 0x74, 0x63, 0x68, + 0x65, 0x63, 0x6b, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x64, 0x65, + 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4e, 0x65, 0x74, + 0x63, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x0e, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x4e, 0x65, 0x74, + 0x63, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x40, 0x0a, 0x0e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x5f, 0x61, 0x67, 0x65, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x67, 0x65, 0x12, 0x44, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x65, 0x74, 0x75, 0x70, 0x18, 0x10, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0f, 0x63, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x75, 0x70, 0x12, 0x36, 0x0a, + 0x09, 0x70, 0x32, 0x70, 0x5f, 0x73, 0x65, 0x74, 0x75, 0x70, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x70, 0x32, 0x70, + 0x53, 0x65, 0x74, 0x75, 0x70, 0x12, 0x3c, 0x0a, 0x0c, 0x64, 0x65, 0x72, 0x70, 0x5f, 0x6c, 0x61, + 0x74, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, 0x65, 0x72, 0x70, 0x4c, 0x61, 0x74, 0x65, + 0x6e, 0x63, 0x79, 0x12, 0x3a, 0x0a, 0x0b, 0x70, 0x32, 0x70, 0x5f, 0x6c, 0x61, 0x74, 0x65, 0x6e, + 0x63, 0x79, 0x18, 0x13, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x70, 0x32, 0x70, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, + 0x46, 0x0a, 0x10, 0x74, 0x68, 0x72, 0x6f, 0x75, 0x67, 0x68, 0x70, 0x75, 0x74, 0x5f, 0x6d, 0x62, + 0x69, 0x74, 0x73, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x6c, 0x6f, 0x61, + 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0f, 0x74, 0x68, 0x72, 0x6f, 0x75, 0x67, 0x68, 0x70, + 0x75, 0x74, 0x4d, 0x62, 0x69, 0x74, 0x73, 0x1a, 0x69, 0x0a, 0x0b, 0x50, 0x32, 0x50, 0x45, 0x6e, + 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, + 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x32, + 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, - 0x32, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x44, 0x45, 0x52, 0x50, 0x4d, 0x61, 0x70, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, - 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x44, 0x45, 0x52, 0x50, 0x4d, - 0x61, 0x70, 0x30, 0x01, 0x12, 0x5b, 0x0a, 0x0a, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, - 0x74, 0x65, 0x12, 0x23, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, - 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, - 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x6f, 0x6f, 0x72, 0x64, - 0x69, 0x6e, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, - 0x01, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, - 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x74, - 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x32, 0x2e, 0x49, 0x50, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, + 0x64, 0x73, 0x1a, 0x5a, 0x0a, 0x10, 0x4c, 0x6f, 0x67, 0x49, 0x70, 0x48, 0x61, 0x73, 0x68, 0x65, + 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x49, 0x50, 0x46, 0x69, 0x65, + 0x6c, 0x64, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x29, + 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0d, 0x0a, 0x09, 0x43, 0x4f, 0x4e, 0x4e, + 0x45, 0x43, 0x54, 0x45, 0x44, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x44, 0x49, 0x53, 0x43, 0x4f, + 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x45, 0x44, 0x10, 0x01, 0x22, 0x39, 0x0a, 0x0a, 0x43, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x07, 0x0a, 0x03, 0x43, 0x4c, 0x49, 0x10, 0x00, + 0x12, 0x09, 0x0a, 0x05, 0x41, 0x47, 0x45, 0x4e, 0x54, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x43, + 0x4f, 0x44, 0x45, 0x52, 0x44, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x57, 0x53, 0x50, 0x52, 0x4f, + 0x58, 0x59, 0x10, 0x03, 0x22, 0x4c, 0x0a, 0x10, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, + 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, + 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x65, 0x6c, 0x65, + 0x6d, 0x65, 0x74, 0x72, 0x79, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, + 0x74, 0x73, 0x22, 0x13, 0x0a, 0x11, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x98, 0x02, 0x0a, 0x07, 0x54, 0x61, 0x69, 0x6c, + 0x6e, 0x65, 0x74, 0x12, 0x58, 0x0a, 0x0d, 0x50, 0x6f, 0x73, 0x74, 0x54, 0x65, 0x6c, 0x65, 0x6d, + 0x65, 0x74, 0x72, 0x79, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, + 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, + 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x65, 0x6c, 0x65, + 0x6d, 0x65, 0x74, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, + 0x0e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x44, 0x45, 0x52, 0x50, 0x4d, 0x61, 0x70, 0x73, 0x12, + 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, + 0x76, 0x32, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x44, 0x45, 0x52, 0x50, 0x4d, 0x61, 0x70, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x44, 0x45, 0x52, 0x50, + 0x4d, 0x61, 0x70, 0x30, 0x01, 0x12, 0x5b, 0x0a, 0x0a, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, + 0x61, 0x74, 0x65, 0x12, 0x23, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, + 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x6f, 0x6f, 0x72, + 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, + 0x30, 0x01, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, + 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2094,102 +2157,106 @@ func file_tailnet_proto_tailnet_proto_rawDescGZIP() []byte { } var file_tailnet_proto_tailnet_proto_enumTypes = make([]protoimpl.EnumInfo, 4) -var file_tailnet_proto_tailnet_proto_msgTypes = make([]protoimpl.MessageInfo, 27) +var file_tailnet_proto_tailnet_proto_msgTypes = make([]protoimpl.MessageInfo, 28) var file_tailnet_proto_tailnet_proto_goTypes = []interface{}{ - (CoordinateResponse_PeerUpdate_Kind)(0), // 0: coder.tailnet.v2.CoordinateResponse.PeerUpdate.Kind - (TelemetryEvent_Status)(0), // 1: coder.tailnet.v2.TelemetryEvent.Status - (TelemetryEvent_ClientType)(0), // 2: coder.tailnet.v2.TelemetryEvent.ClientType - (TelemetryEvent_IPClass)(0), // 3: coder.tailnet.v2.TelemetryEvent.IPClass - (*DERPMap)(nil), // 4: coder.tailnet.v2.DERPMap - (*StreamDERPMapsRequest)(nil), // 5: coder.tailnet.v2.StreamDERPMapsRequest - (*Node)(nil), // 6: coder.tailnet.v2.Node - (*CoordinateRequest)(nil), // 7: coder.tailnet.v2.CoordinateRequest - (*CoordinateResponse)(nil), // 8: coder.tailnet.v2.CoordinateResponse - (*Netcheck)(nil), // 9: coder.tailnet.v2.Netcheck - (*TelemetryEvent)(nil), // 10: coder.tailnet.v2.TelemetryEvent - (*TelemetryRequest)(nil), // 11: coder.tailnet.v2.TelemetryRequest - (*TelemetryResponse)(nil), // 12: coder.tailnet.v2.TelemetryResponse - (*DERPMap_HomeParams)(nil), // 13: coder.tailnet.v2.DERPMap.HomeParams - (*DERPMap_Region)(nil), // 14: coder.tailnet.v2.DERPMap.Region - nil, // 15: coder.tailnet.v2.DERPMap.RegionsEntry - nil, // 16: coder.tailnet.v2.DERPMap.HomeParams.RegionScoreEntry - (*DERPMap_Region_Node)(nil), // 17: coder.tailnet.v2.DERPMap.Region.Node - nil, // 18: coder.tailnet.v2.Node.DerpLatencyEntry - nil, // 19: coder.tailnet.v2.Node.DerpForcedWebsocketEntry - (*CoordinateRequest_UpdateSelf)(nil), // 20: coder.tailnet.v2.CoordinateRequest.UpdateSelf - (*CoordinateRequest_Disconnect)(nil), // 21: coder.tailnet.v2.CoordinateRequest.Disconnect - (*CoordinateRequest_Tunnel)(nil), // 22: coder.tailnet.v2.CoordinateRequest.Tunnel - (*CoordinateRequest_ReadyForHandshake)(nil), // 23: coder.tailnet.v2.CoordinateRequest.ReadyForHandshake - (*CoordinateResponse_PeerUpdate)(nil), // 24: coder.tailnet.v2.CoordinateResponse.PeerUpdate - nil, // 25: coder.tailnet.v2.Netcheck.RegionLatencyEntry - nil, // 26: coder.tailnet.v2.Netcheck.RegionV4LatencyEntry - nil, // 27: coder.tailnet.v2.Netcheck.RegionV6LatencyEntry - (*TelemetryEvent_IPFields)(nil), // 28: coder.tailnet.v2.TelemetryEvent.IPFields - (*TelemetryEvent_P2PEndpoint)(nil), // 29: coder.tailnet.v2.TelemetryEvent.P2PEndpoint - nil, // 30: coder.tailnet.v2.TelemetryEvent.LogIpHashesEntry - (*timestamppb.Timestamp)(nil), // 31: google.protobuf.Timestamp - (*wrapperspb.BoolValue)(nil), // 32: google.protobuf.BoolValue - (*durationpb.Duration)(nil), // 33: google.protobuf.Duration - (*wrapperspb.FloatValue)(nil), // 34: google.protobuf.FloatValue + (CoordinateResponse_PeerUpdate_Kind)(0), // 0: coder.tailnet.v2.CoordinateResponse.PeerUpdate.Kind + (IPFields_IPClass)(0), // 1: coder.tailnet.v2.IPFields.IPClass + (TelemetryEvent_Status)(0), // 2: coder.tailnet.v2.TelemetryEvent.Status + (TelemetryEvent_ClientType)(0), // 3: coder.tailnet.v2.TelemetryEvent.ClientType + (*DERPMap)(nil), // 4: coder.tailnet.v2.DERPMap + (*StreamDERPMapsRequest)(nil), // 5: coder.tailnet.v2.StreamDERPMapsRequest + (*Node)(nil), // 6: coder.tailnet.v2.Node + (*CoordinateRequest)(nil), // 7: coder.tailnet.v2.CoordinateRequest + (*CoordinateResponse)(nil), // 8: coder.tailnet.v2.CoordinateResponse + (*IPFields)(nil), // 9: coder.tailnet.v2.IPFields + (*Netcheck)(nil), // 10: coder.tailnet.v2.Netcheck + (*TelemetryEvent)(nil), // 11: coder.tailnet.v2.TelemetryEvent + (*TelemetryRequest)(nil), // 12: coder.tailnet.v2.TelemetryRequest + (*TelemetryResponse)(nil), // 13: coder.tailnet.v2.TelemetryResponse + (*DERPMap_HomeParams)(nil), // 14: coder.tailnet.v2.DERPMap.HomeParams + (*DERPMap_Region)(nil), // 15: coder.tailnet.v2.DERPMap.Region + nil, // 16: coder.tailnet.v2.DERPMap.RegionsEntry + nil, // 17: coder.tailnet.v2.DERPMap.HomeParams.RegionScoreEntry + (*DERPMap_Region_Node)(nil), // 18: coder.tailnet.v2.DERPMap.Region.Node + nil, // 19: coder.tailnet.v2.Node.DerpLatencyEntry + nil, // 20: coder.tailnet.v2.Node.DerpForcedWebsocketEntry + (*CoordinateRequest_UpdateSelf)(nil), // 21: coder.tailnet.v2.CoordinateRequest.UpdateSelf + (*CoordinateRequest_Disconnect)(nil), // 22: coder.tailnet.v2.CoordinateRequest.Disconnect + (*CoordinateRequest_Tunnel)(nil), // 23: coder.tailnet.v2.CoordinateRequest.Tunnel + (*CoordinateRequest_ReadyForHandshake)(nil), // 24: coder.tailnet.v2.CoordinateRequest.ReadyForHandshake + (*CoordinateResponse_PeerUpdate)(nil), // 25: coder.tailnet.v2.CoordinateResponse.PeerUpdate + nil, // 26: coder.tailnet.v2.Netcheck.RegionLatencyEntry + nil, // 27: coder.tailnet.v2.Netcheck.RegionV4LatencyEntry + nil, // 28: coder.tailnet.v2.Netcheck.RegionV6LatencyEntry + (*Netcheck_NetcheckIP)(nil), // 29: coder.tailnet.v2.Netcheck.NetcheckIP + (*TelemetryEvent_P2PEndpoint)(nil), // 30: coder.tailnet.v2.TelemetryEvent.P2PEndpoint + nil, // 31: coder.tailnet.v2.TelemetryEvent.LogIpHashesEntry + (*timestamppb.Timestamp)(nil), // 32: google.protobuf.Timestamp + (*wrapperspb.BoolValue)(nil), // 33: google.protobuf.BoolValue + (*durationpb.Duration)(nil), // 34: google.protobuf.Duration + (*wrapperspb.FloatValue)(nil), // 35: google.protobuf.FloatValue } var file_tailnet_proto_tailnet_proto_depIdxs = []int32{ - 13, // 0: coder.tailnet.v2.DERPMap.home_params:type_name -> coder.tailnet.v2.DERPMap.HomeParams - 15, // 1: coder.tailnet.v2.DERPMap.regions:type_name -> coder.tailnet.v2.DERPMap.RegionsEntry - 31, // 2: coder.tailnet.v2.Node.as_of:type_name -> google.protobuf.Timestamp - 18, // 3: coder.tailnet.v2.Node.derp_latency:type_name -> coder.tailnet.v2.Node.DerpLatencyEntry - 19, // 4: coder.tailnet.v2.Node.derp_forced_websocket:type_name -> coder.tailnet.v2.Node.DerpForcedWebsocketEntry - 20, // 5: coder.tailnet.v2.CoordinateRequest.update_self:type_name -> coder.tailnet.v2.CoordinateRequest.UpdateSelf - 21, // 6: coder.tailnet.v2.CoordinateRequest.disconnect:type_name -> coder.tailnet.v2.CoordinateRequest.Disconnect - 22, // 7: coder.tailnet.v2.CoordinateRequest.add_tunnel:type_name -> coder.tailnet.v2.CoordinateRequest.Tunnel - 22, // 8: coder.tailnet.v2.CoordinateRequest.remove_tunnel:type_name -> coder.tailnet.v2.CoordinateRequest.Tunnel - 23, // 9: coder.tailnet.v2.CoordinateRequest.ready_for_handshake:type_name -> coder.tailnet.v2.CoordinateRequest.ReadyForHandshake - 24, // 10: coder.tailnet.v2.CoordinateResponse.peer_updates:type_name -> coder.tailnet.v2.CoordinateResponse.PeerUpdate - 32, // 11: coder.tailnet.v2.Netcheck.MappingVariesByDestIP:type_name -> google.protobuf.BoolValue - 32, // 12: coder.tailnet.v2.Netcheck.HairPinning:type_name -> google.protobuf.BoolValue - 32, // 13: coder.tailnet.v2.Netcheck.UPnP:type_name -> google.protobuf.BoolValue - 32, // 14: coder.tailnet.v2.Netcheck.PMP:type_name -> google.protobuf.BoolValue - 32, // 15: coder.tailnet.v2.Netcheck.PCP:type_name -> google.protobuf.BoolValue - 25, // 16: coder.tailnet.v2.Netcheck.RegionLatency:type_name -> coder.tailnet.v2.Netcheck.RegionLatencyEntry - 26, // 17: coder.tailnet.v2.Netcheck.RegionV4Latency:type_name -> coder.tailnet.v2.Netcheck.RegionV4LatencyEntry - 27, // 18: coder.tailnet.v2.Netcheck.RegionV6Latency:type_name -> coder.tailnet.v2.Netcheck.RegionV6LatencyEntry - 32, // 19: coder.tailnet.v2.Netcheck.CaptivePortal:type_name -> google.protobuf.BoolValue - 31, // 20: coder.tailnet.v2.TelemetryEvent.time:type_name -> google.protobuf.Timestamp - 1, // 21: coder.tailnet.v2.TelemetryEvent.status:type_name -> coder.tailnet.v2.TelemetryEvent.Status - 2, // 22: coder.tailnet.v2.TelemetryEvent.client_type:type_name -> coder.tailnet.v2.TelemetryEvent.ClientType - 29, // 23: coder.tailnet.v2.TelemetryEvent.p2p_endpoint:type_name -> coder.tailnet.v2.TelemetryEvent.P2PEndpoint - 30, // 24: coder.tailnet.v2.TelemetryEvent.log_ip_hashes:type_name -> coder.tailnet.v2.TelemetryEvent.LogIpHashesEntry - 4, // 25: coder.tailnet.v2.TelemetryEvent.derp_map:type_name -> coder.tailnet.v2.DERPMap - 9, // 26: coder.tailnet.v2.TelemetryEvent.latest_netcheck:type_name -> coder.tailnet.v2.Netcheck - 33, // 27: coder.tailnet.v2.TelemetryEvent.connection_age:type_name -> google.protobuf.Duration - 33, // 28: coder.tailnet.v2.TelemetryEvent.connection_setup:type_name -> google.protobuf.Duration - 33, // 29: coder.tailnet.v2.TelemetryEvent.p2p_setup:type_name -> google.protobuf.Duration - 33, // 30: coder.tailnet.v2.TelemetryEvent.derp_latency:type_name -> google.protobuf.Duration - 33, // 31: coder.tailnet.v2.TelemetryEvent.p2p_latency:type_name -> google.protobuf.Duration - 34, // 32: coder.tailnet.v2.TelemetryEvent.throughput_mbits:type_name -> google.protobuf.FloatValue - 10, // 33: coder.tailnet.v2.TelemetryRequest.events:type_name -> coder.tailnet.v2.TelemetryEvent - 16, // 34: coder.tailnet.v2.DERPMap.HomeParams.region_score:type_name -> coder.tailnet.v2.DERPMap.HomeParams.RegionScoreEntry - 17, // 35: coder.tailnet.v2.DERPMap.Region.nodes:type_name -> coder.tailnet.v2.DERPMap.Region.Node - 14, // 36: coder.tailnet.v2.DERPMap.RegionsEntry.value:type_name -> coder.tailnet.v2.DERPMap.Region - 6, // 37: coder.tailnet.v2.CoordinateRequest.UpdateSelf.node:type_name -> coder.tailnet.v2.Node - 6, // 38: coder.tailnet.v2.CoordinateResponse.PeerUpdate.node:type_name -> coder.tailnet.v2.Node - 0, // 39: coder.tailnet.v2.CoordinateResponse.PeerUpdate.kind:type_name -> coder.tailnet.v2.CoordinateResponse.PeerUpdate.Kind - 33, // 40: coder.tailnet.v2.Netcheck.RegionLatencyEntry.value:type_name -> google.protobuf.Duration - 33, // 41: coder.tailnet.v2.Netcheck.RegionV4LatencyEntry.value:type_name -> google.protobuf.Duration - 33, // 42: coder.tailnet.v2.Netcheck.RegionV6LatencyEntry.value:type_name -> google.protobuf.Duration - 3, // 43: coder.tailnet.v2.TelemetryEvent.IPFields.class:type_name -> coder.tailnet.v2.TelemetryEvent.IPClass - 28, // 44: coder.tailnet.v2.TelemetryEvent.P2PEndpoint.fields:type_name -> coder.tailnet.v2.TelemetryEvent.IPFields - 28, // 45: coder.tailnet.v2.TelemetryEvent.LogIpHashesEntry.value:type_name -> coder.tailnet.v2.TelemetryEvent.IPFields - 11, // 46: coder.tailnet.v2.Tailnet.PostTelemetry:input_type -> coder.tailnet.v2.TelemetryRequest - 5, // 47: coder.tailnet.v2.Tailnet.StreamDERPMaps:input_type -> coder.tailnet.v2.StreamDERPMapsRequest - 7, // 48: coder.tailnet.v2.Tailnet.Coordinate:input_type -> coder.tailnet.v2.CoordinateRequest - 12, // 49: coder.tailnet.v2.Tailnet.PostTelemetry:output_type -> coder.tailnet.v2.TelemetryResponse - 4, // 50: coder.tailnet.v2.Tailnet.StreamDERPMaps:output_type -> coder.tailnet.v2.DERPMap - 8, // 51: coder.tailnet.v2.Tailnet.Coordinate:output_type -> coder.tailnet.v2.CoordinateResponse - 49, // [49:52] is the sub-list for method output_type - 46, // [46:49] is the sub-list for method input_type - 46, // [46:46] is the sub-list for extension type_name - 46, // [46:46] is the sub-list for extension extendee - 0, // [0:46] is the sub-list for field type_name + 14, // 0: coder.tailnet.v2.DERPMap.home_params:type_name -> coder.tailnet.v2.DERPMap.HomeParams + 16, // 1: coder.tailnet.v2.DERPMap.regions:type_name -> coder.tailnet.v2.DERPMap.RegionsEntry + 32, // 2: coder.tailnet.v2.Node.as_of:type_name -> google.protobuf.Timestamp + 19, // 3: coder.tailnet.v2.Node.derp_latency:type_name -> coder.tailnet.v2.Node.DerpLatencyEntry + 20, // 4: coder.tailnet.v2.Node.derp_forced_websocket:type_name -> coder.tailnet.v2.Node.DerpForcedWebsocketEntry + 21, // 5: coder.tailnet.v2.CoordinateRequest.update_self:type_name -> coder.tailnet.v2.CoordinateRequest.UpdateSelf + 22, // 6: coder.tailnet.v2.CoordinateRequest.disconnect:type_name -> coder.tailnet.v2.CoordinateRequest.Disconnect + 23, // 7: coder.tailnet.v2.CoordinateRequest.add_tunnel:type_name -> coder.tailnet.v2.CoordinateRequest.Tunnel + 23, // 8: coder.tailnet.v2.CoordinateRequest.remove_tunnel:type_name -> coder.tailnet.v2.CoordinateRequest.Tunnel + 24, // 9: coder.tailnet.v2.CoordinateRequest.ready_for_handshake:type_name -> coder.tailnet.v2.CoordinateRequest.ReadyForHandshake + 25, // 10: coder.tailnet.v2.CoordinateResponse.peer_updates:type_name -> coder.tailnet.v2.CoordinateResponse.PeerUpdate + 1, // 11: coder.tailnet.v2.IPFields.class:type_name -> coder.tailnet.v2.IPFields.IPClass + 33, // 12: coder.tailnet.v2.Netcheck.MappingVariesByDestIP:type_name -> google.protobuf.BoolValue + 33, // 13: coder.tailnet.v2.Netcheck.HairPinning:type_name -> google.protobuf.BoolValue + 33, // 14: coder.tailnet.v2.Netcheck.UPnP:type_name -> google.protobuf.BoolValue + 33, // 15: coder.tailnet.v2.Netcheck.PMP:type_name -> google.protobuf.BoolValue + 33, // 16: coder.tailnet.v2.Netcheck.PCP:type_name -> google.protobuf.BoolValue + 26, // 17: coder.tailnet.v2.Netcheck.RegionLatency:type_name -> coder.tailnet.v2.Netcheck.RegionLatencyEntry + 27, // 18: coder.tailnet.v2.Netcheck.RegionV4Latency:type_name -> coder.tailnet.v2.Netcheck.RegionV4LatencyEntry + 28, // 19: coder.tailnet.v2.Netcheck.RegionV6Latency:type_name -> coder.tailnet.v2.Netcheck.RegionV6LatencyEntry + 29, // 20: coder.tailnet.v2.Netcheck.GlobalV4:type_name -> coder.tailnet.v2.Netcheck.NetcheckIP + 29, // 21: coder.tailnet.v2.Netcheck.GlobalV6:type_name -> coder.tailnet.v2.Netcheck.NetcheckIP + 33, // 22: coder.tailnet.v2.Netcheck.CaptivePortal:type_name -> google.protobuf.BoolValue + 32, // 23: coder.tailnet.v2.TelemetryEvent.time:type_name -> google.protobuf.Timestamp + 2, // 24: coder.tailnet.v2.TelemetryEvent.status:type_name -> coder.tailnet.v2.TelemetryEvent.Status + 3, // 25: coder.tailnet.v2.TelemetryEvent.client_type:type_name -> coder.tailnet.v2.TelemetryEvent.ClientType + 30, // 26: coder.tailnet.v2.TelemetryEvent.p2p_endpoint:type_name -> coder.tailnet.v2.TelemetryEvent.P2PEndpoint + 31, // 27: coder.tailnet.v2.TelemetryEvent.log_ip_hashes:type_name -> coder.tailnet.v2.TelemetryEvent.LogIpHashesEntry + 4, // 28: coder.tailnet.v2.TelemetryEvent.derp_map:type_name -> coder.tailnet.v2.DERPMap + 10, // 29: coder.tailnet.v2.TelemetryEvent.latest_netcheck:type_name -> coder.tailnet.v2.Netcheck + 34, // 30: coder.tailnet.v2.TelemetryEvent.connection_age:type_name -> google.protobuf.Duration + 34, // 31: coder.tailnet.v2.TelemetryEvent.connection_setup:type_name -> google.protobuf.Duration + 34, // 32: coder.tailnet.v2.TelemetryEvent.p2p_setup:type_name -> google.protobuf.Duration + 34, // 33: coder.tailnet.v2.TelemetryEvent.derp_latency:type_name -> google.protobuf.Duration + 34, // 34: coder.tailnet.v2.TelemetryEvent.p2p_latency:type_name -> google.protobuf.Duration + 35, // 35: coder.tailnet.v2.TelemetryEvent.throughput_mbits:type_name -> google.protobuf.FloatValue + 11, // 36: coder.tailnet.v2.TelemetryRequest.events:type_name -> coder.tailnet.v2.TelemetryEvent + 17, // 37: coder.tailnet.v2.DERPMap.HomeParams.region_score:type_name -> coder.tailnet.v2.DERPMap.HomeParams.RegionScoreEntry + 18, // 38: coder.tailnet.v2.DERPMap.Region.nodes:type_name -> coder.tailnet.v2.DERPMap.Region.Node + 15, // 39: coder.tailnet.v2.DERPMap.RegionsEntry.value:type_name -> coder.tailnet.v2.DERPMap.Region + 6, // 40: coder.tailnet.v2.CoordinateRequest.UpdateSelf.node:type_name -> coder.tailnet.v2.Node + 6, // 41: coder.tailnet.v2.CoordinateResponse.PeerUpdate.node:type_name -> coder.tailnet.v2.Node + 0, // 42: coder.tailnet.v2.CoordinateResponse.PeerUpdate.kind:type_name -> coder.tailnet.v2.CoordinateResponse.PeerUpdate.Kind + 34, // 43: coder.tailnet.v2.Netcheck.RegionLatencyEntry.value:type_name -> google.protobuf.Duration + 34, // 44: coder.tailnet.v2.Netcheck.RegionV4LatencyEntry.value:type_name -> google.protobuf.Duration + 34, // 45: coder.tailnet.v2.Netcheck.RegionV6LatencyEntry.value:type_name -> google.protobuf.Duration + 9, // 46: coder.tailnet.v2.Netcheck.NetcheckIP.fields:type_name -> coder.tailnet.v2.IPFields + 9, // 47: coder.tailnet.v2.TelemetryEvent.P2PEndpoint.fields:type_name -> coder.tailnet.v2.IPFields + 9, // 48: coder.tailnet.v2.TelemetryEvent.LogIpHashesEntry.value:type_name -> coder.tailnet.v2.IPFields + 12, // 49: coder.tailnet.v2.Tailnet.PostTelemetry:input_type -> coder.tailnet.v2.TelemetryRequest + 5, // 50: coder.tailnet.v2.Tailnet.StreamDERPMaps:input_type -> coder.tailnet.v2.StreamDERPMapsRequest + 7, // 51: coder.tailnet.v2.Tailnet.Coordinate:input_type -> coder.tailnet.v2.CoordinateRequest + 13, // 52: coder.tailnet.v2.Tailnet.PostTelemetry:output_type -> coder.tailnet.v2.TelemetryResponse + 4, // 53: coder.tailnet.v2.Tailnet.StreamDERPMaps:output_type -> coder.tailnet.v2.DERPMap + 8, // 54: coder.tailnet.v2.Tailnet.Coordinate:output_type -> coder.tailnet.v2.CoordinateResponse + 52, // [52:55] is the sub-list for method output_type + 49, // [49:52] is the sub-list for method input_type + 49, // [49:49] is the sub-list for extension type_name + 49, // [49:49] is the sub-list for extension extendee + 0, // [0:49] is the sub-list for field type_name } func init() { file_tailnet_proto_tailnet_proto_init() } @@ -2259,7 +2326,7 @@ func file_tailnet_proto_tailnet_proto_init() { } } file_tailnet_proto_tailnet_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Netcheck); i { + switch v := v.(*IPFields); i { case 0: return &v.state case 1: @@ -2271,7 +2338,7 @@ func file_tailnet_proto_tailnet_proto_init() { } } file_tailnet_proto_tailnet_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TelemetryEvent); i { + switch v := v.(*Netcheck); i { case 0: return &v.state case 1: @@ -2283,7 +2350,7 @@ func file_tailnet_proto_tailnet_proto_init() { } } file_tailnet_proto_tailnet_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TelemetryRequest); i { + switch v := v.(*TelemetryEvent); i { case 0: return &v.state case 1: @@ -2295,7 +2362,7 @@ func file_tailnet_proto_tailnet_proto_init() { } } file_tailnet_proto_tailnet_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TelemetryResponse); i { + switch v := v.(*TelemetryRequest); i { case 0: return &v.state case 1: @@ -2307,7 +2374,7 @@ func file_tailnet_proto_tailnet_proto_init() { } } file_tailnet_proto_tailnet_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DERPMap_HomeParams); i { + switch v := v.(*TelemetryResponse); i { case 0: return &v.state case 1: @@ -2319,6 +2386,18 @@ func file_tailnet_proto_tailnet_proto_init() { } } file_tailnet_proto_tailnet_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DERPMap_HomeParams); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_tailnet_proto_tailnet_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DERPMap_Region); i { case 0: return &v.state @@ -2330,7 +2409,7 @@ func file_tailnet_proto_tailnet_proto_init() { return nil } } - file_tailnet_proto_tailnet_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + file_tailnet_proto_tailnet_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DERPMap_Region_Node); i { case 0: return &v.state @@ -2342,7 +2421,7 @@ func file_tailnet_proto_tailnet_proto_init() { return nil } } - file_tailnet_proto_tailnet_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + file_tailnet_proto_tailnet_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CoordinateRequest_UpdateSelf); i { case 0: return &v.state @@ -2354,7 +2433,7 @@ func file_tailnet_proto_tailnet_proto_init() { return nil } } - file_tailnet_proto_tailnet_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { + file_tailnet_proto_tailnet_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CoordinateRequest_Disconnect); i { case 0: return &v.state @@ -2366,7 +2445,7 @@ func file_tailnet_proto_tailnet_proto_init() { return nil } } - file_tailnet_proto_tailnet_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { + file_tailnet_proto_tailnet_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CoordinateRequest_Tunnel); i { case 0: return &v.state @@ -2378,7 +2457,7 @@ func file_tailnet_proto_tailnet_proto_init() { return nil } } - file_tailnet_proto_tailnet_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { + file_tailnet_proto_tailnet_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CoordinateRequest_ReadyForHandshake); i { case 0: return &v.state @@ -2390,7 +2469,7 @@ func file_tailnet_proto_tailnet_proto_init() { return nil } } - file_tailnet_proto_tailnet_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { + file_tailnet_proto_tailnet_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CoordinateResponse_PeerUpdate); i { case 0: return &v.state @@ -2402,8 +2481,8 @@ func file_tailnet_proto_tailnet_proto_init() { return nil } } - file_tailnet_proto_tailnet_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TelemetryEvent_IPFields); i { + file_tailnet_proto_tailnet_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Netcheck_NetcheckIP); i { case 0: return &v.state case 1: @@ -2414,7 +2493,7 @@ func file_tailnet_proto_tailnet_proto_init() { return nil } } - file_tailnet_proto_tailnet_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { + file_tailnet_proto_tailnet_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*TelemetryEvent_P2PEndpoint); i { case 0: return &v.state @@ -2433,7 +2512,7 @@ func file_tailnet_proto_tailnet_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_tailnet_proto_tailnet_proto_rawDesc, NumEnums: 4, - NumMessages: 27, + NumMessages: 28, NumExtensions: 0, NumServices: 1, }, diff --git a/tailnet/proto/tailnet.proto b/tailnet/proto/tailnet.proto index cba3d83929d24..b4e4b50bbe992 100644 --- a/tailnet/proto/tailnet.proto +++ b/tailnet/proto/tailnet.proto @@ -102,6 +102,18 @@ message CoordinateResponse { string error = 2; } +message IPFields { + int32 version = 1; + enum IPClass { + PUBLIC = 0; + PRIVATE = 1; + LINK_LOCAL = 2; + UNIQUE_LOCAL = 3; + LOOPBACK = 4; + } + IPClass class = 2; +} + message Netcheck { bool UDP = 1; bool IPv6 = 2; @@ -123,8 +135,12 @@ message Netcheck { map RegionV4Latency = 15; map RegionV6Latency = 16; - string GlobalV4 = 17; - string GlobalV6 = 18; + message NetcheckIP { + string hash = 1; + IPFields fields = 2; + } + NetcheckIP GlobalV4 = 17; + NetcheckIP GlobalV6 = 18; google.protobuf.BoolValue CaptivePortal = 19; } @@ -142,19 +158,6 @@ message TelemetryEvent { WSPROXY = 3; } - enum IPClass { - PUBLIC = 0; - PRIVATE = 1; - LINK_LOCAL = 2; - UNIQUE_LOCAL = 3; - LOOPBACK = 4; - } - - message IPFields { - int32 version = 1; - IPClass class = 2; - } - message P2PEndpoint { string hash = 1; int32 port = 2; @@ -167,8 +170,8 @@ message TelemetryEvent { Status status = 4; string disconnection_reason = 5; ClientType client_type = 6; - string node_id_self = 7; - string node_id_remote = 8; + bytes node_id_self = 7; + bytes node_id_remote = 8; P2PEndpoint p2p_endpoint = 9; map log_ip_hashes = 10; string home_derp = 11; diff --git a/tailnet/service.go b/tailnet/service.go index 870b7ff7a4a67..d118950c91447 100644 --- a/tailnet/service.go +++ b/tailnet/service.go @@ -4,18 +4,19 @@ import ( "context" "io" "net" + "sync" "sync/atomic" "time" "github.com/google/uuid" "github.com/hashicorp/yamux" - "storj.io/drpc/drpcerr" "storj.io/drpc/drpcmux" "storj.io/drpc/drpcserver" "tailscale.com/tailcfg" "cdr.dev/slog" "github.com/coder/coder/v2/apiversion" + "github.com/coder/coder/v2/coderd/telemetry" "github.com/coder/coder/v2/tailnet/proto" "golang.org/x/xerrors" @@ -118,14 +119,94 @@ func (s ClientService) ServeConnV2(ctx context.Context, conn net.Conn, streamID // DRPCService is the dRPC-based, version 2.x of the tailnet API and implements proto.DRPCClientServer type DRPCService struct { - CoordPtr *atomic.Pointer[Coordinator] - Logger slog.Logger - DerpMapUpdateFrequency time.Duration - DerpMapFn func() *tailcfg.DERPMap + CoordPtr *atomic.Pointer[Coordinator] + Logger slog.Logger + DerpMapUpdateFrequency time.Duration + DerpMapFn func() *tailcfg.DERPMap + NetworkTelemetryBatchFrequency time.Duration + NetworkTelemetryBatchMaxSize int + NetworkTelemetryBatchFn func(batch []telemetry.NetworkEvent) + + mu sync.Mutex + pendingNetworkEvents []telemetry.NetworkEvent +} + +func (s *DRPCService) writeTelemetryEvents(events []telemetry.NetworkEvent) { + if s.NetworkTelemetryBatchFn == nil { + return + } + s.NetworkTelemetryBatchFn(events) } -func (*DRPCService) PostTelemetry(context.Context, *proto.TelemetryRequest) (*proto.TelemetryResponse, error) { - return nil, drpcerr.WithCode(xerrors.New("Unimplemented"), drpcerr.Unimplemented) +func (s *DRPCService) sendTelemetryBatch() { + s.mu.Lock() + defer s.mu.Unlock() + events := s.pendingNetworkEvents + s.pendingNetworkEvents = []telemetry.NetworkEvent{} + go s.writeTelemetryEvents(events) +} + +// PeriodicTelemetryBatcher starts a goroutine to periodically send telemetry +// events to the telemetry backend. The returned function is a cleanup function +// that should be called when the service is no longer needed. +// +// Note: calling the returned function does not unblock any in-flight calls to +// the underlying telemetry backend that come from PostTelemetry due to +// s.TelemetryBatchMaxSize. +func (s *DRPCService) PeriodicTelemetryBatcher() func() { + var ( + closed = make(chan struct{}) + done = make(chan struct{}) + ) + if s.NetworkTelemetryBatchFn == nil { + return func() {} + } + + go func() { + defer close(done) + ticker := time.NewTicker(s.NetworkTelemetryBatchFrequency) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + s.sendTelemetryBatch() + case <-closed: + // Send any remaining telemetry events before exiting. + s.sendTelemetryBatch() + return + } + } + }() + + return func() { + close(closed) + <-done + } +} + +func (s *DRPCService) PostTelemetry(_ context.Context, req *proto.TelemetryRequest) (*proto.TelemetryResponse, error) { + s.mu.Lock() + defer s.mu.Unlock() + + for _, pEvent := range req.Events { + tEvent, err := telemetry.NetworkEventFromProto(pEvent) + if err != nil { + // TODO(@deansheather): log? return an error? + continue + } + + s.pendingNetworkEvents = append(s.pendingNetworkEvents, tEvent) + + if len(s.pendingNetworkEvents) >= s.NetworkTelemetryBatchMaxSize { + events := s.pendingNetworkEvents + s.pendingNetworkEvents = []telemetry.NetworkEvent{} + // Perform the send in a goroutine to avoid blocking the DRPC call. + go s.writeTelemetryEvents(events) + } + } + + return &proto.TelemetryResponse{}, nil } func (s *DRPCService) StreamDERPMaps(_ *proto.StreamDERPMapsRequest, stream proto.DRPCTailnet_StreamDERPMapsStream) error { From 305fd2b4c939b0e33de2535477a18352053e1f05 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 27 Jun 2024 12:04:12 +0000 Subject: [PATCH 11/14] Progress --- coderd/agentapi/api.go | 2 - coderd/agentapi/api_test.go | 106 ++++++++++++++++++++++++++++++++++ coderd/telemetry/telemetry.go | 16 ++--- coderd/workspaceagentsrpc.go | 2 +- tailnet/proto/tailnet.pb.go | 16 ++--- tailnet/proto/tailnet.proto | 4 +- 6 files changed, 121 insertions(+), 25 deletions(-) create mode 100644 coderd/agentapi/api_test.go diff --git a/coderd/agentapi/api.go b/coderd/agentapi/api.go index 37c84a966f4cb..f99731cb78ddf 100644 --- a/coderd/agentapi/api.go +++ b/coderd/agentapi/api.go @@ -22,7 +22,6 @@ import ( "github.com/coder/coder/v2/coderd/database/pubsub" "github.com/coder/coder/v2/coderd/externalauth" "github.com/coder/coder/v2/coderd/prometheusmetrics" - "github.com/coder/coder/v2/coderd/schedule" "github.com/coder/coder/v2/coderd/telemetry" "github.com/coder/coder/v2/coderd/tracing" "github.com/coder/coder/v2/coderd/workspacestats" @@ -62,7 +61,6 @@ type Options struct { Pubsub pubsub.Pubsub DerpMapFn func() *tailcfg.DERPMap TailnetCoordinator *atomic.Pointer[tailnet.Coordinator] - TemplateScheduleStore *atomic.Pointer[schedule.TemplateScheduleStore] StatsReporter *workspacestats.Reporter AppearanceFetcher *atomic.Pointer[appearance.Fetcher] PublishWorkspaceUpdateFn func(ctx context.Context, workspaceID uuid.UUID) diff --git a/coderd/agentapi/api_test.go b/coderd/agentapi/api_test.go new file mode 100644 index 0000000000000..98b1e072d65bf --- /dev/null +++ b/coderd/agentapi/api_test.go @@ -0,0 +1,106 @@ +package agentapi_test + +import ( + "context" + "net/url" + "sync/atomic" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "go.uber.org/goleak" + "tailscale.com/tailcfg" + + "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/agent/proto" + "github.com/coder/coder/v2/coderd/agentapi" + "github.com/coder/coder/v2/coderd/appearance" + "github.com/coder/coder/v2/coderd/database/dbtestutil" + "github.com/coder/coder/v2/coderd/externalauth" + "github.com/coder/coder/v2/coderd/prometheusmetrics" + "github.com/coder/coder/v2/coderd/schedule" + "github.com/coder/coder/v2/coderd/telemetry" + "github.com/coder/coder/v2/coderd/workspacestats" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/tailnet" + "github.com/coder/coder/v2/tailnet/tailnettest" + "github.com/coder/coder/v2/testutil" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} + +func Test_APIClose(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitMedium) + log := slogtest.Make(t, nil) + + db, pubsub := dbtestutil.NewDB(t) + fCoord := tailnettest.NewFakeCoordinator() + var coord tailnet.Coordinator = fCoord + coordPtr := atomic.Pointer[tailnet.Coordinator]{} + coordPtr.Store(&coord) + + mockTemplateScheduleStore := schedule.MockTemplateScheduleStore{} + var templateScheduleStore schedule.TemplateScheduleStore = mockTemplateScheduleStore + templateScheduleStorePtr := atomic.Pointer[schedule.TemplateScheduleStore]{} + templateScheduleStorePtr.Store(&templateScheduleStore) + + statsBatcher, closeBatcher, err := workspacestats.NewBatcher(ctx, workspacestats.BatcherWithStore(db)) + require.NoError(t, err) + t.Cleanup(closeBatcher) + statsTracker := workspacestats.NewTracker(db) + t.Cleanup(func() { + _ = statsTracker.Close() + }) + statsReporter := workspacestats.NewReporter(workspacestats.ReporterOptions{ + Database: db, + Logger: log, + Pubsub: pubsub, + TemplateScheduleStore: &templateScheduleStorePtr, + StatsBatcher: statsBatcher, + UsageTracker: statsTracker, + UpdateAgentMetricsFn: func(_ context.Context, _ prometheusmetrics.AgentMetricLabels, _ []*proto.Stats_Metric) {}, + AppStatBatchSize: 0, + }) + + appearanceFetcherPtr := atomic.Pointer[appearance.Fetcher]{} + appearanceFetcherPtr.Store(&appearance.DefaultFetcher) + + api := agentapi.New(agentapi.Options{ + AgentID: uuid.New(), + Ctx: ctx, + Log: log, + Database: db, + Pubsub: pubsub, + DerpMapFn: func() *tailcfg.DERPMap { + return &tailcfg.DERPMap{Regions: map[int]*tailcfg.DERPRegion{999: {RegionCode: "test"}}} + }, + TailnetCoordinator: &coordPtr, + StatsReporter: statsReporter, + AppearanceFetcher: &appearanceFetcherPtr, + PublishWorkspaceUpdateFn: func(_ context.Context, _ uuid.UUID) {}, + NetworkTelemetryBatchFn: func(_ []telemetry.NetworkEvent) {}, + AccessURL: &url.URL{ + Scheme: "http", + Host: "localhost", + }, + AppHostname: "", + AgentStatsRefreshInterval: time.Second, + DisableDirectConnections: false, + DerpForceWebSockets: false, + DerpMapUpdateFrequency: time.Second, + NetworkTelemetryBatchFrequency: time.Second, + NetworkTelemetryBatchMaxSize: 1, + ExternalAuthConfigs: []*externalauth.Config{}, + Experiments: codersdk.Experiments{}, + WorkspaceID: uuid.New(), + UpdateAgentMetricsFn: func(_ context.Context, _ prometheusmetrics.AgentMetricLabels, _ []*proto.Stats_Metric) {}, + }) + + err = api.Close() + require.NoError(t, err) +} diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index d2dfbaba44204..e35475c4add71 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -1240,8 +1240,8 @@ type NetworkEvent struct { Status string `json:"status"` // connected, disconnected DisconnectionReason string `json:"disconnection_reason"` ClientType string `json:"client_type"` // cli, agent, coderd, wsproxy - NodeIDSelf uuid.UUID `json:"node_id_self"` - NodeIDRemote uuid.UUID `json:"node_id_remote"` + NodeIDSelf uint64 `json:"node_id_self"` + NodeIDRemote uint64 `json:"node_id_remote"` P2PEndpoint NetworkEventP2PEndpoint `json:"p2p_endpoint"` LogIPHashes map[string]NetworkEventIPFields `json:"log_ip_hashes"` HomeDERP string `json:"home_derp"` @@ -1272,14 +1272,6 @@ func NetworkEventFromProto(proto *tailnetproto.TelemetryEvent) (NetworkEvent, er if err != nil { return NetworkEvent{}, xerrors.Errorf("parse id %q: %w", proto.Id, err) } - nodeIDSelf, err := uuid.ParseBytes(proto.NodeIdSelf) - if err != nil { - return NetworkEvent{}, xerrors.Errorf("parse node_id_self %q: %w", proto.NodeIdSelf, err) - } - nodeIDRemote, err := uuid.ParseBytes(proto.NodeIdRemote) - if err != nil { - return NetworkEvent{}, xerrors.Errorf("parse node_id_remote %q: %w", proto.NodeIdRemote, err) - } logIPHashes := make(map[string]NetworkEventIPFields, len(proto.LogIpHashes)) for k, v := range proto.LogIpHashes { @@ -1293,8 +1285,8 @@ func NetworkEventFromProto(proto *tailnetproto.TelemetryEvent) (NetworkEvent, er Status: strings.ToLower(proto.Status.String()), DisconnectionReason: proto.DisconnectionReason, ClientType: strings.ToLower(proto.ClientType.String()), - NodeIDSelf: nodeIDSelf, - NodeIDRemote: nodeIDRemote, + NodeIDSelf: proto.NodeIdSelf, + NodeIDRemote: proto.NodeIdRemote, P2PEndpoint: p2pEndpointFromProto(proto.P2PEndpoint), LogIPHashes: logIPHashes, HomeDERP: proto.HomeDerp, diff --git a/coderd/workspaceagentsrpc.go b/coderd/workspaceagentsrpc.go index 9853d170eff7c..efa187bc14e98 100644 --- a/coderd/workspaceagentsrpc.go +++ b/coderd/workspaceagentsrpc.go @@ -131,7 +131,6 @@ func (api *API) workspaceAgentRPC(rw http.ResponseWriter, r *http.Request) { Pubsub: api.Pubsub, DerpMapFn: api.DERPMap, TailnetCoordinator: &api.TailnetCoordinator, - TemplateScheduleStore: api.TemplateScheduleStore, AppearanceFetcher: &api.AppearanceFetcher, StatsReporter: api.statsReporter, PublishWorkspaceUpdateFn: api.publishWorkspaceUpdate, @@ -157,6 +156,7 @@ func (api *API) workspaceAgentRPC(rw http.ResponseWriter, r *http.Request) { WorkspaceID: build.WorkspaceID, // saves the extra lookup later UpdateAgentMetricsFn: api.UpdateAgentMetrics, }) + defer agentAPI.Close() streamID := tailnet.StreamID{ Name: fmt.Sprintf("%s-%s-%s", owner.Username, workspace.Name, workspaceAgent.Name), diff --git a/tailnet/proto/tailnet.pb.go b/tailnet/proto/tailnet.pb.go index f293b7ba69f55..a9268f04d992c 100644 --- a/tailnet/proto/tailnet.pb.go +++ b/tailnet/proto/tailnet.pb.go @@ -835,8 +835,8 @@ type TelemetryEvent struct { Status TelemetryEvent_Status `protobuf:"varint,4,opt,name=status,proto3,enum=coder.tailnet.v2.TelemetryEvent_Status" json:"status,omitempty"` DisconnectionReason string `protobuf:"bytes,5,opt,name=disconnection_reason,json=disconnectionReason,proto3" json:"disconnection_reason,omitempty"` ClientType TelemetryEvent_ClientType `protobuf:"varint,6,opt,name=client_type,json=clientType,proto3,enum=coder.tailnet.v2.TelemetryEvent_ClientType" json:"client_type,omitempty"` - NodeIdSelf []byte `protobuf:"bytes,7,opt,name=node_id_self,json=nodeIdSelf,proto3" json:"node_id_self,omitempty"` - NodeIdRemote []byte `protobuf:"bytes,8,opt,name=node_id_remote,json=nodeIdRemote,proto3" json:"node_id_remote,omitempty"` + NodeIdSelf uint64 `protobuf:"varint,7,opt,name=node_id_self,json=nodeIdSelf,proto3" json:"node_id_self,omitempty"` + NodeIdRemote uint64 `protobuf:"varint,8,opt,name=node_id_remote,json=nodeIdRemote,proto3" json:"node_id_remote,omitempty"` P2PEndpoint *TelemetryEvent_P2PEndpoint `protobuf:"bytes,9,opt,name=p2p_endpoint,json=p2pEndpoint,proto3" json:"p2p_endpoint,omitempty"` LogIpHashes map[string]*IPFields `protobuf:"bytes,10,rep,name=log_ip_hashes,json=logIpHashes,proto3" json:"log_ip_hashes,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` HomeDerp string `protobuf:"bytes,11,opt,name=home_derp,json=homeDerp,proto3" json:"home_derp,omitempty"` @@ -925,18 +925,18 @@ func (x *TelemetryEvent) GetClientType() TelemetryEvent_ClientType { return TelemetryEvent_CLI } -func (x *TelemetryEvent) GetNodeIdSelf() []byte { +func (x *TelemetryEvent) GetNodeIdSelf() uint64 { if x != nil { return x.NodeIdSelf } - return nil + return 0 } -func (x *TelemetryEvent) GetNodeIdRemote() []byte { +func (x *TelemetryEvent) GetNodeIdRemote() uint64 { if x != nil { return x.NodeIdRemote } - return nil + return 0 } func (x *TelemetryEvent) GetP2PEndpoint() *TelemetryEvent_P2PEndpoint { @@ -2046,10 +2046,10 @@ var file_tailnet_proto_tailnet_proto_rawDesc = []byte{ 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0a, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x20, 0x0a, 0x0c, 0x6e, 0x6f, 0x64, - 0x65, 0x5f, 0x69, 0x64, 0x5f, 0x73, 0x65, 0x6c, 0x66, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x65, 0x5f, 0x69, 0x64, 0x5f, 0x73, 0x65, 0x6c, 0x66, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x53, 0x65, 0x6c, 0x66, 0x12, 0x24, 0x0a, 0x0e, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x5f, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x52, 0x65, 0x6d, 0x6f, 0x74, + 0x01, 0x28, 0x04, 0x52, 0x0c, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x12, 0x4f, 0x0a, 0x0c, 0x70, 0x32, 0x70, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x54, 0x65, 0x6c, 0x65, 0x6d, diff --git a/tailnet/proto/tailnet.proto b/tailnet/proto/tailnet.proto index b4e4b50bbe992..b8e97d8a7a493 100644 --- a/tailnet/proto/tailnet.proto +++ b/tailnet/proto/tailnet.proto @@ -170,8 +170,8 @@ message TelemetryEvent { Status status = 4; string disconnection_reason = 5; ClientType client_type = 6; - bytes node_id_self = 7; - bytes node_id_remote = 8; + uint64 node_id_self = 7; + uint64 node_id_remote = 8; P2PEndpoint p2p_endpoint = 9; map log_ip_hashes = 10; string home_derp = 11; From 4e3671e81225f66563d5b84dd7465edf0b4fe4fd Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Wed, 26 Jun 2024 15:26:07 +0000 Subject: [PATCH 12/14] chore: add DRPC tailnet & cli implementation --- cli/ping.go | 3 + cli/portforward.go | 3 + cli/root.go | 37 ++-- cli/speedtest.go | 3 + cli/ssh.go | 5 +- cli/testdata/coder_--help.golden | 3 + codersdk/workspacesdk/connector.go | 76 ++++--- .../workspacesdk/connector_internal_test.go | 3 +- codersdk/workspacesdk/workspacesdk.go | 63 +++--- docs/cli.md | 9 + enterprise/cli/testdata/coder_--help.golden | 3 + tailnet/conn.go | 18 ++ tailnet/logger.go | 200 ++++++++++++++++++ tailnet/logger_internal_test.go | 145 +++++++++++++ 14 files changed, 500 insertions(+), 71 deletions(-) create mode 100644 tailnet/logger.go create mode 100644 tailnet/logger_internal_test.go diff --git a/cli/ping.go b/cli/ping.go index 82becb016bde7..ff6c318cc1b84 100644 --- a/cli/ping.go +++ b/cli/ping.go @@ -58,6 +58,9 @@ func (r *RootCmd) ping() *serpent.Command { _, _ = fmt.Fprintln(inv.Stderr, "Direct connections disabled.") opts.BlockEndpoints = true } + if r.noNetworkTelemetry { + opts.EnableTelemetry = true + } conn, err := workspacesdk.New(client).DialAgent(ctx, workspaceAgent.ID, opts) if err != nil { return err diff --git a/cli/portforward.go b/cli/portforward.go index 4c0b1d772eecc..bef0bb6f12be1 100644 --- a/cli/portforward.go +++ b/cli/portforward.go @@ -106,6 +106,9 @@ func (r *RootCmd) portForward() *serpent.Command { _, _ = fmt.Fprintln(inv.Stderr, "Direct connections disabled.") opts.BlockEndpoints = true } + if r.noNetworkTelemetry { + opts.EnableTelemetry = true + } conn, err := workspacesdk.New(client).DialAgent(ctx, workspaceAgent.ID, opts) if err != nil { return err diff --git a/cli/root.go b/cli/root.go index 073486c640744..ac4bd19d74568 100644 --- a/cli/root.go +++ b/cli/root.go @@ -52,20 +52,21 @@ var ( ) const ( - varURL = "url" - varToken = "token" - varAgentToken = "agent-token" - varAgentTokenFile = "agent-token-file" - varAgentURL = "agent-url" - varHeader = "header" - varHeaderCommand = "header-command" - varNoOpen = "no-open" - varNoVersionCheck = "no-version-warning" - varNoFeatureWarning = "no-feature-warning" - varForceTty = "force-tty" - varVerbose = "verbose" - varOrganizationSelect = "organization" - varDisableDirect = "disable-direct-connections" + varURL = "url" + varToken = "token" + varAgentToken = "agent-token" + varAgentTokenFile = "agent-token-file" + varAgentURL = "agent-url" + varHeader = "header" + varHeaderCommand = "header-command" + varNoOpen = "no-open" + varNoVersionCheck = "no-version-warning" + varNoFeatureWarning = "no-feature-warning" + varForceTty = "force-tty" + varVerbose = "verbose" + varOrganizationSelect = "organization" + varDisableDirect = "disable-direct-connections" + varDisableNetworkTelemetry = "disable-network-telemetry" notLoggedInMessage = "You are not logged in. Try logging in using 'coder login '." @@ -436,6 +437,13 @@ func (r *RootCmd) Command(subcommands []*serpent.Command) (*serpent.Command, err Value: serpent.BoolOf(&r.disableDirect), Group: globalGroup, }, + { + Flag: varDisableNetworkTelemetry, + Env: "CODER_DISABLE_NETWORK_TELEMETRY", + Description: "Disable network telemetry.", + Value: serpent.BoolOf(&r.noNetworkTelemetry), + Group: globalGroup, + }, { Flag: "debug-http", Description: "Debug codersdk HTTP requests.", @@ -491,6 +499,7 @@ type RootCmd struct { versionFlag bool disableDirect bool debugHTTP bool + noNetworkTelemetry bool noVersionCheck bool noFeatureWarning bool diff --git a/cli/speedtest.go b/cli/speedtest.go index 42fe7604c6dc4..3f34e5796f66f 100644 --- a/cli/speedtest.go +++ b/cli/speedtest.go @@ -102,6 +102,9 @@ func (r *RootCmd) speedtest() *serpent.Command { _, _ = fmt.Fprintln(inv.Stderr, "Direct connections disabled.") opts.BlockEndpoints = true } + if r.noNetworkTelemetry { + opts.EnableTelemetry = true + } if pcapFile != "" { s := capture.New() opts.CaptureHook = s.LogPacket diff --git a/cli/ssh.go b/cli/ssh.go index e4e9fadf5e8e8..050c7d7fe48c3 100644 --- a/cli/ssh.go +++ b/cli/ssh.go @@ -243,8 +243,9 @@ func (r *RootCmd) ssh() *serpent.Command { } conn, err := workspacesdk.New(client). DialAgent(ctx, workspaceAgent.ID, &workspacesdk.DialAgentOptions{ - Logger: logger, - BlockEndpoints: r.disableDirect, + Logger: logger, + BlockEndpoints: r.disableDirect, + EnableTelemetry: !r.noNetworkTelemetry, }) if err != nil { return xerrors.Errorf("dial agent: %w", err) diff --git a/cli/testdata/coder_--help.golden b/cli/testdata/coder_--help.golden index e970347890eb2..294010e7678e6 100644 --- a/cli/testdata/coder_--help.golden +++ b/cli/testdata/coder_--help.golden @@ -66,6 +66,9 @@ variables or flags. --disable-direct-connections bool, $CODER_DISABLE_DIRECT_CONNECTIONS Disable direct (P2P) connections to workspaces. + --disable-network-telemetry bool, $CODER_DISABLE_NETWORK_TELEMETRY + Disable network telemetry. + --global-config string, $CODER_CONFIG_DIR (default: ~/.config/coderv2) Path to the global `coder` config directory. diff --git a/codersdk/workspacesdk/connector.go b/codersdk/workspacesdk/connector.go index 5ac009af15091..9c65fbd48678c 100644 --- a/codersdk/workspacesdk/connector.go +++ b/codersdk/workspacesdk/connector.go @@ -58,32 +58,28 @@ type tailnetAPIConnector struct { coordinateURL string dialOptions *websocket.DialOptions conn tailnetConn + customDialFn func() (proto.DRPCTailnetClient, error) + + clientMu sync.RWMutex + client proto.DRPCTailnetClient connected chan error isFirst bool closed chan struct{} } -// runTailnetAPIConnector creates and runs a tailnetAPIConnector -func runTailnetAPIConnector( - ctx context.Context, logger slog.Logger, - agentID uuid.UUID, coordinateURL string, dialOptions *websocket.DialOptions, - conn tailnetConn, -) *tailnetAPIConnector { - tac := &tailnetAPIConnector{ +// Create a new tailnetAPIConnector without running it +func newTailnetAPIConnector(ctx context.Context, logger slog.Logger, agentID uuid.UUID, coordinateURL string, dialOptions *websocket.DialOptions) *tailnetAPIConnector { + return &tailnetAPIConnector{ ctx: ctx, logger: logger, agentID: agentID, coordinateURL: coordinateURL, dialOptions: dialOptions, - conn: conn, + conn: nil, connected: make(chan error, 1), closed: make(chan struct{}), } - tac.gracefulCtx, tac.cancelGracefulCtx = context.WithCancel(context.Background()) - go tac.manageGracefulTimeout() - go tac.run() - return tac } // manageGracefulTimeout allows the gracefulContext to last 1 second longer than the main context @@ -99,21 +95,27 @@ func (tac *tailnetAPIConnector) manageGracefulTimeout() { } } -func (tac *tailnetAPIConnector) run() { - tac.isFirst = true - defer close(tac.closed) - for retrier := retry.New(50*time.Millisecond, 10*time.Second); retrier.Wait(tac.ctx); { - tailnetClient, err := tac.dial() - if xerrors.Is(err, &codersdk.Error{}) { - return - } - if err != nil { - continue +// Runs a tailnetAPIConnector using the provided connection +func (tac *tailnetAPIConnector) runConnector(conn tailnetConn) { + tac.conn = conn + tac.gracefulCtx, tac.cancelGracefulCtx = context.WithCancel(context.Background()) + go tac.manageGracefulTimeout() + go func() { + tac.isFirst = true + defer close(tac.closed) + for retrier := retry.New(50*time.Millisecond, 10*time.Second); retrier.Wait(tac.ctx); { + tailnetClient, err := tac.dial() + if err != nil { + continue + } + tac.clientMu.Lock() + tac.client = tailnetClient + tac.clientMu.Unlock() + tac.logger.Debug(tac.ctx, "obtained tailnet API v2+ client") + tac.coordinateAndDERPMap(tailnetClient) + tac.logger.Debug(tac.ctx, "tailnet API v2+ connection lost") } - tac.logger.Debug(tac.ctx, "obtained tailnet API v2+ client") - tac.coordinateAndDERPMap(tailnetClient) - tac.logger.Debug(tac.ctx, "tailnet API v2+ connection lost") - } + }() } var permanentErrorStatuses = []int{ @@ -123,6 +125,10 @@ var permanentErrorStatuses = []int{ } func (tac *tailnetAPIConnector) dial() (proto.DRPCTailnetClient, error) { + if tac.customDialFn != nil { + return tac.customDialFn() + } + tac.logger.Debug(tac.ctx, "dialing Coder tailnet v2+ API") // nolint:bodyclose ws, res, err := websocket.Dial(tac.ctx, tac.coordinateURL, tac.dialOptions) @@ -194,7 +200,10 @@ func (tac *tailnetAPIConnector) coordinateAndDERPMap(client proto.DRPCTailnetCli // we do NOT want to gracefully disconnect on the coordinate() routine. So, we'll just // close the underlying connection. This will trigger a retry of the control plane in // run(). + tac.clientMu.Lock() client.DRPCConn().Close() + tac.client = nil + tac.clientMu.Unlock() // Note that derpMap() logs it own errors, we don't bother here. } }() @@ -258,3 +267,18 @@ func (tac *tailnetAPIConnector) derpMap(client proto.DRPCTailnetClient) error { tac.conn.SetDERPMap(dm) } } + +func (tac *tailnetAPIConnector) SendTelemetryEvent(event *proto.TelemetryEvent) { + tac.clientMu.RLock() + // We hold the lock for the entire telemetry request, but this would only block + // a coordinate retry, and closing the connection. + defer tac.clientMu.RUnlock() + if tac.client == nil { + return + } + ctx, cancel := context.WithTimeout(tac.ctx, 5*time.Second) + defer cancel() + _, _ = tac.client.PostTelemetry(ctx, &proto.TelemetryRequest{ + Events: []*proto.TelemetryEvent{event}, + }) +} diff --git a/codersdk/workspacesdk/connector_internal_test.go b/codersdk/workspacesdk/connector_internal_test.go index c7fc036ffa2a1..04fb3bc127fdd 100644 --- a/codersdk/workspacesdk/connector_internal_test.go +++ b/codersdk/workspacesdk/connector_internal_test.go @@ -72,7 +72,8 @@ func TestTailnetAPIConnector_Disconnects(t *testing.T) { fConn := newFakeTailnetConn() - uut := runTailnetAPIConnector(ctx, logger, agentID, svr.URL, &websocket.DialOptions{}, fConn) + uut := newTailnetAPIConnector(ctx, logger, agentID, svr.URL, &websocket.DialOptions{}) + uut.runConnector(fConn) call := testutil.RequireRecvCtx(ctx, t, fCoord.CoordinateCalls) reqTun := testutil.RequireRecvCtx(ctx, t, call.Reqs) diff --git a/codersdk/workspacesdk/workspacesdk.go b/codersdk/workspacesdk/workspacesdk.go index 04765c13d9877..ca4d8add730cc 100644 --- a/codersdk/workspacesdk/workspacesdk.go +++ b/codersdk/workspacesdk/workspacesdk.go @@ -181,6 +181,9 @@ type DialAgentOptions struct { // CaptureHook is a callback that captures Disco packets and packets sent // into the tailnet tunnel. CaptureHook capture.Callback + // Whether the client will send network telemetry events + // Enable instead of Disable so it's initialized to false + EnableTelemetry bool } func (c *Client) DialAgent(dialCtx context.Context, agentID uuid.UUID, options *DialAgentOptions) (agentConn *AgentConn, err error) { @@ -196,29 +199,6 @@ func (c *Client) DialAgent(dialCtx context.Context, agentID uuid.UUID, options * options.BlockEndpoints = true } - ip := tailnet.IP() - var header http.Header - if headerTransport, ok := c.client.HTTPClient.Transport.(*codersdk.HeaderTransport); ok { - header = headerTransport.Header - } - conn, err := tailnet.NewConn(&tailnet.Options{ - Addresses: []netip.Prefix{netip.PrefixFrom(ip, 128)}, - DERPMap: connInfo.DERPMap, - DERPHeader: &header, - DERPForceWebSockets: connInfo.DERPForceWebSockets, - Logger: options.Logger, - BlockEndpoints: c.client.DisableDirectConnections || options.BlockEndpoints, - CaptureHook: options.CaptureHook, - }) - if err != nil { - return nil, xerrors.Errorf("create tailnet: %w", err) - } - defer func() { - if err != nil { - _ = conn.Close() - } - }() - headers := make(http.Header) tokenHeader := codersdk.SessionTokenHeader if c.client.SessionTokenHeader != "" { @@ -251,16 +231,43 @@ func (c *Client) DialAgent(dialCtx context.Context, agentID uuid.UUID, options * q.Add("version", "2.0") coordinateURL.RawQuery = q.Encode() - connector := runTailnetAPIConnector(ctx, options.Logger, - agentID, coordinateURL.String(), + connector := newTailnetAPIConnector(ctx, options.Logger, agentID, coordinateURL.String(), &websocket.DialOptions{ HTTPClient: c.client.HTTPClient, HTTPHeader: headers, // Need to disable compression to avoid a data-race. CompressionMode: websocket.CompressionDisabled, - }, - conn, - ) + }) + + ip := tailnet.IP() + var header http.Header + if headerTransport, ok := c.client.HTTPClient.Transport.(*codersdk.HeaderTransport); ok { + header = headerTransport.Header + } + var telemetrySink tailnet.TelemetrySink + if options.EnableTelemetry { + telemetrySink = connector + } + conn, err := tailnet.NewConn(&tailnet.Options{ + Addresses: []netip.Prefix{netip.PrefixFrom(ip, 128)}, + DERPMap: connInfo.DERPMap, + DERPHeader: &header, + DERPForceWebSockets: connInfo.DERPForceWebSockets, + Logger: options.Logger, + BlockEndpoints: c.client.DisableDirectConnections || options.BlockEndpoints, + CaptureHook: options.CaptureHook, + TelemetrySink: telemetrySink, + }) + if err != nil { + return nil, xerrors.Errorf("create tailnet: %w", err) + } + defer func() { + if err != nil { + _ = conn.Close() + } + }() + connector.runConnector(conn) + options.Logger.Debug(ctx, "running tailnet API v2+ connector") select { diff --git a/docs/cli.md b/docs/cli.md index 70dd29e28b9da..2962425499040 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -149,6 +149,15 @@ Enable verbose output. Disable direct (P2P) connections to workspaces. +### --disable-network-telemetry + +| | | +| ----------- | --------------------------------------------- | +| Type | bool | +| Environment | $CODER_DISABLE_NETWORK_TELEMETRY | + +Disable network telemetry. + ### --global-config | | | diff --git a/enterprise/cli/testdata/coder_--help.golden b/enterprise/cli/testdata/coder_--help.golden index 7c2ff5c835dff..6895cdde3c9f4 100644 --- a/enterprise/cli/testdata/coder_--help.golden +++ b/enterprise/cli/testdata/coder_--help.golden @@ -30,6 +30,9 @@ variables or flags. --disable-direct-connections bool, $CODER_DISABLE_DIRECT_CONNECTIONS Disable direct (P2P) connections to workspaces. + --disable-network-telemetry bool, $CODER_DISABLE_NETWORK_TELEMETRY + Disable network telemetry. + --global-config string, $CODER_CONFIG_DIR (default: ~/.config/coderv2) Path to the global `coder` config directory. diff --git a/tailnet/conn.go b/tailnet/conn.go index 8b82c455e4788..3a2843539ed04 100644 --- a/tailnet/conn.go +++ b/tailnet/conn.go @@ -99,6 +99,15 @@ type Options struct { // ForceNetworkUp forces the network to be considered up. magicsock will not // do anything if it thinks it can't reach the internet. ForceNetworkUp bool + // TelemetrySink is optional. + TelemetrySink TelemetrySink +} + +// TelemetrySink allows tailnet.Conn to send network telemetry to the Coder +// server. +type TelemetrySink interface { + // SendTelemetryEvent sends a telemetry event to some external sink. + SendTelemetryEvent(event *proto.TelemetryEvent) } // NodeID creates a Tailscale NodeID from the last 8 bytes of a UUID. It ensures @@ -113,6 +122,13 @@ func NodeID(uid uuid.UUID) tailcfg.NodeID { return tailcfg.NodeID(id) } +func (c *Conn) sendTelemetryEvent(e *proto.TelemetryEvent) { + if c.telemetrySink == nil { + return + } + c.telemetrySink.SendTelemetryEvent(e) +} + // NewConn constructs a new Wireguard server that will accept connections from the addresses provided. func NewConn(options *Options) (conn *Conn, err error) { if options == nil { @@ -259,6 +275,7 @@ func NewConn(options *Options) (conn *Conn, err error) { wireguardEngine: wireguardEngine, configMaps: cfgMaps, nodeUpdater: nodeUp, + telemetrySink: options.TelemetrySink, } defer func() { if err != nil { @@ -316,6 +333,7 @@ type Conn struct { wireguardRouter *router.Config wireguardEngine wgengine.Engine listeners map[listenKey]*listener + telemetrySink TelemetrySink trafficStats *connstats.Statistics } diff --git a/tailnet/logger.go b/tailnet/logger.go new file mode 100644 index 0000000000000..044fcde0a9ca3 --- /dev/null +++ b/tailnet/logger.go @@ -0,0 +1,200 @@ +package tailnet + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "io" + "net/netip" + "regexp" + "strings" + "sync" + + "golang.org/x/xerrors" + + "cdr.dev/slog" + "cdr.dev/slog/sloggers/sloghuman" + "github.com/coder/coder/v2/cryptorand" + "github.com/coder/coder/v2/tailnet/proto" +) + +var ipv4And6Regex = regexp.MustCompile(`(((::ffff:)?(25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}|([a-f0-9:]+:+)+[a-f0-9]+)`) + +// Used to store a number of slog logger, and a logger sink for creating network telemetry events +type multiLogger struct { + loggers []slog.Logger +} + +func newMultiLogger(loggers ...slog.Logger) multiLogger { + return multiLogger{loggers: loggers} +} + +func (m multiLogger) appendLogger(logger slog.Logger) multiLogger { + return multiLogger{loggers: append(m.loggers, logger)} +} + +func (m multiLogger) Critical(ctx context.Context, msg string, fields ...any) { + for _, i := range m.loggers { + i.Critical(ctx, msg, fields...) + } +} + +func (m multiLogger) Debug(ctx context.Context, msg string, fields ...any) { + for _, i := range m.loggers { + i.Debug(ctx, msg, fields...) + } +} + +func (m multiLogger) Error(ctx context.Context, msg string, fields ...any) { + for _, i := range m.loggers { + i.Error(ctx, msg, fields...) + } +} + +func (m multiLogger) Fatal(ctx context.Context, msg string, fields ...any) { + for _, i := range m.loggers { + i.Fatal(ctx, msg, fields...) + } +} + +func (m multiLogger) Info(ctx context.Context, msg string, fields ...any) { + for _, i := range m.loggers { + i.Info(ctx, msg, fields...) + } +} + +func (m multiLogger) Warn(ctx context.Context, msg string, fields ...any) { + for _, i := range m.loggers { + i.Warn(ctx, msg, fields...) + } +} + +func (m multiLogger) Named(name string) multiLogger { + var loggers []slog.Logger + for _, i := range m.loggers { + loggers = append(loggers, i.Named(name)) + } + return multiLogger{loggers: loggers} +} + +func (m multiLogger) With(fields ...slog.Field) multiLogger { + var loggers []slog.Logger + for _, i := range m.loggers { + loggers = append(loggers, i.With(fields...)) + } + return multiLogger{loggers: loggers} +} + +// A logger sink that extracts (anonymized) IP addresses from logs for building +// network telemetry events +type bufferLogSink struct { + sink slog.Sink + mu sync.Mutex + logs []string + // We use the same salt so the same IP hashes to the same value. + hashSalt string + // A cache to avoid hashing the same IP multiple times. + ipToHash map[string]string + hashedIPs map[string]*proto.IPFields +} + +var _ slog.Sink = &bufferLogSink{} + +var _ io.Writer = &bufferLogSink{} + +func newBufferLogSink() (*bufferLogSink, error) { + hashSalt, err := cryptorand.String(16) + if err != nil { + return nil, err + } + out := &bufferLogSink{ + logs: []string{}, + hashSalt: hashSalt, + ipToHash: make(map[string]string), + hashedIPs: make(map[string]*proto.IPFields), + } + out.sink = sloghuman.Sink(out) + return out, nil +} + +func (b *bufferLogSink) getLogs() ([]string, map[string]*proto.IPFields) { + b.mu.Lock() + defer b.mu.Unlock() + return append([]string{}, b.logs...), b.hashedIPs +} + +// Write implements io.Writer. +func (b *bufferLogSink) Write(p []byte) (n int, err error) { + b.mu.Lock() + defer b.mu.Unlock() + + // sloghuman writes a full log line in a single Write call with a trailing + // newline. + logLine := strings.TrimSuffix(string(p), "\n") + + logLineSplit := strings.SplitN(logLine, "]", 2) + logLineAfterLevel := logLine + if len(logLineAfterLevel) == 2 { + logLineAfterLevel = logLineSplit[1] + } + for _, match := range ipv4And6Regex.FindAllString(logLineAfterLevel, -1) { + hash, err := b.processIPLocked(match) + if err == nil { + logLine = strings.ReplaceAll(logLine, match, hash) + } + } + + b.logs = append(b.logs, logLine) + return len(p), nil +} + +// LogEntry implements slog.Sink. +func (b *bufferLogSink) LogEntry(ctx context.Context, e slog.SinkEntry) { + // This will call (*bufferLogSink).Write + b.sink.LogEntry(ctx, e) +} + +// Sync implements slog.Sink. +func (b *bufferLogSink) Sync() { + b.sink.Sync() +} + +// processIPLocked will look up the IP in the cache, or hash and salt it and add +// to the cache. It will also add it to hashedIPs. +// +// b.mu must be held. +func (b *bufferLogSink) processIPLocked(ip string) (string, error) { + if hashStr, ok := b.ipToHash[ip]; ok { + return hashStr, nil + } + + addr, err := netip.ParseAddr(ip) + if err != nil { + return "", xerrors.Errorf("failed to parse IP %q: %w", ip, err) + } + version := int32(4) + if addr.Is6() { + version = 6 + } + + class := proto.IPFields_PUBLIC + switch { + case addr.IsLoopback(): + class = proto.IPFields_LOOPBACK + case addr.IsLinkLocalUnicast(): + class = proto.IPFields_LINK_LOCAL + case addr.IsLinkLocalMulticast(): + class = proto.IPFields_LINK_LOCAL + case addr.IsPrivate(): + class = proto.IPFields_PRIVATE + } + + hash := sha256.Sum256([]byte(b.hashSalt + ip)) + hashStr := hex.EncodeToString(hash[:]) + b.ipToHash[ip] = hashStr + b.hashedIPs[hashStr] = &proto.IPFields{ + Version: version, + Class: class, + } + return hashStr, nil +} diff --git a/tailnet/logger_internal_test.go b/tailnet/logger_internal_test.go new file mode 100644 index 0000000000000..7b0b253c39672 --- /dev/null +++ b/tailnet/logger_internal_test.go @@ -0,0 +1,145 @@ +package tailnet + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/tailnet/proto" + + "cdr.dev/slog" +) + +func TestBufferLogSink(t *testing.T) { + t.Parallel() + + t.Run("NoIP", func(t *testing.T) { + t.Parallel() + ctx := context.Background() + sink, err := newBufferLogSink() + require.NoError(t, err) + logger := slog.Make(sink).Leveled(slog.LevelDebug) + + logger.Debug(ctx, "line1") + logger.Debug(ctx, "line2 fe80") + logger.Debug(ctx, "line3 xxxx::x") + + logs, hashes := sink.getLogs() + require.Len(t, logs, 3) + require.Len(t, hashes, 0) + require.Contains(t, logs[0], "line1") + require.Contains(t, logs[1], "line2 fe80") + require.Contains(t, logs[2], "line3 xxxx::x") + }) + + t.Run("OneOrMoreIPs", func(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + ip string + expectedVersion int32 + expectedClass proto.IPFields_IPClass + }{ + { + name: "IPv4/Public", + ip: "142.250.71.78", + expectedVersion: 4, + expectedClass: proto.IPFields_PUBLIC, + }, + { + name: "IPv4/Private", + ip: "192.168.0.1", + expectedVersion: 4, + expectedClass: proto.IPFields_PRIVATE, + }, + { + name: "IPv4/LinkLocal", + ip: "169.254.1.1", + expectedVersion: 4, + expectedClass: proto.IPFields_LINK_LOCAL, + }, + { + name: "IPv4/Loopback", + ip: "127.0.0.1", + expectedVersion: 4, + expectedClass: proto.IPFields_LOOPBACK, + }, + { + name: "IPv6/Public", + ip: "2404:6800:4006:812::200e", + expectedVersion: 6, + expectedClass: proto.IPFields_PUBLIC, + }, + { + name: "IPv6/Private", + ip: "fd12:3456:789a:1::1", + expectedVersion: 6, + expectedClass: proto.IPFields_PRIVATE, + }, + { + name: "IPv6/LinkLocal", + ip: "fe80::1", + expectedVersion: 6, + expectedClass: proto.IPFields_LINK_LOCAL, + }, + { + name: "IPv6/Loopback", + ip: "::1", + expectedVersion: 6, + expectedClass: proto.IPFields_LOOPBACK, + }, + { + name: "IPv6/IPv4Mapped", + ip: "::ffff:1.2.3.4", + expectedVersion: 6, + expectedClass: proto.IPFields_PUBLIC, + }, + } + + for _, c := range cases { + c := c + t.Run(c.name, func(t *testing.T) { + t.Parallel() + ctx := context.Background() + sink, err := newBufferLogSink() + require.NoError(t, err) + logger := slog.Make(sink).Leveled(slog.LevelDebug) + + ipWithPort := c.ip + ":8080" + if c.expectedVersion == 6 { + ipWithPort = fmt.Sprintf("[%s]:8080", c.ip) + } + + logger.Debug(ctx, "line1", slog.F("ip", c.ip)) + logger.Debug(ctx, fmt.Sprintf("line2: %s/24", c.ip)) + logger.Debug(ctx, fmt.Sprintf("line3: %s foo (%s)", ipWithPort, c.ip)) + + logs, hashes := sink.getLogs() + require.Len(t, logs, 3) + require.Len(t, hashes, 1) + for _, log := range logs { + t.Log(log) + } + + // This only runs once since we only processed a single IP. + for expectedHash, ipFields := range hashes { + hashedIPWithPort := expectedHash + ":8080" + if c.expectedVersion == 6 { + hashedIPWithPort = fmt.Sprintf("[%s]:8080", expectedHash) + } + + require.Contains(t, logs[0], "line1") + require.Contains(t, logs[0], "ip="+expectedHash) + require.Contains(t, logs[1], fmt.Sprintf("line2: %s/24", expectedHash)) + require.Contains(t, logs[2], fmt.Sprintf("line3: %s foo (%s)", hashedIPWithPort, expectedHash)) + + require.Equal(t, c.expectedVersion, ipFields.Version) + require.Equal(t, c.expectedClass, ipFields.Class) + } + }) + } + }) +} From b7870d2e50444e054875344c95d4fd8a9057214f Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Wed, 26 Jun 2024 15:43:41 +0000 Subject: [PATCH 13/14] fixup --- codersdk/workspacesdk/connector.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/codersdk/workspacesdk/connector.go b/codersdk/workspacesdk/connector.go index 9c65fbd48678c..dc4bebd147e09 100644 --- a/codersdk/workspacesdk/connector.go +++ b/codersdk/workspacesdk/connector.go @@ -38,6 +38,7 @@ type tailnetConn interface { // // 1) run the Coordinate API and pass node information back and forth // 2) stream DERPMap updates and program the Conn +// 3) Send network telemetry events // // These functions share the same websocket, and so are combined here so that if we hit a problem // we tear the whole thing down and start over with a new websocket. @@ -58,7 +59,6 @@ type tailnetAPIConnector struct { coordinateURL string dialOptions *websocket.DialOptions conn tailnetConn - customDialFn func() (proto.DRPCTailnetClient, error) clientMu sync.RWMutex client proto.DRPCTailnetClient @@ -125,10 +125,6 @@ var permanentErrorStatuses = []int{ } func (tac *tailnetAPIConnector) dial() (proto.DRPCTailnetClient, error) { - if tac.customDialFn != nil { - return tac.customDialFn() - } - tac.logger.Debug(tac.ctx, "dialing Coder tailnet v2+ API") // nolint:bodyclose ws, res, err := websocket.Dial(tac.ctx, tac.coordinateURL, tac.dialOptions) From a7eca55e619509c57c057ff516c3e022fd9b6a5b Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Wed, 26 Jun 2024 15:49:34 +0000 Subject: [PATCH 14/14] fixup --- cli/ping.go | 2 +- cli/portforward.go | 2 +- cli/speedtest.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/ping.go b/cli/ping.go index ff6c318cc1b84..4d4e9308909dc 100644 --- a/cli/ping.go +++ b/cli/ping.go @@ -58,7 +58,7 @@ func (r *RootCmd) ping() *serpent.Command { _, _ = fmt.Fprintln(inv.Stderr, "Direct connections disabled.") opts.BlockEndpoints = true } - if r.noNetworkTelemetry { + if !r.noNetworkTelemetry { opts.EnableTelemetry = true } conn, err := workspacesdk.New(client).DialAgent(ctx, workspaceAgent.ID, opts) diff --git a/cli/portforward.go b/cli/portforward.go index bef0bb6f12be1..2a0126032e3bb 100644 --- a/cli/portforward.go +++ b/cli/portforward.go @@ -106,7 +106,7 @@ func (r *RootCmd) portForward() *serpent.Command { _, _ = fmt.Fprintln(inv.Stderr, "Direct connections disabled.") opts.BlockEndpoints = true } - if r.noNetworkTelemetry { + if !r.noNetworkTelemetry { opts.EnableTelemetry = true } conn, err := workspacesdk.New(client).DialAgent(ctx, workspaceAgent.ID, opts) diff --git a/cli/speedtest.go b/cli/speedtest.go index 3f34e5796f66f..9f23d0c58f780 100644 --- a/cli/speedtest.go +++ b/cli/speedtest.go @@ -102,7 +102,7 @@ func (r *RootCmd) speedtest() *serpent.Command { _, _ = fmt.Fprintln(inv.Stderr, "Direct connections disabled.") opts.BlockEndpoints = true } - if r.noNetworkTelemetry { + if !r.noNetworkTelemetry { opts.EnableTelemetry = true } if pcapFile != "" {