From 72db5672eed19cabfa91e45c9ba91489594790fc Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Tue, 3 Jun 2025 16:22:39 +1000 Subject: [PATCH 01/13] chore(vpn): send ping results over tunnel --- cli/ssh.go | 45 +-- coderd/database/db2sdk/db2sdk.go | 8 - coderd/util/maps/maps.go | 8 + tailnet/derpmap.go | 46 +++ tailnet/derpmap_test.go | 74 ++++ vpn/client.go | 10 + vpn/speaker_internal_test.go | 8 +- vpn/tunnel.go | 117 +++++- vpn/tunnel_internal_test.go | 80 +++- vpn/version.go | 9 +- vpn/vpn.pb.go | 615 +++++++++++++++++++------------ vpn/vpn.proto | 16 + 12 files changed, 726 insertions(+), 310 deletions(-) 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..88786bcec8472 100644 --- a/tailnet/derpmap_test.go +++ b/tailnet/derpmap_test.go @@ -162,3 +162,77 @@ 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) + }) +} 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..62b8585112aea 100644 --- a/vpn/speaker_internal_test.go +++ b/vpn/speaker_internal_test.go @@ -48,7 +48,7 @@ func TestSpeaker_RawPeer(t *testing.T) { errCh <- err }() - expectedHandshake := "codervpn tunnel 1.1\n" + expectedHandshake := "codervpn tunnel 1.2\n" b := make([]byte, 256) n, err := mp.Read(b) @@ -157,7 +157,7 @@ func TestSpeaker_OversizeHandshake(t *testing.T) { errCh <- err }() - expectedHandshake := "codervpn tunnel 1.1\n" + expectedHandshake := "codervpn tunnel 1.2\n" b := make([]byte, 256) n, err := mp.Read(b) @@ -210,7 +210,7 @@ func TestSpeaker_HandshakeInvalid(t *testing.T) { _, err = mp.Write([]byte(tc.handshake)) require.NoError(t, err) - expectedHandshake := "codervpn tunnel 1.1\n" + expectedHandshake := "codervpn tunnel 1.2\n" b := make([]byte, 256) n, err := mp.Read(b) require.NoError(t, err) @@ -248,7 +248,7 @@ func TestSpeaker_CorruptMessage(t *testing.T) { errCh <- err }() - expectedHandshake := "codervpn tunnel 1.1\n" + expectedHandshake := "codervpn tunnel 1.2\n" b := make([]byte, 256) n, err := mp.Read(b) diff --git a/vpn/tunnel.go b/vpn/tunnel.go index 6c71aecaa0965..7529e89cd134f 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) @@ -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) } @@ -559,16 +608,64 @@ func (u *updater) netStatusLoop() { return case <-ticker.C: u.sendAgentUpdate() + u.recordLatency() } } } +func (u *updater) recordLatency() { + var agentsIDsToPing []uuid.UUID + u.mu.Lock() + for _, agent := range u.agents { + agentsIDsToPing = append(agentsIDsToPing, agent.ID) + } + u.mu.Unlock() + + node := u.conn.Node() + derpMap := u.conn.DERPMap() + derpLatencies := tailnet.ExtractDERPLatency(node, derpMap) + + var wg sync.WaitGroup + for _, agentID := range agentsIDsToPing { + wg.Add(1) + go func() { + defer wg.Done() + pingDur, didP2p, pingResult, err := u.conn.Ping(u.ctx, agentID) + if err != nil { + u.logger.Warn(u.ctx, "failed to ping agent", slog.F("agent_id", agentID), slog.Error(err)) + return + } + 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)) + } + 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..7dcca9de2f9ef 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" @@ -64,8 +67,35 @@ type fakeConn 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 (*fakeConn) Ping(ctx context.Context, agentID uuid.UUID) (time.Duration, bool, *ipnstate.PingResult, error) { + return time.Millisecond * 100, true, &ipnstate.PingResult{ + DERPRegionID: 999, + }, nil +} + func (f *fakeConn) CurrentWorkspaceState() (tailnet.WorkspaceUpdate, error) { return f.state, nil } @@ -292,7 +322,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 +460,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 + require.Eventually(t, func() 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.WaitShort, testutil.IntervalFast) + // Upsert a new agent err = tun.Update(tailnet.WorkspaceUpdate{ UpsertedWorkspaces: []*tailnet.Workspace{}, @@ -459,6 +505,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 +536,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 + require.Eventually(t, func() 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.WaitShort, testutil.IntervalFast) } func TestTunnel_sendAgentUpdateReconnect(t *testing.T) { @@ -902,11 +968,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 From 91f3ba0bae1dc6b0fdb27d0747fe104817af5082 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Tue, 3 Jun 2025 16:32:57 +1000 Subject: [PATCH 02/13] fixup --- vpn/tunnel.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/vpn/tunnel.go b/vpn/tunnel.go index 7529e89cd134f..058cf5db8209d 100644 --- a/vpn/tunnel.go +++ b/vpn/tunnel.go @@ -621,10 +621,6 @@ func (u *updater) recordLatency() { } u.mu.Unlock() - node := u.conn.Node() - derpMap := u.conn.DERPMap() - derpLatencies := tailnet.ExtractDERPLatency(node, derpMap) - var wg sync.WaitGroup for _, agentID := range agentsIDsToPing { wg.Add(1) @@ -635,6 +631,9 @@ func (u *updater) recordLatency() { u.logger.Warn(u.ctx, "failed to ping agent", slog.F("agent_id", agentID), slog.Error(err)) return } + node := u.conn.Node() + derpMap := u.conn.DERPMap() + derpLatencies := tailnet.ExtractDERPLatency(node, derpMap) preferredDerp := tailnet.ExtractPreferredDERPName(pingResult, node, derpMap) var preferredDerpLatency *time.Duration if derpLatency, ok := derpLatencies[preferredDerp]; ok { From 896548de057a81564ec31f9739916ca674630536 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Tue, 3 Jun 2025 10:32:46 +0000 Subject: [PATCH 03/13] add pingctx --- vpn/tunnel.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vpn/tunnel.go b/vpn/tunnel.go index 058cf5db8209d..f3ab8a1c8ef06 100644 --- a/vpn/tunnel.go +++ b/vpn/tunnel.go @@ -621,12 +621,14 @@ func (u *updater) recordLatency() { } u.mu.Unlock() + pingCtx, cancelFunc := context.WithTimeout(u.ctx, 5*time.Second) + defer cancelFunc() var wg sync.WaitGroup for _, agentID := range agentsIDsToPing { wg.Add(1) go func() { defer wg.Done() - pingDur, didP2p, pingResult, err := u.conn.Ping(u.ctx, agentID) + pingDur, didP2p, pingResult, err := u.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 From 176a375365de2a8b0735fc1bd64648cb26abbf10 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Tue, 3 Jun 2025 10:52:32 +0000 Subject: [PATCH 04/13] review --- tailnet/derpmap_test.go | 35 +++++++++++++++++++++++++++++++++++ vpn/tunnel_internal_test.go | 8 ++++---- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/tailnet/derpmap_test.go b/tailnet/derpmap_test.go index 88786bcec8472..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" @@ -236,3 +237,37 @@ func TestExtractDERPLatency(t *testing.T) { 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/tunnel_internal_test.go b/vpn/tunnel_internal_test.go index 7dcca9de2f9ef..c481093199171 100644 --- a/vpn/tunnel_internal_test.go +++ b/vpn/tunnel_internal_test.go @@ -461,7 +461,7 @@ func TestTunnel_sendAgentUpdate(t *testing.T) { } // Latency is gathered in the background, so it'll eventually be sent - require.Eventually(t, func() bool { + 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 { @@ -474,7 +474,7 @@ func TestTunnel_sendAgentUpdate(t *testing.T) { return false } return req.msg.GetPeerUpdate().UpsertedAgents[0].LastPing.PreferredDerp == "Coder Region" - }, testutil.WaitShort, testutil.IntervalFast) + }, testutil.IntervalFast) // Upsert a new agent err = tun.Update(tailnet.WorkspaceUpdate{ @@ -538,7 +538,7 @@ func TestTunnel_sendAgentUpdate(t *testing.T) { require.Equal(t, hsTime, req.msg.GetPeerUpdate().UpsertedAgents[0].LastHandshake.AsTime()) // Eventually the second agent's latency is set - require.Eventually(t, func() bool { + 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 { @@ -551,7 +551,7 @@ func TestTunnel_sendAgentUpdate(t *testing.T) { return false } return req.msg.GetPeerUpdate().UpsertedAgents[0].LastPing.PreferredDerp == "Coder Region" - }, testutil.WaitShort, testutil.IntervalFast) + }, testutil.IntervalFast) } func TestTunnel_sendAgentUpdateReconnect(t *testing.T) { From f30bd122c1dd0c9baafae692473958e6a3b31689 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Wed, 4 Jun 2025 09:59:01 +0000 Subject: [PATCH 05/13] forgot to goroutine --- vpn/tunnel.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vpn/tunnel.go b/vpn/tunnel.go index f3ab8a1c8ef06..1a93df73daeb6 100644 --- a/vpn/tunnel.go +++ b/vpn/tunnel.go @@ -607,8 +607,8 @@ func (u *updater) netStatusLoop() { case <-u.ctx.Done(): return case <-ticker.C: + go u.recordLatency() u.sendAgentUpdate() - u.recordLatency() } } } From a472f5656e3c7a8500aa215dac08a76e68f859c9 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Wed, 4 Jun 2025 10:02:34 +0000 Subject: [PATCH 06/13] remove wg --- vpn/tunnel.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/vpn/tunnel.go b/vpn/tunnel.go index 1a93df73daeb6..fafd7fb0ccac2 100644 --- a/vpn/tunnel.go +++ b/vpn/tunnel.go @@ -607,7 +607,7 @@ func (u *updater) netStatusLoop() { case <-u.ctx.Done(): return case <-ticker.C: - go u.recordLatency() + u.recordLatency() u.sendAgentUpdate() } } @@ -623,11 +623,8 @@ func (u *updater) recordLatency() { pingCtx, cancelFunc := context.WithTimeout(u.ctx, 5*time.Second) defer cancelFunc() - var wg sync.WaitGroup for _, agentID := range agentsIDsToPing { - wg.Add(1) go func() { - defer wg.Done() pingDur, didP2p, pingResult, err := u.conn.Ping(pingCtx, agentID) if err != nil { u.logger.Warn(u.ctx, "failed to ping agent", slog.F("agent_id", agentID), slog.Error(err)) @@ -658,7 +655,6 @@ func (u *updater) recordLatency() { } }() } - wg.Wait() } // processSnapshotUpdate handles the logic when a full state update is received. From 91f80096c0a3e29a53b5e5ae57f3ba46ffc6f12d Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Wed, 4 Jun 2025 10:15:40 +0000 Subject: [PATCH 07/13] lock for conn read --- vpn/tunnel.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/vpn/tunnel.go b/vpn/tunnel.go index fafd7fb0ccac2..1dab543dc482a 100644 --- a/vpn/tunnel.go +++ b/vpn/tunnel.go @@ -563,8 +563,8 @@ func (u *updater) stop() error { return nil } err := u.conn.Close() - u.conn = nil u.cancel() + u.conn = nil return err } @@ -630,6 +630,13 @@ func (u *updater) recordLatency() { u.logger.Warn(u.ctx, "failed to ping agent", slog.F("agent_id", agentID), slog.Error(err)) return } + + u.mu.Lock() + defer u.mu.Unlock() + if u.conn == nil { + u.logger.Debug(u.ctx, "ignoring ping result as connection is closed", slog.F("agent_id", agentID)) + return + } node := u.conn.Node() derpMap := u.conn.DERPMap() derpLatencies := tailnet.ExtractDERPLatency(node, derpMap) @@ -640,8 +647,6 @@ func (u *updater) recordLatency() { } else { u.logger.Debug(u.ctx, "preferred DERP not found in DERP latency map", slog.F("preferred_derp", preferredDerp)) } - u.mu.Lock() - defer u.mu.Unlock() if agent, ok := u.agents[agentID]; ok { agent.lastPing = &lastPing{ pingDur: pingDur, From 90f619d1f4845a83ced69d70536e0e0b066d1d8b Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Wed, 4 Jun 2025 11:03:38 +0000 Subject: [PATCH 08/13] fixup --- vpn/tunnel.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vpn/tunnel.go b/vpn/tunnel.go index 1dab543dc482a..f8c8c45649c6b 100644 --- a/vpn/tunnel.go +++ b/vpn/tunnel.go @@ -607,13 +607,13 @@ func (u *updater) netStatusLoop() { case <-u.ctx.Done(): return case <-ticker.C: - u.recordLatency() + u.recordLatencies() u.sendAgentUpdate() } } } -func (u *updater) recordLatency() { +func (u *updater) recordLatencies() { var agentsIDsToPing []uuid.UUID u.mu.Lock() for _, agent := range u.agents { @@ -621,10 +621,10 @@ func (u *updater) recordLatency() { } u.mu.Unlock() - pingCtx, cancelFunc := context.WithTimeout(u.ctx, 5*time.Second) - defer cancelFunc() for _, agentID := range agentsIDsToPing { go func() { + pingCtx, cancelFunc := context.WithTimeout(u.ctx, 5*time.Second) + defer cancelFunc() pingDur, didP2p, pingResult, err := u.conn.Ping(pingCtx, agentID) if err != nil { u.logger.Warn(u.ctx, "failed to ping agent", slog.F("agent_id", agentID), slog.Error(err)) From 6b5ea629603abbb745a58b3506cb2f30854a9e7b Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Thu, 5 Jun 2025 03:05:06 +0000 Subject: [PATCH 09/13] actually fixed with extra tests --- vpn/tunnel.go | 88 ++++++++------- vpn/tunnel_internal_test.go | 206 ++++++++++++++++++++++++++++++++++-- 2 files changed, 249 insertions(+), 45 deletions(-) diff --git a/vpn/tunnel.go b/vpn/tunnel.go index f8c8c45649c6b..d10eb84358169 100644 --- a/vpn/tunnel.go +++ b/vpn/tunnel.go @@ -619,47 +619,61 @@ func (u *updater) recordLatencies() { for _, agent := range u.agents { agentsIDsToPing = append(agentsIDsToPing, agent.ID) } + conn := u.conn u.mu.Unlock() - for _, agentID := range agentsIDsToPing { - go func() { - pingCtx, cancelFunc := context.WithTimeout(u.ctx, 5*time.Second) - defer cancelFunc() - pingDur, didP2p, pingResult, err := u.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 - } + if conn == nil { + u.logger.Debug(u.ctx, "skipping pings as tunnel is not connected") + return + } - u.mu.Lock() - defer u.mu.Unlock() - if u.conn == nil { - u.logger.Debug(u.ctx, "ignoring ping result as connection is closed", slog.F("agent_id", agentID)) - return - } - node := u.conn.Node() - derpMap := u.conn.DERPMap() - 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)) - } - if agent, ok := u.agents[agentID]; ok { - agent.lastPing = &lastPing{ - pingDur: pingDur, - didP2p: didP2p, - preferredDerp: preferredDerp, - preferredDerpLatency: preferredDerpLatency, + 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, 5*time.Second) + 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 } - u.agents[agentID] = agent - } else { - u.logger.Debug(u.ctx, "ignoring ping result for unknown agent", slog.F("agent_id", agentID)) - } - }() - } + + // We fetch the Node and DERPMap after each ping, as it may have + // changed. + node := conn.Node() + derpMap := conn.DERPMap() + 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. diff --git a/vpn/tunnel_internal_test.go b/vpn/tunnel_internal_test.go index c481093199171..e56bc4a3340f3 100644 --- a/vpn/tunnel_internal_test.go +++ b/vpn/tunnel_internal_test.go @@ -60,11 +60,17 @@ 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 { @@ -90,10 +96,22 @@ func (*fakeConn) Node() *tailnet.Node { var _ Conn = (*fakeConn)(nil) -func (*fakeConn) Ping(ctx context.Context, agentID uuid.UUID) (time.Duration, bool, *ipnstate.PingResult, error) { - return time.Millisecond * 100, true, &ipnstate.PingResult{ - DERPRegionID: 999, - }, 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) { @@ -759,6 +777,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() From 50d3ef5b08264ce55f5301ef53c35ad50d29927d Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Thu, 5 Jun 2025 03:06:26 +0000 Subject: [PATCH 10/13] const version string --- vpn/speaker_internal_test.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/vpn/speaker_internal_test.go b/vpn/speaker_internal_test.go index 62b8585112aea..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.2\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.2\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.2\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.2\n" - b := make([]byte, 256) n, err := mp.Read(b) require.NoError(t, err) From 1ac2328053f7f21c4af75f69acded6a1b01e2d1c Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Thu, 5 Jun 2025 03:08:44 +0000 Subject: [PATCH 11/13] lint --- vpn/tunnel_internal_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/vpn/tunnel_internal_test.go b/vpn/tunnel_internal_test.go index e56bc4a3340f3..5c4e6ec03d47f 100644 --- a/vpn/tunnel_internal_test.go +++ b/vpn/tunnel_internal_test.go @@ -111,7 +111,6 @@ func (f *fakeConn) Ping(ctx context.Context, agentID uuid.UUID) (time.Duration, DERPRegionID: 999, }, nil } - } func (f *fakeConn) CurrentWorkspaceState() (tailnet.WorkspaceUpdate, error) { From 3f49238f41ded5099023d335fec19ee81751c699 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Thu, 5 Jun 2025 04:39:03 +0000 Subject: [PATCH 12/13] potential nil deref --- vpn/tunnel.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vpn/tunnel.go b/vpn/tunnel.go index d10eb84358169..53f0301a5809b 100644 --- a/vpn/tunnel.go +++ b/vpn/tunnel.go @@ -647,6 +647,10 @@ func (u *updater) recordLatencies() { // 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 From 208fcc224180228318d355d00328a1a04c4c5be7 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Fri, 6 Jun 2025 02:25:33 +0000 Subject: [PATCH 13/13] review --- vpn/tunnel.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vpn/tunnel.go b/vpn/tunnel.go index 53f0301a5809b..aa1d0e32ef5b9 100644 --- a/vpn/tunnel.go +++ b/vpn/tunnel.go @@ -630,7 +630,7 @@ func (u *updater) recordLatencies() { 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, 5*time.Second) + pingCtx, cancelFunc := context.WithTimeout(u.ctx, netStatusInterval) defer cancelFunc() for _, agentID := range agentsIDsToPing { wg.Add(1) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy