diff --git a/cli/ssh.go b/cli/ssh.go index 51f53e10bcbd2..4adbf12cccf7e 100644 --- a/cli/ssh.go +++ b/cli/ssh.go @@ -16,7 +16,6 @@ import ( "path/filepath" "regexp" "slices" - "strconv" "strings" "sync" "time" @@ -31,7 +30,6 @@ import ( "golang.org/x/term" "golang.org/x/xerrors" "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" - "tailscale.com/tailcfg" "tailscale.com/types/netlogtype" "cdr.dev/slog" @@ -40,11 +38,13 @@ import ( "github.com/coder/coder/v2/cli/cliui" "github.com/coder/coder/v2/cli/cliutil" "github.com/coder/coder/v2/coderd/autobuild/notify" + "github.com/coder/coder/v2/coderd/util/maps" "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/workspacesdk" "github.com/coder/coder/v2/cryptorand" "github.com/coder/coder/v2/pty" + "github.com/coder/coder/v2/tailnet" "github.com/coder/quartz" "github.com/coder/retry" "github.com/coder/serpent" @@ -1456,28 +1456,6 @@ func collectNetworkStats(ctx context.Context, agentConn *workspacesdk.AgentConn, } node := agentConn.Node() derpMap := agentConn.DERPMap() - derpLatency := map[string]float64{} - - // Convert DERP region IDs to friendly names for display in the UI. - for rawRegion, latency := range node.DERPLatency { - regionParts := strings.SplitN(rawRegion, "-", 2) - regionID, err := strconv.Atoi(regionParts[0]) - if err != nil { - continue - } - region, found := derpMap.Regions[regionID] - if !found { - // It's possible that a workspace agent is using an old DERPMap - // and reports regions that do not exist. If that's the case, - // report the region as unknown! - region = &tailcfg.DERPRegion{ - RegionID: regionID, - RegionName: fmt.Sprintf("Unnamed %d", regionID), - } - } - // Convert the microseconds to milliseconds. - derpLatency[region.RegionName] = latency * 1000 - } totalRx := uint64(0) totalTx := uint64(0) @@ -1491,27 +1469,20 @@ func collectNetworkStats(ctx context.Context, agentConn *workspacesdk.AgentConn, uploadSecs := float64(totalTx) / dur.Seconds() downloadSecs := float64(totalRx) / dur.Seconds() - // Sometimes the preferred DERP doesn't match the one we're actually - // connected with. Perhaps because the agent prefers a different DERP and - // we're using that server instead. - preferredDerpID := node.PreferredDERP - if pingResult.DERPRegionID != 0 { - preferredDerpID = pingResult.DERPRegionID - } - preferredDerp, ok := derpMap.Regions[preferredDerpID] - preferredDerpName := fmt.Sprintf("Unnamed %d", preferredDerpID) - if ok { - preferredDerpName = preferredDerp.RegionName - } + preferredDerpName := tailnet.ExtractPreferredDERPName(pingResult, node, derpMap) + derpLatency := tailnet.ExtractDERPLatency(node, derpMap) if _, ok := derpLatency[preferredDerpName]; !ok { derpLatency[preferredDerpName] = 0 } + derpLatencyMs := maps.Map(derpLatency, func(dur time.Duration) float64 { + return float64(dur) / float64(time.Millisecond) + }) return &sshNetworkStats{ P2P: p2p, Latency: float64(latency.Microseconds()) / 1000, PreferredDERP: preferredDerpName, - DERPLatency: derpLatency, + DERPLatency: derpLatencyMs, UploadBytesSec: int64(uploadSecs), DownloadBytesSec: int64(downloadSecs), }, nil diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go index 40b1423a0f730..9978aa0bcaff5 100644 --- a/coderd/database/db2sdk/db2sdk.go +++ b/coderd/database/db2sdk/db2sdk.go @@ -47,14 +47,6 @@ func ListLazy[F any, T any](convert func(F) T) func(list []F) []T { } } -func Map[K comparable, F any, T any](params map[K]F, convert func(F) T) map[K]T { - into := make(map[K]T) - for k, item := range params { - into[k] = convert(item) - } - return into -} - type ExternalAuthMeta struct { Authenticated bool ValidateError string diff --git a/coderd/util/maps/maps.go b/coderd/util/maps/maps.go index 8aaa6669cb8af..6a858bf3f7085 100644 --- a/coderd/util/maps/maps.go +++ b/coderd/util/maps/maps.go @@ -6,6 +6,14 @@ import ( "golang.org/x/exp/constraints" ) +func Map[K comparable, F any, T any](params map[K]F, convert func(F) T) map[K]T { + into := make(map[K]T) + for k, item := range params { + into[k] = convert(item) + } + return into +} + // Subset returns true if all the keys of a are present // in b and have the same values. // If the corresponding value of a[k] is the zero value in diff --git a/tailnet/derpmap.go b/tailnet/derpmap.go index e2722c1ff9ab4..6f284dad05991 100644 --- a/tailnet/derpmap.go +++ b/tailnet/derpmap.go @@ -8,8 +8,11 @@ import ( "net/http" "os" "strconv" + "strings" + "time" "golang.org/x/xerrors" + "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" ) @@ -152,6 +155,49 @@ regionLoop: return derpMap, nil } +func ExtractPreferredDERPName(pingResult *ipnstate.PingResult, node *Node, derpMap *tailcfg.DERPMap) string { + // Sometimes the preferred DERP doesn't match the one we're actually + // connected with. Perhaps because the agent prefers a different DERP and + // we're using that server instead. + preferredDerpID := node.PreferredDERP + if pingResult.DERPRegionID != 0 { + preferredDerpID = pingResult.DERPRegionID + } + preferredDerp, ok := derpMap.Regions[preferredDerpID] + preferredDerpName := fmt.Sprintf("Unnamed %d", preferredDerpID) + if ok { + preferredDerpName = preferredDerp.RegionName + } + + return preferredDerpName +} + +// ExtractDERPLatency extracts a map of derp region names to their latencies +func ExtractDERPLatency(node *Node, derpMap *tailcfg.DERPMap) map[string]time.Duration { + latencyMs := make(map[string]time.Duration) + + // Convert DERP region IDs to friendly names for display in the UI. + for rawRegion, latency := range node.DERPLatency { + regionParts := strings.SplitN(rawRegion, "-", 2) + regionID, err := strconv.Atoi(regionParts[0]) + if err != nil { + continue + } + region, found := derpMap.Regions[regionID] + if !found { + // It's possible that a workspace agent is using an old DERPMap + // and reports regions that do not exist. If that's the case, + // report the region as unknown! + region = &tailcfg.DERPRegion{ + RegionID: regionID, + RegionName: fmt.Sprintf("Unnamed %d", regionID), + } + } + latencyMs[region.RegionName] = time.Duration(latency * float64(time.Second)) + } + return latencyMs +} + // CompareDERPMaps returns true if the given DERPMaps are equivalent. Ordering // of slices is ignored. // diff --git a/tailnet/derpmap_test.go b/tailnet/derpmap_test.go index a91969bfeca09..c723437cad0d2 100644 --- a/tailnet/derpmap_test.go +++ b/tailnet/derpmap_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/stretchr/testify/require" + "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" "github.com/coder/coder/v2/tailnet" @@ -162,3 +163,111 @@ func TestNewDERPMap(t *testing.T) { require.ErrorContains(t, err, "DERP map has no DERP nodes") }) } + +func TestExtractDERPLatency(t *testing.T) { + t.Parallel() + + derpMap := &tailcfg.DERPMap{ + Regions: map[int]*tailcfg.DERPRegion{ + 1: { + RegionID: 1, + RegionName: "Region One", + Nodes: []*tailcfg.DERPNode{ + {Name: "node1", RegionID: 1}, + }, + }, + 2: { + RegionID: 2, + RegionName: "Region Two", + Nodes: []*tailcfg.DERPNode{ + {Name: "node2", RegionID: 2}, + }, + }, + }, + } + + t.Run("Basic", func(t *testing.T) { + t.Parallel() + node := &tailnet.Node{ + DERPLatency: map[string]float64{ + "1-node1": 0.05, + "2-node2": 0.1, + }, + } + latencyMs := tailnet.ExtractDERPLatency(node, derpMap) + require.EqualValues(t, 50, latencyMs["Region One"].Milliseconds()) + require.EqualValues(t, 100, latencyMs["Region Two"].Milliseconds()) + require.Len(t, latencyMs, 2) + }) + + t.Run("UnknownRegion", func(t *testing.T) { + t.Parallel() + node := &tailnet.Node{ + DERPLatency: map[string]float64{ + "999-node999": 0.2, + }, + } + latencyMs := tailnet.ExtractDERPLatency(node, derpMap) + require.EqualValues(t, 200, latencyMs["Unnamed 999"].Milliseconds()) + require.Len(t, latencyMs, 1) + }) + + t.Run("InvalidRegionFormat", func(t *testing.T) { + t.Parallel() + node := &tailnet.Node{ + DERPLatency: map[string]float64{ + "invalid": 0.3, + "1-node1": 0.05, + "abc-node": 0.15, + }, + } + latencyMs := tailnet.ExtractDERPLatency(node, derpMap) + require.EqualValues(t, 50, latencyMs["Region One"].Milliseconds()) + require.Len(t, latencyMs, 1) + require.NotContains(t, latencyMs, "invalid") + require.NotContains(t, latencyMs, "abc-node") + }) + + t.Run("EmptyInput", func(t *testing.T) { + t.Parallel() + node := &tailnet.Node{ + DERPLatency: map[string]float64{}, + } + latencyMs := tailnet.ExtractDERPLatency(node, derpMap) + require.Empty(t, latencyMs) + }) +} + +func TestExtractPreferredDERPName(t *testing.T) { + t.Parallel() + derpMap := &tailcfg.DERPMap{ + Regions: map[int]*tailcfg.DERPRegion{ + 1: {RegionName: "New York"}, + 2: {RegionName: "London"}, + }, + } + + t.Run("UsesPingRegion", func(t *testing.T) { + t.Parallel() + pingResult := &ipnstate.PingResult{DERPRegionID: 2} + node := &tailnet.Node{PreferredDERP: 1} + result := tailnet.ExtractPreferredDERPName(pingResult, node, derpMap) + require.Equal(t, "London", result) + }) + + t.Run("UsesNodePreferred", func(t *testing.T) { + t.Parallel() + pingResult := &ipnstate.PingResult{DERPRegionID: 0} + node := &tailnet.Node{PreferredDERP: 1} + result := tailnet.ExtractPreferredDERPName(pingResult, node, derpMap) + require.Equal(t, "New York", result) + }) + + t.Run("UnknownRegion", func(t *testing.T) { + t.Parallel() + pingResult := &ipnstate.PingResult{DERPRegionID: 99} + node := &tailnet.Node{PreferredDERP: 1} + result := tailnet.ExtractPreferredDERPName(pingResult, node, derpMap) + require.Equal(t, "Unnamed 99", result) + }) +} diff --git a/vpn/client.go b/vpn/client.go index da066bbcd62b3..e3f3e767fc477 100644 --- a/vpn/client.go +++ b/vpn/client.go @@ -5,11 +5,14 @@ import ( "net/http" "net/netip" "net/url" + "time" "golang.org/x/xerrors" + "tailscale.com/ipn/ipnstate" "tailscale.com/net/dns" "tailscale.com/net/netmon" + "tailscale.com/tailcfg" "tailscale.com/wgengine/router" "github.com/google/uuid" @@ -27,6 +30,9 @@ import ( type Conn interface { CurrentWorkspaceState() (tailnet.WorkspaceUpdate, error) GetPeerDiagnostics(peerID uuid.UUID) tailnet.PeerDiagnostics + Ping(ctx context.Context, agentID uuid.UUID) (time.Duration, bool, *ipnstate.PingResult, error) + Node() *tailnet.Node + DERPMap() *tailcfg.DERPMap Close() error } @@ -38,6 +44,10 @@ type vpnConn struct { updatesCtrl *tailnet.TunnelAllWorkspaceUpdatesController } +func (c *vpnConn) Ping(ctx context.Context, agentID uuid.UUID) (time.Duration, bool, *ipnstate.PingResult, error) { + return c.Conn.Ping(ctx, tailnet.TailscaleServicePrefix.AddrFromUUID(agentID)) +} + func (c *vpnConn) CurrentWorkspaceState() (tailnet.WorkspaceUpdate, error) { return c.updatesCtrl.CurrentState() } diff --git a/vpn/speaker_internal_test.go b/vpn/speaker_internal_test.go index 2f3d131093382..433868851a5bc 100644 --- a/vpn/speaker_internal_test.go +++ b/vpn/speaker_internal_test.go @@ -23,6 +23,8 @@ func TestMain(m *testing.M) { goleak.VerifyTestMain(m, testutil.GoleakOptions...) } +const expectedHandshake = "codervpn tunnel 1.2\n" + // TestSpeaker_RawPeer tests the speaker with a peer that we simulate by directly making reads and // writes to the other end of the pipe. There should be at least one test that does this, rather // than use 2 speakers so that we don't have a bug where we don't adhere to the stated protocol, but @@ -48,8 +50,6 @@ func TestSpeaker_RawPeer(t *testing.T) { errCh <- err }() - expectedHandshake := "codervpn tunnel 1.1\n" - b := make([]byte, 256) n, err := mp.Read(b) require.NoError(t, err) @@ -157,8 +157,6 @@ func TestSpeaker_OversizeHandshake(t *testing.T) { errCh <- err }() - expectedHandshake := "codervpn tunnel 1.1\n" - b := make([]byte, 256) n, err := mp.Read(b) require.NoError(t, err) @@ -210,7 +208,6 @@ func TestSpeaker_HandshakeInvalid(t *testing.T) { _, err = mp.Write([]byte(tc.handshake)) require.NoError(t, err) - expectedHandshake := "codervpn tunnel 1.1\n" b := make([]byte, 256) n, err := mp.Read(b) require.NoError(t, err) @@ -248,8 +245,6 @@ func TestSpeaker_CorruptMessage(t *testing.T) { errCh <- err }() - expectedHandshake := "codervpn tunnel 1.1\n" - b := make([]byte, 256) n, err := mp.Read(b) require.NoError(t, err) diff --git a/vpn/tunnel.go b/vpn/tunnel.go index 6c71aecaa0965..aa1d0e32ef5b9 100644 --- a/vpn/tunnel.go +++ b/vpn/tunnel.go @@ -19,6 +19,7 @@ import ( "github.com/google/uuid" "github.com/tailscale/wireguard-go/tun" "golang.org/x/xerrors" + "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" "tailscale.com/net/dns" "tailscale.com/net/netmon" @@ -32,9 +33,9 @@ import ( "github.com/coder/coder/v2/tailnet" ) -// netStatusInterval is the interval at which the tunnel sends network status updates to the manager. -// This is currently only used to keep `last_handshake` up to date. -const netStatusInterval = 10 * time.Second +// netStatusInterval is the interval at which the tunnel records latencies, +// and sends network status updates to the manager. +const netStatusInterval = 5 * time.Second type Tunnel struct { speaker[*TunnelMessage, *ManagerMessage, ManagerMessage] @@ -86,8 +87,9 @@ func NewTunnel( ctx: uCtx, cancel: uCancel, netLoopDone: make(chan struct{}), + logger: logger, uSendCh: s.sendCh, - agents: map[uuid.UUID]tailnet.Agent{}, + agents: map[uuid.UUID]agentWithPing{}, workspaces: map[uuid.UUID]tailnet.Workspace{}, clock: quartz.NewReal(), }, @@ -344,10 +346,12 @@ type updater struct { cancel context.CancelFunc netLoopDone chan struct{} + logger slog.Logger + mu sync.Mutex uSendCh chan<- *TunnelMessage // agents contains the agents that are currently connected to the tunnel. - agents map[uuid.UUID]tailnet.Agent + agents map[uuid.UUID]agentWithPing // workspaces contains the workspaces to which agents are currently connected via the tunnel. workspaces map[uuid.UUID]tailnet.Workspace conn Conn @@ -355,6 +359,26 @@ type updater struct { clock quartz.Clock } +type agentWithPing struct { + tailnet.Agent + // non-nil if a successful ping has been made + lastPing *lastPing +} + +func (a *agentWithPing) Clone() *agentWithPing { + return &agentWithPing{ + Agent: a.Agent.Clone(), + lastPing: a.lastPing, + } +} + +type lastPing struct { + pingDur time.Duration + didP2p bool + preferredDerp string + preferredDerpLatency *time.Duration +} + // Update pushes a workspace update to the manager func (u *updater) Update(update tailnet.WorkspaceUpdate) error { u.mu.Lock() @@ -412,10 +436,21 @@ func (u *updater) createPeerUpdateLocked(update tailnet.WorkspaceUpdate) *PeerUp DeletedAgents: make([]*Agent, len(update.DeletedAgents)), } + var upsertedAgentsWithPing []*agentWithPing + // save the workspace update to the tunnel's state, such that it can // be used to populate automated peer updates. for _, agent := range update.UpsertedAgents { - u.agents[agent.ID] = agent.Clone() + var lastPing *lastPing + if existing, ok := u.agents[agent.ID]; ok { + lastPing = existing.lastPing + } + upsertedAgent := agentWithPing{ + Agent: agent.Clone(), + lastPing: lastPing, + } + u.agents[agent.ID] = upsertedAgent + upsertedAgentsWithPing = append(upsertedAgentsWithPing, &upsertedAgent) } for _, agent := range update.DeletedAgents { delete(u.agents, agent.ID) @@ -435,7 +470,7 @@ func (u *updater) createPeerUpdateLocked(update tailnet.WorkspaceUpdate) *PeerUp } } - upsertedAgents := u.convertAgentsLocked(update.UpsertedAgents) + upsertedAgents := u.convertAgentsLocked(upsertedAgentsWithPing) out.UpsertedAgents = upsertedAgents for i, ws := range update.DeletedWorkspaces { out.DeletedWorkspaces[i] = &Workspace{ @@ -466,7 +501,7 @@ func (u *updater) createPeerUpdateLocked(update tailnet.WorkspaceUpdate) *PeerUp // convertAgentsLocked takes a list of `tailnet.Agent` and converts them to proto agents. // If there is an active connection, the last handshake time is populated. -func (u *updater) convertAgentsLocked(agents []*tailnet.Agent) []*Agent { +func (u *updater) convertAgentsLocked(agents []*agentWithPing) []*Agent { out := make([]*Agent, 0, len(agents)) for _, agent := range agents { @@ -477,12 +512,26 @@ func (u *updater) convertAgentsLocked(agents []*tailnet.Agent) []*Agent { sort.Slice(fqdn, func(i, j int) bool { return len(fqdn[i]) < len(fqdn[j]) }) + var lastPing *LastPing + if agent.lastPing != nil { + var preferredDerpLatency *durationpb.Duration + if agent.lastPing.preferredDerpLatency != nil { + preferredDerpLatency = durationpb.New(*agent.lastPing.preferredDerpLatency) + } + lastPing = &LastPing{ + Latency: durationpb.New(agent.lastPing.pingDur), + DidP2P: agent.lastPing.didP2p, + PreferredDerp: agent.lastPing.preferredDerp, + PreferredDerpLatency: preferredDerpLatency, + } + } protoAgent := &Agent{ Id: tailnet.UUIDToByteSlice(agent.ID), Name: agent.Name, WorkspaceId: tailnet.UUIDToByteSlice(agent.WorkspaceID), Fqdn: fqdn, IpAddrs: hostsToIPStrings(agent.Hosts), + LastPing: lastPing, } if u.conn != nil { diags := u.conn.GetPeerDiagnostics(agent.ID) @@ -514,8 +563,8 @@ func (u *updater) stop() error { return nil } err := u.conn.Close() - u.conn = nil u.cancel() + u.conn = nil return err } @@ -525,7 +574,7 @@ func (u *updater) sendAgentUpdate() { u.mu.Lock() defer u.mu.Unlock() - agents := make([]*tailnet.Agent, 0, len(u.agents)) + agents := make([]*agentWithPing, 0, len(u.agents)) for _, agent := range u.agents { agents = append(agents, &agent) } @@ -558,17 +607,85 @@ func (u *updater) netStatusLoop() { case <-u.ctx.Done(): return case <-ticker.C: + u.recordLatencies() u.sendAgentUpdate() } } } +func (u *updater) recordLatencies() { + var agentsIDsToPing []uuid.UUID + u.mu.Lock() + for _, agent := range u.agents { + agentsIDsToPing = append(agentsIDsToPing, agent.ID) + } + conn := u.conn + u.mu.Unlock() + + if conn == nil { + u.logger.Debug(u.ctx, "skipping pings as tunnel is not connected") + return + } + + go func() { + // We need a waitgroup to cancel the context after all pings are done. + var wg sync.WaitGroup + pingCtx, cancelFunc := context.WithTimeout(u.ctx, netStatusInterval) + defer cancelFunc() + for _, agentID := range agentsIDsToPing { + wg.Add(1) + go func() { + defer wg.Done() + + pingDur, didP2p, pingResult, err := conn.Ping(pingCtx, agentID) + if err != nil { + u.logger.Warn(u.ctx, "failed to ping agent", slog.F("agent_id", agentID), slog.Error(err)) + return + } + + // We fetch the Node and DERPMap after each ping, as it may have + // changed. + node := conn.Node() + derpMap := conn.DERPMap() + if node == nil || derpMap == nil { + u.logger.Warn(u.ctx, "failed to get DERP map or node after ping") + return + } + derpLatencies := tailnet.ExtractDERPLatency(node, derpMap) + preferredDerp := tailnet.ExtractPreferredDERPName(pingResult, node, derpMap) + var preferredDerpLatency *time.Duration + if derpLatency, ok := derpLatencies[preferredDerp]; ok { + preferredDerpLatency = &derpLatency + } else { + u.logger.Debug(u.ctx, "preferred DERP not found in DERP latency map", slog.F("preferred_derp", preferredDerp)) + } + + // Write back results + u.mu.Lock() + defer u.mu.Unlock() + if agent, ok := u.agents[agentID]; ok { + agent.lastPing = &lastPing{ + pingDur: pingDur, + didP2p: didP2p, + preferredDerp: preferredDerp, + preferredDerpLatency: preferredDerpLatency, + } + u.agents[agentID] = agent + } else { + u.logger.Debug(u.ctx, "ignoring ping result for unknown agent", slog.F("agent_id", agentID)) + } + }() + } + wg.Wait() + }() +} + // processSnapshotUpdate handles the logic when a full state update is received. // When the tunnel is live, we only receive diffs, but the first packet on any given // reconnect to the tailnet API is a full state. // Without this logic we weren't processing deletes for any workspaces or agents deleted // while the client was disconnected while the computer was asleep. -func processSnapshotUpdate(update *tailnet.WorkspaceUpdate, agents map[uuid.UUID]tailnet.Agent, workspaces map[uuid.UUID]tailnet.Workspace) { +func processSnapshotUpdate(update *tailnet.WorkspaceUpdate, agents map[uuid.UUID]agentWithPing, workspaces map[uuid.UUID]tailnet.Workspace) { // ignoredWorkspaces is initially populated with the workspaces that are // in the current update. Later on we populate it with the deleted workspaces too // so that we don't send duplicate updates. Same applies to ignoredAgents. diff --git a/vpn/tunnel_internal_test.go b/vpn/tunnel_internal_test.go index 15eb9cf569f5e..5c4e6ec03d47f 100644 --- a/vpn/tunnel_internal_test.go +++ b/vpn/tunnel_internal_test.go @@ -15,10 +15,13 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/timestamppb" + "tailscale.com/ipn/ipnstate" + "tailscale.com/tailcfg" "tailscale.com/util/dnsname" "github.com/coder/quartz" + maputil "github.com/coder/coder/v2/coderd/util/maps" "github.com/coder/coder/v2/tailnet" "github.com/coder/coder/v2/tailnet/proto" "github.com/coder/coder/v2/testutil" @@ -57,15 +60,59 @@ func newFakeConn(state tailnet.WorkspaceUpdate, hsTime time.Time) *fakeConn { } } +func (f *fakeConn) withManualPings() *fakeConn { + f.returnPing = make(chan struct{}) + return f +} + type fakeConn struct { - state tailnet.WorkspaceUpdate - hsTime time.Time - closed chan struct{} - doClose sync.Once + state tailnet.WorkspaceUpdate + returnPing chan struct{} + hsTime time.Time + closed chan struct{} + doClose sync.Once +} + +func (*fakeConn) DERPMap() *tailcfg.DERPMap { + return &tailcfg.DERPMap{ + Regions: map[int]*tailcfg.DERPRegion{ + 999: { + RegionID: 999, + RegionCode: "zzz", + RegionName: "Coder Region", + }, + }, + } +} + +func (*fakeConn) Node() *tailnet.Node { + return &tailnet.Node{ + PreferredDERP: 999, + DERPLatency: map[string]float64{ + "999": 0.1, + }, + } } var _ Conn = (*fakeConn)(nil) +func (f *fakeConn) Ping(ctx context.Context, agentID uuid.UUID) (time.Duration, bool, *ipnstate.PingResult, error) { + if f.returnPing == nil { + return time.Millisecond * 100, true, &ipnstate.PingResult{ + DERPRegionID: 999, + }, nil + } + + select { + case <-ctx.Done(): + return 0, false, nil, ctx.Err() + case <-f.returnPing: + return time.Millisecond * 100, true, &ipnstate.PingResult{ + DERPRegionID: 999, + }, nil + } +} + func (f *fakeConn) CurrentWorkspaceState() (tailnet.WorkspaceUpdate, error) { return f.state, nil } @@ -292,7 +339,7 @@ func TestUpdater_createPeerUpdate(t *testing.T) { updater := updater{ ctx: ctx, netLoopDone: make(chan struct{}), - agents: map[uuid.UUID]tailnet.Agent{}, + agents: map[uuid.UUID]agentWithPing{}, workspaces: map[uuid.UUID]tailnet.Workspace{}, conn: newFakeConn(tailnet.WorkspaceUpdate{}, hsTime), } @@ -430,6 +477,22 @@ func TestTunnel_sendAgentUpdate(t *testing.T) { require.Equal(t, hsTime, req.msg.GetPeerUpdate().UpsertedAgents[0].LastHandshake.AsTime()) } + // Latency is gathered in the background, so it'll eventually be sent + testutil.Eventually(ctx, t, func(ctx context.Context) bool { + mClock.AdvanceNext() + req = testutil.TryReceive(ctx, t, mgr.requests) + if len(req.msg.GetPeerUpdate().UpsertedAgents) == 0 { + return false + } + if req.msg.GetPeerUpdate().UpsertedAgents[0].LastPing == nil { + return false + } + if req.msg.GetPeerUpdate().UpsertedAgents[0].LastPing.Latency.AsDuration().Milliseconds() != 100 { + return false + } + return req.msg.GetPeerUpdate().UpsertedAgents[0].LastPing.PreferredDerp == "Coder Region" + }, testutil.IntervalFast) + // Upsert a new agent err = tun.Update(tailnet.WorkspaceUpdate{ UpsertedWorkspaces: []*tailnet.Workspace{}, @@ -459,6 +522,10 @@ func TestTunnel_sendAgentUpdate(t *testing.T) { require.Equal(t, aID1[:], req.msg.GetPeerUpdate().UpsertedAgents[0].Id) require.Equal(t, hsTime, req.msg.GetPeerUpdate().UpsertedAgents[0].LastHandshake.AsTime()) + // The latency of the first agent is still set + require.NotNil(t, req.msg.GetPeerUpdate().UpsertedAgents[0].LastPing) + require.EqualValues(t, 100, req.msg.GetPeerUpdate().UpsertedAgents[0].LastPing.Latency.AsDuration().Milliseconds()) + require.Equal(t, aID2[:], req.msg.GetPeerUpdate().UpsertedAgents[1].Id) require.Equal(t, hsTime, req.msg.GetPeerUpdate().UpsertedAgents[1].LastHandshake.AsTime()) @@ -486,6 +553,22 @@ func TestTunnel_sendAgentUpdate(t *testing.T) { require.Len(t, req.msg.GetPeerUpdate().UpsertedAgents, 1) require.Equal(t, aID2[:], req.msg.GetPeerUpdate().UpsertedAgents[0].Id) require.Equal(t, hsTime, req.msg.GetPeerUpdate().UpsertedAgents[0].LastHandshake.AsTime()) + + // Eventually the second agent's latency is set + testutil.Eventually(ctx, t, func(ctx context.Context) bool { + mClock.AdvanceNext() + req = testutil.TryReceive(ctx, t, mgr.requests) + if len(req.msg.GetPeerUpdate().UpsertedAgents) == 0 { + return false + } + if req.msg.GetPeerUpdate().UpsertedAgents[0].LastPing == nil { + return false + } + if req.msg.GetPeerUpdate().UpsertedAgents[0].LastPing.Latency.AsDuration().Milliseconds() != 100 { + return false + } + return req.msg.GetPeerUpdate().UpsertedAgents[0].LastPing.PreferredDerp == "Coder Region" + }, testutil.IntervalFast) } func TestTunnel_sendAgentUpdateReconnect(t *testing.T) { @@ -693,6 +776,178 @@ func TestTunnel_sendAgentUpdateWorkspaceReconnect(t *testing.T) { require.Equal(t, wID1[:], peerUpdate.DeletedWorkspaces[0].Id) } +func TestTunnel_slowPing(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitShort) + + mClock := quartz.NewMock(t) + + wID1 := uuid.UUID{1} + aID1 := uuid.UUID{2} + hsTime := time.Now().Add(-time.Minute).UTC() + + client := newFakeClient(ctx, t) + conn := newFakeConn(tailnet.WorkspaceUpdate{}, hsTime).withManualPings() + + tun, mgr := setupTunnel(t, ctx, client, mClock) + errCh := make(chan error, 1) + var resp *TunnelMessage + go func() { + r, err := mgr.unaryRPC(ctx, &ManagerMessage{ + Msg: &ManagerMessage_Start{ + Start: &StartRequest{ + TunnelFileDescriptor: 2, + CoderUrl: "https://coder.example.com", + ApiToken: "fakeToken", + }, + }, + }) + resp = r + errCh <- err + }() + testutil.RequireSend(ctx, t, client.ch, conn) + err := testutil.TryReceive(ctx, t, errCh) + require.NoError(t, err) + _, ok := resp.Msg.(*TunnelMessage_Start) + require.True(t, ok) + + // Inform the tunnel of the initial state + err = tun.Update(tailnet.WorkspaceUpdate{ + UpsertedWorkspaces: []*tailnet.Workspace{ + { + ID: wID1, Name: "w1", Status: proto.Workspace_STARTING, + }, + }, + UpsertedAgents: []*tailnet.Agent{ + { + ID: aID1, + Name: "agent1", + WorkspaceID: wID1, + Hosts: map[dnsname.FQDN][]netip.Addr{ + "agent1.coder.": {netip.MustParseAddr("fd60:627a:a42b:0101::")}, + }, + }, + }, + }) + require.NoError(t, err) + req := testutil.TryReceive(ctx, t, mgr.requests) + require.Nil(t, req.msg.Rpc) + require.NotNil(t, req.msg.GetPeerUpdate()) + require.Len(t, req.msg.GetPeerUpdate().UpsertedAgents, 1) + require.Equal(t, aID1[:], req.msg.GetPeerUpdate().UpsertedAgents[0].Id) + + // We can't check that it *never* pings, so the best we can do is + // check it doesn't ping even with 5 goroutines attempting to, + // and that updates are received as normal + for range 5 { + mClock.AdvanceNext() + require.Nil(t, req.msg.GetPeerUpdate().UpsertedAgents[0].LastPing) + } + + // Provided that it hasn't been 5 seconds since the last AdvanceNext call, + // there'll be a ping in-flight that will return with this message + testutil.RequireSend(ctx, t, conn.returnPing, struct{}{}) + // Which will mean we'll eventually receive a PeerUpdate with the ping + testutil.Eventually(ctx, t, func(ctx context.Context) bool { + mClock.AdvanceNext() + req = testutil.TryReceive(ctx, t, mgr.requests) + if len(req.msg.GetPeerUpdate().UpsertedAgents) == 0 { + return false + } + if req.msg.GetPeerUpdate().UpsertedAgents[0].LastPing == nil { + return false + } + if req.msg.GetPeerUpdate().UpsertedAgents[0].LastPing.Latency.AsDuration().Milliseconds() != 100 { + return false + } + return req.msg.GetPeerUpdate().UpsertedAgents[0].LastPing.PreferredDerp == "Coder Region" + }, testutil.IntervalFast) +} + +func TestTunnel_stopMidPing(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitShort) + + mClock := quartz.NewMock(t) + + wID1 := uuid.UUID{1} + aID1 := uuid.UUID{2} + hsTime := time.Now().Add(-time.Minute).UTC() + + client := newFakeClient(ctx, t) + conn := newFakeConn(tailnet.WorkspaceUpdate{}, hsTime).withManualPings() + + tun, mgr := setupTunnel(t, ctx, client, mClock) + errCh := make(chan error, 1) + var resp *TunnelMessage + go func() { + r, err := mgr.unaryRPC(ctx, &ManagerMessage{ + Msg: &ManagerMessage_Start{ + Start: &StartRequest{ + TunnelFileDescriptor: 2, + CoderUrl: "https://coder.example.com", + ApiToken: "fakeToken", + }, + }, + }) + resp = r + errCh <- err + }() + testutil.RequireSend(ctx, t, client.ch, conn) + err := testutil.TryReceive(ctx, t, errCh) + require.NoError(t, err) + _, ok := resp.Msg.(*TunnelMessage_Start) + require.True(t, ok) + + // Inform the tunnel of the initial state + err = tun.Update(tailnet.WorkspaceUpdate{ + UpsertedWorkspaces: []*tailnet.Workspace{ + { + ID: wID1, Name: "w1", Status: proto.Workspace_STARTING, + }, + }, + UpsertedAgents: []*tailnet.Agent{ + { + ID: aID1, + Name: "agent1", + WorkspaceID: wID1, + Hosts: map[dnsname.FQDN][]netip.Addr{ + "agent1.coder.": {netip.MustParseAddr("fd60:627a:a42b:0101::")}, + }, + }, + }, + }) + require.NoError(t, err) + req := testutil.TryReceive(ctx, t, mgr.requests) + require.Nil(t, req.msg.Rpc) + require.NotNil(t, req.msg.GetPeerUpdate()) + require.Len(t, req.msg.GetPeerUpdate().UpsertedAgents, 1) + require.Equal(t, aID1[:], req.msg.GetPeerUpdate().UpsertedAgents[0].Id) + + // We'll have some pings in flight when we stop + for range 5 { + mClock.AdvanceNext() + req = testutil.TryReceive(ctx, t, mgr.requests) + require.Nil(t, req.msg.GetPeerUpdate().UpsertedAgents[0].LastPing) + } + + // Stop the tunnel + go func() { + r, err := mgr.unaryRPC(ctx, &ManagerMessage{ + Msg: &ManagerMessage_Stop{}, + }) + resp = r + errCh <- err + }() + testutil.TryReceive(ctx, t, conn.closed) + err = testutil.TryReceive(ctx, t, errCh) + require.NoError(t, err) + _, ok = resp.Msg.(*TunnelMessage_Stop) + require.True(t, ok) +} + //nolint:revive // t takes precedence func setupTunnel(t *testing.T, ctx context.Context, client *fakeClient, mClock *quartz.Mock) (*Tunnel, *speaker[*ManagerMessage, *TunnelMessage, TunnelMessage]) { mp, tp := net.Pipe() @@ -902,11 +1157,13 @@ func TestProcessFreshState(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - agentsCopy := make(map[uuid.UUID]tailnet.Agent) - maps.Copy(agentsCopy, tt.initialAgents) - - workspaceCopy := make(map[uuid.UUID]tailnet.Workspace) - maps.Copy(workspaceCopy, tt.initialWorkspaces) + agentsCopy := maputil.Map(tt.initialAgents, func(a tailnet.Agent) agentWithPing { + return agentWithPing{ + Agent: a.Clone(), + lastPing: nil, + } + }) + workspaceCopy := maps.Clone(tt.initialWorkspaces) processSnapshotUpdate(tt.update, agentsCopy, workspaceCopy) diff --git a/vpn/version.go b/vpn/version.go index 91aac9175f748..2bf815e903e29 100644 --- a/vpn/version.go +++ b/vpn/version.go @@ -16,7 +16,14 @@ var CurrentSupportedVersions = RPCVersionList{ // - device_id: Coder Desktop device ID // - device_os: Coder Desktop OS information // - coder_desktop_version: Coder Desktop version - {Major: 1, Minor: 1}, + // 1.2 adds network related information to Agent: + // - last_ping: + // - latency: RTT of the most recently sent ping + // - did_p2p: Whether the last ping was sent over P2P + // - preferred_derp: The server that DERP relayed connections are + // using, if they're not using P2P. + // - preferred_derp_latency: The latency to the preferred DERP + {Major: 1, Minor: 2}, }, } diff --git a/vpn/vpn.pb.go b/vpn/vpn.pb.go index c89d3e51e6c92..bc5829d763dfd 100644 --- a/vpn/vpn.pb.go +++ b/vpn/vpn.pb.go @@ -9,6 +9,7 @@ package vpn import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + durationpb "google.golang.org/protobuf/types/known/durationpb" timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" @@ -205,7 +206,7 @@ func (x Status_Lifecycle) Number() protoreflect.EnumNumber { // Deprecated: Use Status_Lifecycle.Descriptor instead. func (Status_Lifecycle) EnumDescriptor() ([]byte, []int) { - return file_vpn_vpn_proto_rawDescGZIP(), []int{17, 0} + return file_vpn_vpn_proto_rawDescGZIP(), []int{18, 0} } // RPC allows a very simple unary request/response RPC mechanism. The requester generates a unique @@ -986,6 +987,8 @@ type Agent struct { // last_handshake is the primary indicator of whether we are connected to a peer. Zero value or // anything longer than 5 minutes ago means there is a problem. LastHandshake *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=last_handshake,json=lastHandshake,proto3" json:"last_handshake,omitempty"` + // If unset, a successful ping has not yet been made. + LastPing *LastPing `protobuf:"bytes,7,opt,name=last_ping,json=lastPing,proto3,oneof" json:"last_ping,omitempty"` } func (x *Agent) Reset() { @@ -1062,6 +1065,90 @@ func (x *Agent) GetLastHandshake() *timestamppb.Timestamp { return nil } +func (x *Agent) GetLastPing() *LastPing { + if x != nil { + return x.LastPing + } + return nil +} + +type LastPing struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // latency is the RTT of the ping to the agent. + Latency *durationpb.Duration `protobuf:"bytes,1,opt,name=latency,proto3" json:"latency,omitempty"` + // did_p2p indicates whether the ping was sent P2P, or over DERP. + DidP2P bool `protobuf:"varint,2,opt,name=did_p2p,json=didP2p,proto3" json:"did_p2p,omitempty"` + // preferred_derp is the human readable name of the preferred DERP region, + // or the region used for the last ping, if it was sent over DERP. + PreferredDerp string `protobuf:"bytes,3,opt,name=preferred_derp,json=preferredDerp,proto3" json:"preferred_derp,omitempty"` + // preferred_derp_latency is the last known latency to the preferred DERP + // region. Unset if the region does not appear in the DERP map. + PreferredDerpLatency *durationpb.Duration `protobuf:"bytes,4,opt,name=preferred_derp_latency,json=preferredDerpLatency,proto3,oneof" json:"preferred_derp_latency,omitempty"` +} + +func (x *LastPing) Reset() { + *x = LastPing{} + if protoimpl.UnsafeEnabled { + mi := &file_vpn_vpn_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LastPing) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LastPing) ProtoMessage() {} + +func (x *LastPing) ProtoReflect() protoreflect.Message { + mi := &file_vpn_vpn_proto_msgTypes[10] + 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 LastPing.ProtoReflect.Descriptor instead. +func (*LastPing) Descriptor() ([]byte, []int) { + return file_vpn_vpn_proto_rawDescGZIP(), []int{10} +} + +func (x *LastPing) GetLatency() *durationpb.Duration { + if x != nil { + return x.Latency + } + return nil +} + +func (x *LastPing) GetDidP2P() bool { + if x != nil { + return x.DidP2P + } + return false +} + +func (x *LastPing) GetPreferredDerp() string { + if x != nil { + return x.PreferredDerp + } + return "" +} + +func (x *LastPing) GetPreferredDerpLatency() *durationpb.Duration { + if x != nil { + return x.PreferredDerpLatency + } + return nil +} + // NetworkSettingsRequest is based on // https://developer.apple.com/documentation/networkextension/nepackettunnelnetworksettings for // macOS. It is a request/response message with response NetworkSettingsResponse @@ -1081,7 +1168,7 @@ type NetworkSettingsRequest struct { func (x *NetworkSettingsRequest) Reset() { *x = NetworkSettingsRequest{} if protoimpl.UnsafeEnabled { - mi := &file_vpn_vpn_proto_msgTypes[10] + mi := &file_vpn_vpn_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1094,7 +1181,7 @@ func (x *NetworkSettingsRequest) String() string { func (*NetworkSettingsRequest) ProtoMessage() {} func (x *NetworkSettingsRequest) ProtoReflect() protoreflect.Message { - mi := &file_vpn_vpn_proto_msgTypes[10] + mi := &file_vpn_vpn_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1107,7 +1194,7 @@ func (x *NetworkSettingsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use NetworkSettingsRequest.ProtoReflect.Descriptor instead. func (*NetworkSettingsRequest) Descriptor() ([]byte, []int) { - return file_vpn_vpn_proto_rawDescGZIP(), []int{10} + return file_vpn_vpn_proto_rawDescGZIP(), []int{11} } func (x *NetworkSettingsRequest) GetTunnelOverheadBytes() uint32 { @@ -1166,7 +1253,7 @@ type NetworkSettingsResponse struct { func (x *NetworkSettingsResponse) Reset() { *x = NetworkSettingsResponse{} if protoimpl.UnsafeEnabled { - mi := &file_vpn_vpn_proto_msgTypes[11] + mi := &file_vpn_vpn_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1179,7 +1266,7 @@ func (x *NetworkSettingsResponse) String() string { func (*NetworkSettingsResponse) ProtoMessage() {} func (x *NetworkSettingsResponse) ProtoReflect() protoreflect.Message { - mi := &file_vpn_vpn_proto_msgTypes[11] + mi := &file_vpn_vpn_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1192,7 +1279,7 @@ func (x *NetworkSettingsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use NetworkSettingsResponse.ProtoReflect.Descriptor instead. func (*NetworkSettingsResponse) Descriptor() ([]byte, []int) { - return file_vpn_vpn_proto_rawDescGZIP(), []int{11} + return file_vpn_vpn_proto_rawDescGZIP(), []int{12} } func (x *NetworkSettingsResponse) GetSuccess() bool { @@ -1231,7 +1318,7 @@ type StartRequest struct { func (x *StartRequest) Reset() { *x = StartRequest{} if protoimpl.UnsafeEnabled { - mi := &file_vpn_vpn_proto_msgTypes[12] + mi := &file_vpn_vpn_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1244,7 +1331,7 @@ func (x *StartRequest) String() string { func (*StartRequest) ProtoMessage() {} func (x *StartRequest) ProtoReflect() protoreflect.Message { - mi := &file_vpn_vpn_proto_msgTypes[12] + mi := &file_vpn_vpn_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1257,7 +1344,7 @@ func (x *StartRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StartRequest.ProtoReflect.Descriptor instead. func (*StartRequest) Descriptor() ([]byte, []int) { - return file_vpn_vpn_proto_rawDescGZIP(), []int{12} + return file_vpn_vpn_proto_rawDescGZIP(), []int{13} } func (x *StartRequest) GetTunnelFileDescriptor() int32 { @@ -1321,7 +1408,7 @@ type StartResponse struct { func (x *StartResponse) Reset() { *x = StartResponse{} if protoimpl.UnsafeEnabled { - mi := &file_vpn_vpn_proto_msgTypes[13] + mi := &file_vpn_vpn_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1334,7 +1421,7 @@ func (x *StartResponse) String() string { func (*StartResponse) ProtoMessage() {} func (x *StartResponse) ProtoReflect() protoreflect.Message { - mi := &file_vpn_vpn_proto_msgTypes[13] + mi := &file_vpn_vpn_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1347,7 +1434,7 @@ func (x *StartResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StartResponse.ProtoReflect.Descriptor instead. func (*StartResponse) Descriptor() ([]byte, []int) { - return file_vpn_vpn_proto_rawDescGZIP(), []int{13} + return file_vpn_vpn_proto_rawDescGZIP(), []int{14} } func (x *StartResponse) GetSuccess() bool { @@ -1375,7 +1462,7 @@ type StopRequest struct { func (x *StopRequest) Reset() { *x = StopRequest{} if protoimpl.UnsafeEnabled { - mi := &file_vpn_vpn_proto_msgTypes[14] + mi := &file_vpn_vpn_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1388,7 +1475,7 @@ func (x *StopRequest) String() string { func (*StopRequest) ProtoMessage() {} func (x *StopRequest) ProtoReflect() protoreflect.Message { - mi := &file_vpn_vpn_proto_msgTypes[14] + mi := &file_vpn_vpn_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1401,7 +1488,7 @@ func (x *StopRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StopRequest.ProtoReflect.Descriptor instead. func (*StopRequest) Descriptor() ([]byte, []int) { - return file_vpn_vpn_proto_rawDescGZIP(), []int{14} + return file_vpn_vpn_proto_rawDescGZIP(), []int{15} } // StopResponse is a response to stopping the tunnel. After sending this response, the tunnel closes @@ -1418,7 +1505,7 @@ type StopResponse struct { func (x *StopResponse) Reset() { *x = StopResponse{} if protoimpl.UnsafeEnabled { - mi := &file_vpn_vpn_proto_msgTypes[15] + mi := &file_vpn_vpn_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1431,7 +1518,7 @@ func (x *StopResponse) String() string { func (*StopResponse) ProtoMessage() {} func (x *StopResponse) ProtoReflect() protoreflect.Message { - mi := &file_vpn_vpn_proto_msgTypes[15] + mi := &file_vpn_vpn_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1444,7 +1531,7 @@ func (x *StopResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StopResponse.ProtoReflect.Descriptor instead. func (*StopResponse) Descriptor() ([]byte, []int) { - return file_vpn_vpn_proto_rawDescGZIP(), []int{15} + return file_vpn_vpn_proto_rawDescGZIP(), []int{16} } func (x *StopResponse) GetSuccess() bool { @@ -1472,7 +1559,7 @@ type StatusRequest struct { func (x *StatusRequest) Reset() { *x = StatusRequest{} if protoimpl.UnsafeEnabled { - mi := &file_vpn_vpn_proto_msgTypes[16] + mi := &file_vpn_vpn_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1485,7 +1572,7 @@ func (x *StatusRequest) String() string { func (*StatusRequest) ProtoMessage() {} func (x *StatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_vpn_vpn_proto_msgTypes[16] + mi := &file_vpn_vpn_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1498,7 +1585,7 @@ func (x *StatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StatusRequest.ProtoReflect.Descriptor instead. func (*StatusRequest) Descriptor() ([]byte, []int) { - return file_vpn_vpn_proto_rawDescGZIP(), []int{16} + return file_vpn_vpn_proto_rawDescGZIP(), []int{17} } // Status is sent in response to a StatusRequest or broadcasted to all clients @@ -1519,7 +1606,7 @@ type Status struct { func (x *Status) Reset() { *x = Status{} if protoimpl.UnsafeEnabled { - mi := &file_vpn_vpn_proto_msgTypes[17] + mi := &file_vpn_vpn_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1532,7 +1619,7 @@ func (x *Status) String() string { func (*Status) ProtoMessage() {} func (x *Status) ProtoReflect() protoreflect.Message { - mi := &file_vpn_vpn_proto_msgTypes[17] + mi := &file_vpn_vpn_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1545,7 +1632,7 @@ func (x *Status) ProtoReflect() protoreflect.Message { // Deprecated: Use Status.ProtoReflect.Descriptor instead. func (*Status) Descriptor() ([]byte, []int) { - return file_vpn_vpn_proto_rawDescGZIP(), []int{17} + return file_vpn_vpn_proto_rawDescGZIP(), []int{18} } func (x *Status) GetLifecycle() Status_Lifecycle { @@ -1581,7 +1668,7 @@ type Log_Field struct { func (x *Log_Field) Reset() { *x = Log_Field{} if protoimpl.UnsafeEnabled { - mi := &file_vpn_vpn_proto_msgTypes[18] + mi := &file_vpn_vpn_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1594,7 +1681,7 @@ func (x *Log_Field) String() string { func (*Log_Field) ProtoMessage() {} func (x *Log_Field) ProtoReflect() protoreflect.Message { - mi := &file_vpn_vpn_proto_msgTypes[18] + mi := &file_vpn_vpn_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1642,7 +1729,7 @@ type NetworkSettingsRequest_DNSSettings struct { func (x *NetworkSettingsRequest_DNSSettings) Reset() { *x = NetworkSettingsRequest_DNSSettings{} if protoimpl.UnsafeEnabled { - mi := &file_vpn_vpn_proto_msgTypes[19] + mi := &file_vpn_vpn_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1655,7 +1742,7 @@ func (x *NetworkSettingsRequest_DNSSettings) String() string { func (*NetworkSettingsRequest_DNSSettings) ProtoMessage() {} func (x *NetworkSettingsRequest_DNSSettings) ProtoReflect() protoreflect.Message { - mi := &file_vpn_vpn_proto_msgTypes[19] + mi := &file_vpn_vpn_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1668,7 +1755,7 @@ func (x *NetworkSettingsRequest_DNSSettings) ProtoReflect() protoreflect.Message // Deprecated: Use NetworkSettingsRequest_DNSSettings.ProtoReflect.Descriptor instead. func (*NetworkSettingsRequest_DNSSettings) Descriptor() ([]byte, []int) { - return file_vpn_vpn_proto_rawDescGZIP(), []int{10, 0} + return file_vpn_vpn_proto_rawDescGZIP(), []int{11, 0} } func (x *NetworkSettingsRequest_DNSSettings) GetServers() []string { @@ -1722,7 +1809,7 @@ type NetworkSettingsRequest_IPv4Settings struct { func (x *NetworkSettingsRequest_IPv4Settings) Reset() { *x = NetworkSettingsRequest_IPv4Settings{} if protoimpl.UnsafeEnabled { - mi := &file_vpn_vpn_proto_msgTypes[20] + mi := &file_vpn_vpn_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1735,7 +1822,7 @@ func (x *NetworkSettingsRequest_IPv4Settings) String() string { func (*NetworkSettingsRequest_IPv4Settings) ProtoMessage() {} func (x *NetworkSettingsRequest_IPv4Settings) ProtoReflect() protoreflect.Message { - mi := &file_vpn_vpn_proto_msgTypes[20] + mi := &file_vpn_vpn_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1748,7 +1835,7 @@ func (x *NetworkSettingsRequest_IPv4Settings) ProtoReflect() protoreflect.Messag // Deprecated: Use NetworkSettingsRequest_IPv4Settings.ProtoReflect.Descriptor instead. func (*NetworkSettingsRequest_IPv4Settings) Descriptor() ([]byte, []int) { - return file_vpn_vpn_proto_rawDescGZIP(), []int{10, 1} + return file_vpn_vpn_proto_rawDescGZIP(), []int{11, 1} } func (x *NetworkSettingsRequest_IPv4Settings) GetAddrs() []string { @@ -1800,7 +1887,7 @@ type NetworkSettingsRequest_IPv6Settings struct { func (x *NetworkSettingsRequest_IPv6Settings) Reset() { *x = NetworkSettingsRequest_IPv6Settings{} if protoimpl.UnsafeEnabled { - mi := &file_vpn_vpn_proto_msgTypes[21] + mi := &file_vpn_vpn_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1813,7 +1900,7 @@ func (x *NetworkSettingsRequest_IPv6Settings) String() string { func (*NetworkSettingsRequest_IPv6Settings) ProtoMessage() {} func (x *NetworkSettingsRequest_IPv6Settings) ProtoReflect() protoreflect.Message { - mi := &file_vpn_vpn_proto_msgTypes[21] + mi := &file_vpn_vpn_proto_msgTypes[22] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1826,7 +1913,7 @@ func (x *NetworkSettingsRequest_IPv6Settings) ProtoReflect() protoreflect.Messag // Deprecated: Use NetworkSettingsRequest_IPv6Settings.ProtoReflect.Descriptor instead. func (*NetworkSettingsRequest_IPv6Settings) Descriptor() ([]byte, []int) { - return file_vpn_vpn_proto_rawDescGZIP(), []int{10, 2} + return file_vpn_vpn_proto_rawDescGZIP(), []int{11, 2} } func (x *NetworkSettingsRequest_IPv6Settings) GetAddrs() []string { @@ -1871,7 +1958,7 @@ type NetworkSettingsRequest_IPv4Settings_IPv4Route struct { func (x *NetworkSettingsRequest_IPv4Settings_IPv4Route) Reset() { *x = NetworkSettingsRequest_IPv4Settings_IPv4Route{} if protoimpl.UnsafeEnabled { - mi := &file_vpn_vpn_proto_msgTypes[22] + mi := &file_vpn_vpn_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1884,7 +1971,7 @@ func (x *NetworkSettingsRequest_IPv4Settings_IPv4Route) String() string { func (*NetworkSettingsRequest_IPv4Settings_IPv4Route) ProtoMessage() {} func (x *NetworkSettingsRequest_IPv4Settings_IPv4Route) ProtoReflect() protoreflect.Message { - mi := &file_vpn_vpn_proto_msgTypes[22] + mi := &file_vpn_vpn_proto_msgTypes[23] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1897,7 +1984,7 @@ func (x *NetworkSettingsRequest_IPv4Settings_IPv4Route) ProtoReflect() protorefl // Deprecated: Use NetworkSettingsRequest_IPv4Settings_IPv4Route.ProtoReflect.Descriptor instead. func (*NetworkSettingsRequest_IPv4Settings_IPv4Route) Descriptor() ([]byte, []int) { - return file_vpn_vpn_proto_rawDescGZIP(), []int{10, 1, 0} + return file_vpn_vpn_proto_rawDescGZIP(), []int{11, 1, 0} } func (x *NetworkSettingsRequest_IPv4Settings_IPv4Route) GetDestination() string { @@ -1935,7 +2022,7 @@ type NetworkSettingsRequest_IPv6Settings_IPv6Route struct { func (x *NetworkSettingsRequest_IPv6Settings_IPv6Route) Reset() { *x = NetworkSettingsRequest_IPv6Settings_IPv6Route{} if protoimpl.UnsafeEnabled { - mi := &file_vpn_vpn_proto_msgTypes[23] + mi := &file_vpn_vpn_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1948,7 +2035,7 @@ func (x *NetworkSettingsRequest_IPv6Settings_IPv6Route) String() string { func (*NetworkSettingsRequest_IPv6Settings_IPv6Route) ProtoMessage() {} func (x *NetworkSettingsRequest_IPv6Settings_IPv6Route) ProtoReflect() protoreflect.Message { - mi := &file_vpn_vpn_proto_msgTypes[23] + mi := &file_vpn_vpn_proto_msgTypes[24] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1961,7 +2048,7 @@ func (x *NetworkSettingsRequest_IPv6Settings_IPv6Route) ProtoReflect() protorefl // Deprecated: Use NetworkSettingsRequest_IPv6Settings_IPv6Route.ProtoReflect.Descriptor instead. func (*NetworkSettingsRequest_IPv6Settings_IPv6Route) Descriptor() ([]byte, []int) { - return file_vpn_vpn_proto_rawDescGZIP(), []int{10, 2, 0} + return file_vpn_vpn_proto_rawDescGZIP(), []int{11, 2, 0} } func (x *NetworkSettingsRequest_IPv6Settings_IPv6Route) GetDestination() string { @@ -1998,7 +2085,7 @@ type StartRequest_Header struct { func (x *StartRequest_Header) Reset() { *x = StartRequest_Header{} if protoimpl.UnsafeEnabled { - mi := &file_vpn_vpn_proto_msgTypes[24] + mi := &file_vpn_vpn_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2011,7 +2098,7 @@ func (x *StartRequest_Header) String() string { func (*StartRequest_Header) ProtoMessage() {} func (x *StartRequest_Header) ProtoReflect() protoreflect.Message { - mi := &file_vpn_vpn_proto_msgTypes[24] + mi := &file_vpn_vpn_proto_msgTypes[25] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2024,7 +2111,7 @@ func (x *StartRequest_Header) ProtoReflect() protoreflect.Message { // Deprecated: Use StartRequest_Header.ProtoReflect.Descriptor instead. func (*StartRequest_Header) Descriptor() ([]byte, []int) { - return file_vpn_vpn_proto_rawDescGZIP(), []int{12, 0} + return file_vpn_vpn_proto_rawDescGZIP(), []int{13, 0} } func (x *StartRequest_Header) GetName() string { @@ -2047,6 +2134,8 @@ var file_vpn_vpn_proto_rawDesc = []byte{ 0x0a, 0x0d, 0x76, 0x70, 0x6e, 0x2f, 0x76, 0x70, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x76, 0x70, 0x6e, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x3d, 0x0a, 0x03, 0x52, 0x50, 0x43, 0x12, 0x15, 0x0a, 0x06, 0x6d, 0x73, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x6d, 0x73, 0x67, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, @@ -2158,7 +2247,7 @@ var file_vpn_vpn_proto_rawDesc = []byte{ 0x49, 0x4c, 0x45, 0x44, 0x10, 0x06, 0x12, 0x0d, 0x0a, 0x09, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x49, 0x4e, 0x47, 0x10, 0x07, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x45, 0x44, 0x10, 0x08, 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x49, 0x4e, 0x47, 0x10, - 0x09, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x0a, 0x22, 0xc0, + 0x09, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x0a, 0x22, 0xff, 0x01, 0x0a, 0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, @@ -2171,148 +2260,167 @@ var file_vpn_vpn_proto_rawDesc = []byte{ 0x18, 0x06, 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, 0x0d, 0x6c, 0x61, 0x73, 0x74, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, - 0x65, 0x22, 0xb5, 0x0a, 0x0a, 0x16, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x74, - 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x15, - 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x6f, 0x76, 0x65, 0x72, 0x68, 0x65, 0x61, 0x64, 0x5f, - 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x13, 0x74, 0x75, 0x6e, - 0x6e, 0x65, 0x6c, 0x4f, 0x76, 0x65, 0x72, 0x68, 0x65, 0x61, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, - 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x74, 0x75, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x6d, - 0x74, 0x75, 0x12, 0x4a, 0x0a, 0x0c, 0x64, 0x6e, 0x73, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, - 0x67, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x76, 0x70, 0x6e, 0x2e, 0x4e, + 0x65, 0x12, 0x2f, 0x0a, 0x09, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x70, 0x69, 0x6e, 0x67, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x76, 0x70, 0x6e, 0x2e, 0x4c, 0x61, 0x73, 0x74, 0x50, + 0x69, 0x6e, 0x67, 0x48, 0x00, 0x52, 0x08, 0x6c, 0x61, 0x73, 0x74, 0x50, 0x69, 0x6e, 0x67, 0x88, + 0x01, 0x01, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x70, 0x69, 0x6e, 0x67, + 0x22, 0xf0, 0x01, 0x0a, 0x08, 0x4c, 0x61, 0x73, 0x74, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x33, 0x0a, + 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x01, 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, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, + 0x63, 0x79, 0x12, 0x17, 0x0a, 0x07, 0x64, 0x69, 0x64, 0x5f, 0x70, 0x32, 0x70, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x06, 0x64, 0x69, 0x64, 0x50, 0x32, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x70, + 0x72, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x5f, 0x64, 0x65, 0x72, 0x70, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0d, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x44, 0x65, + 0x72, 0x70, 0x12, 0x54, 0x0a, 0x16, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x5f, + 0x64, 0x65, 0x72, 0x70, 0x5f, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, + 0x14, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x44, 0x65, 0x72, 0x70, 0x4c, 0x61, + 0x74, 0x65, 0x6e, 0x63, 0x79, 0x88, 0x01, 0x01, 0x42, 0x19, 0x0a, 0x17, 0x5f, 0x70, 0x72, 0x65, + 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x5f, 0x64, 0x65, 0x72, 0x70, 0x5f, 0x6c, 0x61, 0x74, 0x65, + 0x6e, 0x63, 0x79, 0x22, 0xb5, 0x0a, 0x0a, 0x16, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, + 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x32, + 0x0a, 0x15, 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x6f, 0x76, 0x65, 0x72, 0x68, 0x65, 0x61, + 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x13, 0x74, + 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x4f, 0x76, 0x65, 0x72, 0x68, 0x65, 0x61, 0x64, 0x42, 0x79, 0x74, + 0x65, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x74, 0x75, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, + 0x03, 0x6d, 0x74, 0x75, 0x12, 0x4a, 0x0a, 0x0c, 0x64, 0x6e, 0x73, 0x5f, 0x73, 0x65, 0x74, 0x74, + 0x69, 0x6e, 0x67, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x76, 0x70, 0x6e, + 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x44, 0x4e, 0x53, 0x53, 0x65, 0x74, 0x74, 0x69, + 0x6e, 0x67, 0x73, 0x52, 0x0b, 0x64, 0x6e, 0x73, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, + 0x12, 0x32, 0x0a, 0x15, 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x72, 0x65, 0x6d, 0x6f, 0x74, + 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x13, 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x41, 0x64, 0x64, + 0x72, 0x65, 0x73, 0x73, 0x12, 0x4d, 0x0a, 0x0d, 0x69, 0x70, 0x76, 0x34, 0x5f, 0x73, 0x65, 0x74, + 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x76, 0x70, + 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x49, 0x50, 0x76, 0x34, 0x53, 0x65, 0x74, + 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x0c, 0x69, 0x70, 0x76, 0x34, 0x53, 0x65, 0x74, 0x74, 0x69, + 0x6e, 0x67, 0x73, 0x12, 0x4d, 0x0a, 0x0d, 0x69, 0x70, 0x76, 0x36, 0x5f, 0x73, 0x65, 0x74, 0x74, + 0x69, 0x6e, 0x67, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x76, 0x70, 0x6e, + 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x49, 0x50, 0x76, 0x36, 0x53, 0x65, 0x74, 0x74, + 0x69, 0x6e, 0x67, 0x73, 0x52, 0x0c, 0x69, 0x70, 0x76, 0x36, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, + 0x67, 0x73, 0x1a, 0xcb, 0x01, 0x0a, 0x0b, 0x44, 0x4e, 0x53, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, + 0x67, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x25, 0x0a, 0x0e, + 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x5f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x02, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, + 0x69, 0x6e, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, + 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x64, 0x6f, + 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x6d, 0x61, 0x74, + 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x35, 0x0a, 0x17, 0x6d, 0x61, 0x74, + 0x63, 0x68, 0x5f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x5f, 0x6e, 0x6f, 0x5f, 0x73, 0x65, + 0x61, 0x72, 0x63, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x6d, 0x61, 0x74, 0x63, + 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x4e, 0x6f, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, + 0x1a, 0xf4, 0x02, 0x0a, 0x0c, 0x49, 0x50, 0x76, 0x34, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, + 0x73, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x64, 0x64, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x05, 0x61, 0x64, 0x64, 0x72, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6e, 0x65, + 0x74, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x73, + 0x75, 0x62, 0x6e, 0x65, 0x74, 0x4d, 0x61, 0x73, 0x6b, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x6f, + 0x75, 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, + 0x65, 0x72, 0x12, 0x5b, 0x0a, 0x0f, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x5f, 0x72, + 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x76, 0x70, + 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x49, 0x50, 0x76, 0x34, 0x53, 0x65, 0x74, + 0x74, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x49, 0x50, 0x76, 0x34, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, + 0x0e, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, + 0x5b, 0x0a, 0x0f, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x5f, 0x72, 0x6f, 0x75, 0x74, + 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x76, 0x70, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x44, 0x4e, 0x53, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, - 0x73, 0x52, 0x0b, 0x64, 0x6e, 0x73, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x32, - 0x0a, 0x15, 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x5f, - 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x74, - 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, - 0x73, 0x73, 0x12, 0x4d, 0x0a, 0x0d, 0x69, 0x70, 0x76, 0x34, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, - 0x6e, 0x67, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x76, 0x70, 0x6e, 0x2e, - 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x49, 0x50, 0x76, 0x34, 0x53, 0x65, 0x74, 0x74, 0x69, - 0x6e, 0x67, 0x73, 0x52, 0x0c, 0x69, 0x70, 0x76, 0x34, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, - 0x73, 0x12, 0x4d, 0x0a, 0x0d, 0x69, 0x70, 0x76, 0x36, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, - 0x67, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x76, 0x70, 0x6e, 0x2e, 0x4e, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x49, 0x50, 0x76, 0x34, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, + 0x67, 0x73, 0x2e, 0x49, 0x50, 0x76, 0x34, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x0e, 0x65, 0x78, + 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x1a, 0x59, 0x0a, 0x09, + 0x49, 0x50, 0x76, 0x34, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, + 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, + 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6d, + 0x61, 0x73, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x61, 0x73, 0x6b, 0x12, + 0x16, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x1a, 0xf1, 0x02, 0x0a, 0x0c, 0x49, 0x50, 0x76, 0x36, + 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x64, 0x64, 0x72, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x61, 0x64, 0x64, 0x72, 0x73, 0x12, 0x25, + 0x0a, 0x0e, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x73, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x0d, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x4c, 0x65, + 0x6e, 0x67, 0x74, 0x68, 0x73, 0x12, 0x5b, 0x0a, 0x0f, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, + 0x64, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, + 0x2e, 0x76, 0x70, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x74, 0x74, + 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x49, 0x50, 0x76, 0x36, + 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x49, 0x50, 0x76, 0x36, 0x52, 0x6f, 0x75, + 0x74, 0x65, 0x52, 0x0e, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x52, 0x6f, 0x75, 0x74, + 0x65, 0x73, 0x12, 0x5b, 0x0a, 0x0f, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x5f, 0x72, + 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x76, 0x70, + 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x49, 0x50, 0x76, 0x36, 0x53, 0x65, 0x74, + 0x74, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x49, 0x50, 0x76, 0x36, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, + 0x0e, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x1a, + 0x6a, 0x0a, 0x09, 0x49, 0x50, 0x76, 0x36, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x20, 0x0a, 0x0b, + 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x23, + 0x0a, 0x0d, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x4c, 0x65, 0x6e, + 0x67, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x22, 0x58, 0x0a, 0x17, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x49, 0x50, 0x76, 0x36, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, - 0x67, 0x73, 0x52, 0x0c, 0x69, 0x70, 0x76, 0x36, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, - 0x1a, 0xcb, 0x01, 0x0a, 0x0b, 0x44, 0x4e, 0x53, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, - 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x65, - 0x61, 0x72, 0x63, 0x68, 0x5f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x0d, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, - 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4e, 0x61, - 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x64, 0x6f, 0x6d, 0x61, - 0x69, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x6d, 0x61, 0x74, 0x63, 0x68, - 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x35, 0x0a, 0x17, 0x6d, 0x61, 0x74, 0x63, 0x68, - 0x5f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x5f, 0x6e, 0x6f, 0x5f, 0x73, 0x65, 0x61, 0x72, - 0x63, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x44, - 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x4e, 0x6f, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x1a, 0xf4, - 0x02, 0x0a, 0x0c, 0x49, 0x50, 0x76, 0x34, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, - 0x14, 0x0a, 0x05, 0x61, 0x64, 0x64, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, - 0x61, 0x64, 0x64, 0x72, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x5f, - 0x6d, 0x61, 0x73, 0x6b, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, - 0x6e, 0x65, 0x74, 0x4d, 0x61, 0x73, 0x6b, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, - 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, - 0x12, 0x5b, 0x0a, 0x0f, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x5f, 0x72, 0x6f, 0x75, - 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x76, 0x70, 0x6e, 0x2e, - 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x49, 0x50, 0x76, 0x34, 0x53, 0x65, 0x74, 0x74, 0x69, - 0x6e, 0x67, 0x73, 0x2e, 0x49, 0x50, 0x76, 0x34, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x0e, 0x69, - 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x5b, 0x0a, - 0x0f, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, - 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x76, 0x70, 0x6e, 0x2e, 0x4e, 0x65, 0x74, - 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x2e, 0x49, 0x50, 0x76, 0x34, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, - 0x2e, 0x49, 0x50, 0x76, 0x34, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x0e, 0x65, 0x78, 0x63, 0x6c, - 0x75, 0x64, 0x65, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x1a, 0x59, 0x0a, 0x09, 0x49, 0x50, - 0x76, 0x34, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, - 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, - 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x61, 0x73, - 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x61, 0x73, 0x6b, 0x12, 0x16, 0x0a, - 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, - 0x6f, 0x75, 0x74, 0x65, 0x72, 0x1a, 0xf1, 0x02, 0x0a, 0x0c, 0x49, 0x50, 0x76, 0x36, 0x53, 0x65, - 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x64, 0x64, 0x72, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x61, 0x64, 0x64, 0x72, 0x73, 0x12, 0x25, 0x0a, 0x0e, - 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x73, 0x18, 0x02, - 0x20, 0x03, 0x28, 0x0d, 0x52, 0x0d, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x4c, 0x65, 0x6e, 0x67, - 0x74, 0x68, 0x73, 0x12, 0x5b, 0x0a, 0x0f, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x5f, - 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x76, - 0x70, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, - 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x49, 0x50, 0x76, 0x36, 0x53, 0x65, - 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x49, 0x50, 0x76, 0x36, 0x52, 0x6f, 0x75, 0x74, 0x65, - 0x52, 0x0e, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, - 0x12, 0x5b, 0x0a, 0x0f, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x5f, 0x72, 0x6f, 0x75, - 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x76, 0x70, 0x6e, 0x2e, - 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x49, 0x50, 0x76, 0x36, 0x53, 0x65, 0x74, 0x74, 0x69, - 0x6e, 0x67, 0x73, 0x2e, 0x49, 0x50, 0x76, 0x36, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x0e, 0x65, - 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x1a, 0x6a, 0x0a, - 0x09, 0x49, 0x50, 0x76, 0x36, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, - 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x0d, - 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x4c, 0x65, 0x6e, 0x67, 0x74, - 0x68, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x22, 0x58, 0x0a, 0x17, 0x4e, 0x65, 0x74, - 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x23, - 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, - 0x61, 0x67, 0x65, 0x22, 0xd4, 0x02, 0x0a, 0x0c, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x16, 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x66, - 0x69, 0x6c, 0x65, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x14, 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x46, 0x69, 0x6c, 0x65, - 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, - 0x64, 0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, - 0x6f, 0x64, 0x65, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x61, 0x70, 0x69, 0x5f, 0x74, - 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x70, 0x69, 0x54, - 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x32, 0x0a, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, - 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x76, 0x70, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x72, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, - 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x65, 0x76, 0x69, - 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x76, - 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, - 0x6f, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, - 0x4f, 0x73, 0x12, 0x32, 0x0a, 0x15, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x5f, 0x64, 0x65, 0x73, 0x6b, - 0x74, 0x6f, 0x70, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x13, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x44, 0x65, 0x73, 0x6b, 0x74, 0x6f, 0x70, 0x56, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x32, 0x0a, 0x06, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, - 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x4e, 0x0a, 0x0d, 0x53, 0x74, - 0x61, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, + 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0xd4, 0x02, 0x0a, 0x0c, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x16, 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, + 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x14, 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x46, 0x69, + 0x6c, 0x65, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x12, 0x1b, 0x0a, 0x09, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x61, 0x70, 0x69, + 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x70, + 0x69, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x32, 0x0a, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, + 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x76, 0x70, 0x6e, 0x2e, 0x53, 0x74, + 0x61, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, + 0x72, 0x52, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x5f, 0x6f, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x4f, 0x73, 0x12, 0x32, 0x0a, 0x15, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x5f, 0x64, 0x65, + 0x73, 0x6b, 0x74, 0x6f, 0x70, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x13, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x44, 0x65, 0x73, 0x6b, 0x74, 0x6f, + 0x70, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x32, 0x0a, 0x06, 0x48, 0x65, 0x61, 0x64, + 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x4e, 0x0a, 0x0d, + 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, + 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, + 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, + 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x0d, 0x0a, 0x0b, + 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x4d, 0x0a, 0x0c, 0x53, + 0x74, 0x6f, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, - 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x0d, 0x0a, 0x0b, 0x53, 0x74, - 0x6f, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x4d, 0x0a, 0x0c, 0x53, 0x74, 0x6f, - 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, - 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, - 0x65, 0x73, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, - 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x0f, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xe4, 0x01, 0x0a, 0x06, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x6c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x76, 0x70, 0x6e, 0x2e, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x2e, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x52, 0x09, - 0x6c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x72, 0x72, - 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x30, - 0x0a, 0x0b, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x76, 0x70, 0x6e, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x22, 0x4e, 0x0a, 0x09, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x0b, 0x0a, - 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x54, - 0x41, 0x52, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x54, 0x41, 0x52, - 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x54, 0x4f, 0x50, 0x50, 0x49, 0x4e, - 0x47, 0x10, 0x03, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x54, 0x4f, 0x50, 0x50, 0x45, 0x44, 0x10, 0x04, - 0x42, 0x39, 0x5a, 0x1d, 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, 0x76, 0x70, - 0x6e, 0xaa, 0x02, 0x17, 0x43, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x44, 0x65, 0x73, 0x6b, 0x74, 0x6f, - 0x70, 0x2e, 0x56, 0x70, 0x6e, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x0f, 0x0a, 0x0d, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xe4, 0x01, 0x0a, 0x06, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x6c, 0x69, 0x66, 0x65, 0x63, 0x79, + 0x63, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x76, 0x70, 0x6e, 0x2e, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, + 0x52, 0x09, 0x6c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x12, 0x30, 0x0a, 0x0b, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x76, 0x70, 0x6e, 0x2e, 0x50, 0x65, 0x65, 0x72, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x22, 0x4e, 0x0a, 0x09, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, + 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, + 0x53, 0x54, 0x41, 0x52, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x54, + 0x41, 0x52, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x54, 0x4f, 0x50, 0x50, + 0x49, 0x4e, 0x47, 0x10, 0x03, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x54, 0x4f, 0x50, 0x50, 0x45, 0x44, + 0x10, 0x04, 0x42, 0x39, 0x5a, 0x1d, 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, + 0x76, 0x70, 0x6e, 0xaa, 0x02, 0x17, 0x43, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x44, 0x65, 0x73, 0x6b, + 0x74, 0x6f, 0x70, 0x2e, 0x56, 0x70, 0x6e, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2328,7 +2436,7 @@ func file_vpn_vpn_proto_rawDescGZIP() []byte { } var file_vpn_vpn_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_vpn_vpn_proto_msgTypes = make([]protoimpl.MessageInfo, 25) +var file_vpn_vpn_proto_msgTypes = make([]protoimpl.MessageInfo, 26) var file_vpn_vpn_proto_goTypes = []interface{}{ (Log_Level)(0), // 0: vpn.Log.Level (Workspace_Status)(0), // 1: vpn.Workspace.Status @@ -2343,66 +2451,71 @@ var file_vpn_vpn_proto_goTypes = []interface{}{ (*PeerUpdate)(nil), // 10: vpn.PeerUpdate (*Workspace)(nil), // 11: vpn.Workspace (*Agent)(nil), // 12: vpn.Agent - (*NetworkSettingsRequest)(nil), // 13: vpn.NetworkSettingsRequest - (*NetworkSettingsResponse)(nil), // 14: vpn.NetworkSettingsResponse - (*StartRequest)(nil), // 15: vpn.StartRequest - (*StartResponse)(nil), // 16: vpn.StartResponse - (*StopRequest)(nil), // 17: vpn.StopRequest - (*StopResponse)(nil), // 18: vpn.StopResponse - (*StatusRequest)(nil), // 19: vpn.StatusRequest - (*Status)(nil), // 20: vpn.Status - (*Log_Field)(nil), // 21: vpn.Log.Field - (*NetworkSettingsRequest_DNSSettings)(nil), // 22: vpn.NetworkSettingsRequest.DNSSettings - (*NetworkSettingsRequest_IPv4Settings)(nil), // 23: vpn.NetworkSettingsRequest.IPv4Settings - (*NetworkSettingsRequest_IPv6Settings)(nil), // 24: vpn.NetworkSettingsRequest.IPv6Settings - (*NetworkSettingsRequest_IPv4Settings_IPv4Route)(nil), // 25: vpn.NetworkSettingsRequest.IPv4Settings.IPv4Route - (*NetworkSettingsRequest_IPv6Settings_IPv6Route)(nil), // 26: vpn.NetworkSettingsRequest.IPv6Settings.IPv6Route - (*StartRequest_Header)(nil), // 27: vpn.StartRequest.Header - (*timestamppb.Timestamp)(nil), // 28: google.protobuf.Timestamp + (*LastPing)(nil), // 13: vpn.LastPing + (*NetworkSettingsRequest)(nil), // 14: vpn.NetworkSettingsRequest + (*NetworkSettingsResponse)(nil), // 15: vpn.NetworkSettingsResponse + (*StartRequest)(nil), // 16: vpn.StartRequest + (*StartResponse)(nil), // 17: vpn.StartResponse + (*StopRequest)(nil), // 18: vpn.StopRequest + (*StopResponse)(nil), // 19: vpn.StopResponse + (*StatusRequest)(nil), // 20: vpn.StatusRequest + (*Status)(nil), // 21: vpn.Status + (*Log_Field)(nil), // 22: vpn.Log.Field + (*NetworkSettingsRequest_DNSSettings)(nil), // 23: vpn.NetworkSettingsRequest.DNSSettings + (*NetworkSettingsRequest_IPv4Settings)(nil), // 24: vpn.NetworkSettingsRequest.IPv4Settings + (*NetworkSettingsRequest_IPv6Settings)(nil), // 25: vpn.NetworkSettingsRequest.IPv6Settings + (*NetworkSettingsRequest_IPv4Settings_IPv4Route)(nil), // 26: vpn.NetworkSettingsRequest.IPv4Settings.IPv4Route + (*NetworkSettingsRequest_IPv6Settings_IPv6Route)(nil), // 27: vpn.NetworkSettingsRequest.IPv6Settings.IPv6Route + (*StartRequest_Header)(nil), // 28: vpn.StartRequest.Header + (*timestamppb.Timestamp)(nil), // 29: google.protobuf.Timestamp + (*durationpb.Duration)(nil), // 30: google.protobuf.Duration } var file_vpn_vpn_proto_depIdxs = []int32{ 3, // 0: vpn.ManagerMessage.rpc:type_name -> vpn.RPC 9, // 1: vpn.ManagerMessage.get_peer_update:type_name -> vpn.GetPeerUpdate - 14, // 2: vpn.ManagerMessage.network_settings:type_name -> vpn.NetworkSettingsResponse - 15, // 3: vpn.ManagerMessage.start:type_name -> vpn.StartRequest - 17, // 4: vpn.ManagerMessage.stop:type_name -> vpn.StopRequest + 15, // 2: vpn.ManagerMessage.network_settings:type_name -> vpn.NetworkSettingsResponse + 16, // 3: vpn.ManagerMessage.start:type_name -> vpn.StartRequest + 18, // 4: vpn.ManagerMessage.stop:type_name -> vpn.StopRequest 3, // 5: vpn.TunnelMessage.rpc:type_name -> vpn.RPC 8, // 6: vpn.TunnelMessage.log:type_name -> vpn.Log 10, // 7: vpn.TunnelMessage.peer_update:type_name -> vpn.PeerUpdate - 13, // 8: vpn.TunnelMessage.network_settings:type_name -> vpn.NetworkSettingsRequest - 16, // 9: vpn.TunnelMessage.start:type_name -> vpn.StartResponse - 18, // 10: vpn.TunnelMessage.stop:type_name -> vpn.StopResponse + 14, // 8: vpn.TunnelMessage.network_settings:type_name -> vpn.NetworkSettingsRequest + 17, // 9: vpn.TunnelMessage.start:type_name -> vpn.StartResponse + 19, // 10: vpn.TunnelMessage.stop:type_name -> vpn.StopResponse 3, // 11: vpn.ClientMessage.rpc:type_name -> vpn.RPC - 15, // 12: vpn.ClientMessage.start:type_name -> vpn.StartRequest - 17, // 13: vpn.ClientMessage.stop:type_name -> vpn.StopRequest - 19, // 14: vpn.ClientMessage.status:type_name -> vpn.StatusRequest + 16, // 12: vpn.ClientMessage.start:type_name -> vpn.StartRequest + 18, // 13: vpn.ClientMessage.stop:type_name -> vpn.StopRequest + 20, // 14: vpn.ClientMessage.status:type_name -> vpn.StatusRequest 3, // 15: vpn.ServiceMessage.rpc:type_name -> vpn.RPC - 16, // 16: vpn.ServiceMessage.start:type_name -> vpn.StartResponse - 18, // 17: vpn.ServiceMessage.stop:type_name -> vpn.StopResponse - 20, // 18: vpn.ServiceMessage.status:type_name -> vpn.Status + 17, // 16: vpn.ServiceMessage.start:type_name -> vpn.StartResponse + 19, // 17: vpn.ServiceMessage.stop:type_name -> vpn.StopResponse + 21, // 18: vpn.ServiceMessage.status:type_name -> vpn.Status 0, // 19: vpn.Log.level:type_name -> vpn.Log.Level - 21, // 20: vpn.Log.fields:type_name -> vpn.Log.Field + 22, // 20: vpn.Log.fields:type_name -> vpn.Log.Field 11, // 21: vpn.PeerUpdate.upserted_workspaces:type_name -> vpn.Workspace 12, // 22: vpn.PeerUpdate.upserted_agents:type_name -> vpn.Agent 11, // 23: vpn.PeerUpdate.deleted_workspaces:type_name -> vpn.Workspace 12, // 24: vpn.PeerUpdate.deleted_agents:type_name -> vpn.Agent 1, // 25: vpn.Workspace.status:type_name -> vpn.Workspace.Status - 28, // 26: vpn.Agent.last_handshake:type_name -> google.protobuf.Timestamp - 22, // 27: vpn.NetworkSettingsRequest.dns_settings:type_name -> vpn.NetworkSettingsRequest.DNSSettings - 23, // 28: vpn.NetworkSettingsRequest.ipv4_settings:type_name -> vpn.NetworkSettingsRequest.IPv4Settings - 24, // 29: vpn.NetworkSettingsRequest.ipv6_settings:type_name -> vpn.NetworkSettingsRequest.IPv6Settings - 27, // 30: vpn.StartRequest.headers:type_name -> vpn.StartRequest.Header - 2, // 31: vpn.Status.lifecycle:type_name -> vpn.Status.Lifecycle - 10, // 32: vpn.Status.peer_update:type_name -> vpn.PeerUpdate - 25, // 33: vpn.NetworkSettingsRequest.IPv4Settings.included_routes:type_name -> vpn.NetworkSettingsRequest.IPv4Settings.IPv4Route - 25, // 34: vpn.NetworkSettingsRequest.IPv4Settings.excluded_routes:type_name -> vpn.NetworkSettingsRequest.IPv4Settings.IPv4Route - 26, // 35: vpn.NetworkSettingsRequest.IPv6Settings.included_routes:type_name -> vpn.NetworkSettingsRequest.IPv6Settings.IPv6Route - 26, // 36: vpn.NetworkSettingsRequest.IPv6Settings.excluded_routes:type_name -> vpn.NetworkSettingsRequest.IPv6Settings.IPv6Route - 37, // [37:37] is the sub-list for method output_type - 37, // [37:37] is the sub-list for method input_type - 37, // [37:37] is the sub-list for extension type_name - 37, // [37:37] is the sub-list for extension extendee - 0, // [0:37] is the sub-list for field type_name + 29, // 26: vpn.Agent.last_handshake:type_name -> google.protobuf.Timestamp + 13, // 27: vpn.Agent.last_ping:type_name -> vpn.LastPing + 30, // 28: vpn.LastPing.latency:type_name -> google.protobuf.Duration + 30, // 29: vpn.LastPing.preferred_derp_latency:type_name -> google.protobuf.Duration + 23, // 30: vpn.NetworkSettingsRequest.dns_settings:type_name -> vpn.NetworkSettingsRequest.DNSSettings + 24, // 31: vpn.NetworkSettingsRequest.ipv4_settings:type_name -> vpn.NetworkSettingsRequest.IPv4Settings + 25, // 32: vpn.NetworkSettingsRequest.ipv6_settings:type_name -> vpn.NetworkSettingsRequest.IPv6Settings + 28, // 33: vpn.StartRequest.headers:type_name -> vpn.StartRequest.Header + 2, // 34: vpn.Status.lifecycle:type_name -> vpn.Status.Lifecycle + 10, // 35: vpn.Status.peer_update:type_name -> vpn.PeerUpdate + 26, // 36: vpn.NetworkSettingsRequest.IPv4Settings.included_routes:type_name -> vpn.NetworkSettingsRequest.IPv4Settings.IPv4Route + 26, // 37: vpn.NetworkSettingsRequest.IPv4Settings.excluded_routes:type_name -> vpn.NetworkSettingsRequest.IPv4Settings.IPv4Route + 27, // 38: vpn.NetworkSettingsRequest.IPv6Settings.included_routes:type_name -> vpn.NetworkSettingsRequest.IPv6Settings.IPv6Route + 27, // 39: vpn.NetworkSettingsRequest.IPv6Settings.excluded_routes:type_name -> vpn.NetworkSettingsRequest.IPv6Settings.IPv6Route + 40, // [40:40] is the sub-list for method output_type + 40, // [40:40] is the sub-list for method input_type + 40, // [40:40] is the sub-list for extension type_name + 40, // [40:40] is the sub-list for extension extendee + 0, // [0:40] is the sub-list for field type_name } func init() { file_vpn_vpn_proto_init() } @@ -2532,7 +2645,7 @@ func file_vpn_vpn_proto_init() { } } file_vpn_vpn_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NetworkSettingsRequest); i { + switch v := v.(*LastPing); i { case 0: return &v.state case 1: @@ -2544,7 +2657,7 @@ func file_vpn_vpn_proto_init() { } } file_vpn_vpn_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NetworkSettingsResponse); i { + switch v := v.(*NetworkSettingsRequest); i { case 0: return &v.state case 1: @@ -2556,7 +2669,7 @@ func file_vpn_vpn_proto_init() { } } file_vpn_vpn_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*StartRequest); i { + switch v := v.(*NetworkSettingsResponse); i { case 0: return &v.state case 1: @@ -2568,7 +2681,7 @@ func file_vpn_vpn_proto_init() { } } file_vpn_vpn_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*StartResponse); i { + switch v := v.(*StartRequest); i { case 0: return &v.state case 1: @@ -2580,7 +2693,7 @@ func file_vpn_vpn_proto_init() { } } file_vpn_vpn_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*StopRequest); i { + switch v := v.(*StartResponse); i { case 0: return &v.state case 1: @@ -2592,7 +2705,7 @@ func file_vpn_vpn_proto_init() { } } file_vpn_vpn_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*StopResponse); i { + switch v := v.(*StopRequest); i { case 0: return &v.state case 1: @@ -2604,7 +2717,7 @@ func file_vpn_vpn_proto_init() { } } file_vpn_vpn_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*StatusRequest); i { + switch v := v.(*StopResponse); i { case 0: return &v.state case 1: @@ -2616,7 +2729,7 @@ func file_vpn_vpn_proto_init() { } } file_vpn_vpn_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Status); i { + switch v := v.(*StatusRequest); i { case 0: return &v.state case 1: @@ -2628,7 +2741,7 @@ func file_vpn_vpn_proto_init() { } } file_vpn_vpn_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Log_Field); i { + switch v := v.(*Status); i { case 0: return &v.state case 1: @@ -2640,7 +2753,7 @@ func file_vpn_vpn_proto_init() { } } file_vpn_vpn_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NetworkSettingsRequest_DNSSettings); i { + switch v := v.(*Log_Field); i { case 0: return &v.state case 1: @@ -2652,7 +2765,7 @@ func file_vpn_vpn_proto_init() { } } file_vpn_vpn_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NetworkSettingsRequest_IPv4Settings); i { + switch v := v.(*NetworkSettingsRequest_DNSSettings); i { case 0: return &v.state case 1: @@ -2664,7 +2777,7 @@ func file_vpn_vpn_proto_init() { } } file_vpn_vpn_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NetworkSettingsRequest_IPv6Settings); i { + switch v := v.(*NetworkSettingsRequest_IPv4Settings); i { case 0: return &v.state case 1: @@ -2676,7 +2789,7 @@ func file_vpn_vpn_proto_init() { } } file_vpn_vpn_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NetworkSettingsRequest_IPv4Settings_IPv4Route); i { + switch v := v.(*NetworkSettingsRequest_IPv6Settings); i { case 0: return &v.state case 1: @@ -2688,7 +2801,7 @@ func file_vpn_vpn_proto_init() { } } file_vpn_vpn_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NetworkSettingsRequest_IPv6Settings_IPv6Route); i { + switch v := v.(*NetworkSettingsRequest_IPv4Settings_IPv4Route); i { case 0: return &v.state case 1: @@ -2700,6 +2813,18 @@ func file_vpn_vpn_proto_init() { } } file_vpn_vpn_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*NetworkSettingsRequest_IPv6Settings_IPv6Route); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_vpn_vpn_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*StartRequest_Header); i { case 0: return &v.state @@ -2735,13 +2860,15 @@ func file_vpn_vpn_proto_init() { (*ServiceMessage_Stop)(nil), (*ServiceMessage_Status)(nil), } + file_vpn_vpn_proto_msgTypes[9].OneofWrappers = []interface{}{} + file_vpn_vpn_proto_msgTypes[10].OneofWrappers = []interface{}{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_vpn_vpn_proto_rawDesc, NumEnums: 3, - NumMessages: 25, + NumMessages: 26, NumExtensions: 0, NumServices: 0, }, diff --git a/vpn/vpn.proto b/vpn/vpn.proto index 963098c60a648..44383fa80e0cb 100644 --- a/vpn/vpn.proto +++ b/vpn/vpn.proto @@ -3,6 +3,7 @@ option go_package = "github.com/coder/coder/v2/vpn"; option csharp_namespace = "Coder.Desktop.Vpn.Proto"; import "google/protobuf/timestamp.proto"; +import "google/protobuf/duration.proto"; package vpn; @@ -130,6 +131,21 @@ message Agent { // last_handshake is the primary indicator of whether we are connected to a peer. Zero value or // anything longer than 5 minutes ago means there is a problem. google.protobuf.Timestamp last_handshake = 6; + // If unset, a successful ping has not yet been made. + optional LastPing last_ping = 7; +} + +message LastPing { + // latency is the RTT of the ping to the agent. + google.protobuf.Duration latency = 1; + // did_p2p indicates whether the ping was sent P2P, or over DERP. + bool did_p2p = 2; + // preferred_derp is the human readable name of the preferred DERP region, + // or the region used for the last ping, if it was sent over DERP. + string preferred_derp = 3; + // preferred_derp_latency is the last known latency to the preferred DERP + // region. Unset if the region does not appear in the DERP map. + optional google.protobuf.Duration preferred_derp_latency = 4; } // NetworkSettingsRequest is based on