chore(vpn): send ping results over tunnel (#18200)

Closes #17982.

The purpose of this PR is to expose network latency via the API used by Coder Desktop.

This PR has the tunnel ping all known agents every 5 seconds, in order to produce an instance of:
```proto
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;
}
```
The contents of this message are stored and included on all subsequent upsertions of the agent. 
Note that we upsert existing agents every 5 seconds to update the `last_handshake` value.

On the desktop apps, this message will be used to produce a tooltip similar to that of the VS Code extension:
<img width="495" alt="image" src="https://github.com/user-attachments/assets/d8b65f3d-f536-4c64-9af9-35c1a42c92d2" />
(wording not final)

Unlike the VS Code extension, we omit:
- The Latency of *all* available DERP regions. It seems not ideal to send a copy of this entire map for every online agent, and it certainly doesn't make sense for it to be on the `Agent` or `LastPing` message. 
If we do want to expose this info on Coder Desktop, we should consider how best to do so; maybe we want to include it on a more generic `Netcheck` message.
- The current throughput (Bytes up/down). This is out of scope of the linked issue, and is non-trivial to implement. I'm also not sure of the value given the frequency we're doing these status updates (every 5 seconds).
If we want to expose it, it'll be in a separate PR.

<img width="343" alt="image" src="https://github.com/user-attachments/assets/8447d03b-9721-4111-8ac1-332d70a1e8f1" />
This commit is contained in:
Ethan
2025-06-06 14:18:57 +10:00
committed by GitHub
parent b4f71b70aa
commit 0076e8479f
12 changed files with 973 additions and 318 deletions

View File

@ -10,6 +10,7 @@ import (
"testing"
"github.com/stretchr/testify/require"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
"github.com/coder/coder/v2/tailnet"
@ -162,3 +163,111 @@ func TestNewDERPMap(t *testing.T) {
require.ErrorContains(t, err, "DERP map has no DERP nodes")
})
}
func TestExtractDERPLatency(t *testing.T) {
t.Parallel()
derpMap := &tailcfg.DERPMap{
Regions: map[int]*tailcfg.DERPRegion{
1: {
RegionID: 1,
RegionName: "Region One",
Nodes: []*tailcfg.DERPNode{
{Name: "node1", RegionID: 1},
},
},
2: {
RegionID: 2,
RegionName: "Region Two",
Nodes: []*tailcfg.DERPNode{
{Name: "node2", RegionID: 2},
},
},
},
}
t.Run("Basic", func(t *testing.T) {
t.Parallel()
node := &tailnet.Node{
DERPLatency: map[string]float64{
"1-node1": 0.05,
"2-node2": 0.1,
},
}
latencyMs := tailnet.ExtractDERPLatency(node, derpMap)
require.EqualValues(t, 50, latencyMs["Region One"].Milliseconds())
require.EqualValues(t, 100, latencyMs["Region Two"].Milliseconds())
require.Len(t, latencyMs, 2)
})
t.Run("UnknownRegion", func(t *testing.T) {
t.Parallel()
node := &tailnet.Node{
DERPLatency: map[string]float64{
"999-node999": 0.2,
},
}
latencyMs := tailnet.ExtractDERPLatency(node, derpMap)
require.EqualValues(t, 200, latencyMs["Unnamed 999"].Milliseconds())
require.Len(t, latencyMs, 1)
})
t.Run("InvalidRegionFormat", func(t *testing.T) {
t.Parallel()
node := &tailnet.Node{
DERPLatency: map[string]float64{
"invalid": 0.3,
"1-node1": 0.05,
"abc-node": 0.15,
},
}
latencyMs := tailnet.ExtractDERPLatency(node, derpMap)
require.EqualValues(t, 50, latencyMs["Region One"].Milliseconds())
require.Len(t, latencyMs, 1)
require.NotContains(t, latencyMs, "invalid")
require.NotContains(t, latencyMs, "abc-node")
})
t.Run("EmptyInput", func(t *testing.T) {
t.Parallel()
node := &tailnet.Node{
DERPLatency: map[string]float64{},
}
latencyMs := tailnet.ExtractDERPLatency(node, derpMap)
require.Empty(t, latencyMs)
})
}
func TestExtractPreferredDERPName(t *testing.T) {
t.Parallel()
derpMap := &tailcfg.DERPMap{
Regions: map[int]*tailcfg.DERPRegion{
1: {RegionName: "New York"},
2: {RegionName: "London"},
},
}
t.Run("UsesPingRegion", func(t *testing.T) {
t.Parallel()
pingResult := &ipnstate.PingResult{DERPRegionID: 2}
node := &tailnet.Node{PreferredDERP: 1}
result := tailnet.ExtractPreferredDERPName(pingResult, node, derpMap)
require.Equal(t, "London", result)
})
t.Run("UsesNodePreferred", func(t *testing.T) {
t.Parallel()
pingResult := &ipnstate.PingResult{DERPRegionID: 0}
node := &tailnet.Node{PreferredDERP: 1}
result := tailnet.ExtractPreferredDERPName(pingResult, node, derpMap)
require.Equal(t, "New York", result)
})
t.Run("UnknownRegion", func(t *testing.T) {
t.Parallel()
pingResult := &ipnstate.PingResult{DERPRegionID: 99}
node := &tailnet.Node{PreferredDERP: 1}
result := tailnet.ExtractPreferredDERPName(pingResult, node, derpMap)
require.Equal(t, "Unnamed 99", result)
})
}