Files
coder/vpn/version.go
Dean Sheather 14a60303ac chore: rework RPC version negotiation (#15687)
Changes the RPC header format from `codervpn <version> <role>` to
`codervpn <role> <version1,version2,...>`.

The versions list is a list of the maximum supported minor version for
each major version, sorted by major versions.

E.g. `1.0,2.3,3.1` means `1.0, 2.0, 2.1, 2.2, 2.3, 3.0, 3.1` are
supported.

When we eventually support multiple versions, the peer's version list
will be compared against the current supported versions list to
determine the maximum major and minor version supported by both peers.

Closes #15601
2024-12-04 09:38:24 +00:00

142 lines
4.1 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{
{Major: 1, Minor: 0},
},
}
// 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
}