mirror of
https://github.com/coder/coder.git
synced 2025-07-12 00:14:10 +00:00
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" />
153 lines
4.6 KiB
Go
153 lines
4.6 KiB
Go
package vpn
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"golang.org/x/xerrors"
|
|
)
|
|
|
|
// CurrentSupportedVersions is the list of versions supported by this
|
|
// implementation of the VPN RPC protocol.
|
|
var CurrentSupportedVersions = RPCVersionList{
|
|
Versions: []RPCVersion{
|
|
// 1.1 adds telemetry fields to StartRequest:
|
|
// - device_id: Coder Desktop device ID
|
|
// - device_os: Coder Desktop OS information
|
|
// - coder_desktop_version: Coder Desktop version
|
|
// 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},
|
|
},
|
|
}
|
|
|
|
// RPCVersion represents a single version of the RPC protocol. Any given version
|
|
// is expected to be backwards compatible with all previous minor versions on
|
|
// the same major version.
|
|
//
|
|
// e.g. RPCVersion{2, 3} is backwards compatible with RPCVersion{2, 2} but is
|
|
// not backwards compatible with RPCVersion{1, 2}.
|
|
type RPCVersion struct {
|
|
Major uint64 `json:"major"`
|
|
Minor uint64 `json:"minor"`
|
|
}
|
|
|
|
// ParseRPCVersion parses a version string in the format "major.minor" into a
|
|
// RPCVersion.
|
|
func ParseRPCVersion(str string) (RPCVersion, error) {
|
|
split := strings.Split(str, ".")
|
|
if len(split) != 2 {
|
|
return RPCVersion{}, xerrors.Errorf("invalid version string: %s", str)
|
|
}
|
|
major, err := strconv.ParseUint(split[0], 10, 64)
|
|
if err != nil {
|
|
return RPCVersion{}, xerrors.Errorf("invalid version string: %s", str)
|
|
}
|
|
if major == 0 {
|
|
return RPCVersion{}, xerrors.Errorf("invalid version string: %s", str)
|
|
}
|
|
minor, err := strconv.ParseUint(split[1], 10, 64)
|
|
if err != nil {
|
|
return RPCVersion{}, xerrors.Errorf("invalid version string: %s", str)
|
|
}
|
|
return RPCVersion{Major: major, Minor: minor}, nil
|
|
}
|
|
|
|
func (v RPCVersion) String() string {
|
|
return fmt.Sprintf("%d.%d", v.Major, v.Minor)
|
|
}
|
|
|
|
// IsCompatibleWith returns the lowest version that is compatible with both
|
|
// versions. If the versions are not compatible, the second return value will be
|
|
// false.
|
|
func (v RPCVersion) IsCompatibleWith(other RPCVersion) (RPCVersion, bool) {
|
|
if v.Major != other.Major {
|
|
return RPCVersion{}, false
|
|
}
|
|
// The lowest minor version from the two versions should be returned.
|
|
if v.Minor < other.Minor {
|
|
return v, true
|
|
}
|
|
return other, true
|
|
}
|
|
|
|
// RPCVersionList represents a list of RPC versions supported by a RPC peer. An
|
|
type RPCVersionList struct {
|
|
Versions []RPCVersion `json:"versions"`
|
|
}
|
|
|
|
// ParseRPCVersionList parses a version string in the format
|
|
// "major.minor,major.minor" into a RPCVersionList.
|
|
func ParseRPCVersionList(str string) (RPCVersionList, error) {
|
|
split := strings.Split(str, ",")
|
|
versions := make([]RPCVersion, len(split))
|
|
for i, v := range split {
|
|
version, err := ParseRPCVersion(v)
|
|
if err != nil {
|
|
return RPCVersionList{}, xerrors.Errorf("invalid version list: %s", str)
|
|
}
|
|
versions[i] = version
|
|
}
|
|
vl := RPCVersionList{Versions: versions}
|
|
err := vl.Validate()
|
|
if err != nil {
|
|
return RPCVersionList{}, xerrors.Errorf("invalid parsed version list %q: %w", str, err)
|
|
}
|
|
return vl, nil
|
|
}
|
|
|
|
func (vl RPCVersionList) String() string {
|
|
versionStrings := make([]string, len(vl.Versions))
|
|
for i, v := range vl.Versions {
|
|
versionStrings[i] = v.String()
|
|
}
|
|
return strings.Join(versionStrings, ",")
|
|
}
|
|
|
|
// Validate returns an error if the version list is not sorted or contains
|
|
// duplicate major versions.
|
|
func (vl RPCVersionList) Validate() error {
|
|
if len(vl.Versions) == 0 {
|
|
return xerrors.New("no versions")
|
|
}
|
|
for i := 0; i < len(vl.Versions); i++ {
|
|
if vl.Versions[i].Major == 0 {
|
|
return xerrors.Errorf("invalid version: %s", vl.Versions[i].String())
|
|
}
|
|
if i > 0 && vl.Versions[i-1].Major == vl.Versions[i].Major {
|
|
return xerrors.Errorf("duplicate major version: %d", vl.Versions[i].Major)
|
|
}
|
|
if i > 0 && vl.Versions[i-1].Major > vl.Versions[i].Major {
|
|
return xerrors.Errorf("versions are not sorted")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// IsCompatibleWith returns the lowest version that is compatible with both
|
|
// version lists. If the versions are not compatible, the second return value
|
|
// will be false.
|
|
func (vl RPCVersionList) IsCompatibleWith(other RPCVersionList) (RPCVersion, bool) {
|
|
bestVersion := RPCVersion{}
|
|
for _, v1 := range vl.Versions {
|
|
for _, v2 := range other.Versions {
|
|
if v1.Major == v2.Major && v1.Major > bestVersion.Major {
|
|
v, ok := v1.IsCompatibleWith(v2)
|
|
if ok {
|
|
bestVersion = v
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if bestVersion.Major == 0 {
|
|
return bestVersion, false
|
|
}
|
|
return bestVersion, true
|
|
}
|