initial capver packet tracking version (#2391)
* initial capver packet tracking version Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * Log the minimum version as client version, not only capver Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * remove old versions Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * use capver for integration tests Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * changelog Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * patch through m and n key Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> --------- Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
parent
cd3b8e68ff
commit
e172c29360
8 changed files with 397 additions and 68 deletions
|
@ -24,6 +24,7 @@ import (
|
|||
grpcRuntime "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||
"github.com/juanfont/headscale"
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/capver"
|
||||
"github.com/juanfont/headscale/hscontrol/db"
|
||||
"github.com/juanfont/headscale/hscontrol/derp"
|
||||
derpServer "github.com/juanfont/headscale/hscontrol/derp/server"
|
||||
|
@ -560,6 +561,11 @@ func (h *Headscale) Serve() error {
|
|||
spew.Dump(h.cfg)
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Caller().
|
||||
Str("minimum_version", capver.TailscaleVersion(MinimumCapVersion)).
|
||||
Msg("Clients with a lower minimum version will be rejected")
|
||||
|
||||
// Fetch an initial DERP Map before we start serving
|
||||
h.DERPMap = derp.GetDERPMap(h.cfg.DERP)
|
||||
h.mapper = mapper.NewMapper(h.db, h.cfg, h.DERPMap, h.nodeNotifier, h.polMan)
|
||||
|
|
92
hscontrol/capver/capver.go
Normal file
92
hscontrol/capver/capver.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
package capver
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
xmaps "golang.org/x/exp/maps"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
func tailscaleVersSorted() []string {
|
||||
vers := xmaps.Keys(tailscaleToCapVer)
|
||||
sort.Strings(vers)
|
||||
return vers
|
||||
}
|
||||
|
||||
func capVersSorted() []tailcfg.CapabilityVersion {
|
||||
capVers := xmaps.Keys(capVerToTailscaleVer)
|
||||
sort.Slice(capVers, func(i, j int) bool {
|
||||
return capVers[i] < capVers[j]
|
||||
})
|
||||
return capVers
|
||||
}
|
||||
|
||||
// TailscaleVersion returns the Tailscale version for the given CapabilityVersion.
|
||||
func TailscaleVersion(ver tailcfg.CapabilityVersion) string {
|
||||
return capVerToTailscaleVer[ver]
|
||||
}
|
||||
|
||||
// CapabilityVersion returns the CapabilityVersion for the given Tailscale version.
|
||||
func CapabilityVersion(ver string) tailcfg.CapabilityVersion {
|
||||
if !strings.HasPrefix(ver, "v") {
|
||||
ver = "v" + ver
|
||||
}
|
||||
return tailscaleToCapVer[ver]
|
||||
}
|
||||
|
||||
// TailscaleLatest returns the n latest Tailscale versions.
|
||||
func TailscaleLatest(n int) []string {
|
||||
if n <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
tsSorted := tailscaleVersSorted()
|
||||
|
||||
if n > len(tsSorted) {
|
||||
return tsSorted
|
||||
}
|
||||
|
||||
return tsSorted[len(tsSorted)-n:]
|
||||
}
|
||||
|
||||
// TailscaleLatestMajorMinor returns the n latest Tailscale versions (e.g. 1.80).
|
||||
func TailscaleLatestMajorMinor(n int, stripV bool) []string {
|
||||
if n <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
majors := set.Set[string]{}
|
||||
for _, vers := range tailscaleVersSorted() {
|
||||
if stripV {
|
||||
vers = strings.TrimPrefix(vers, "v")
|
||||
}
|
||||
v := strings.Split(vers, ".")
|
||||
majors.Add(v[0] + "." + v[1])
|
||||
}
|
||||
|
||||
majorSl := majors.Slice()
|
||||
sort.Strings(majorSl)
|
||||
|
||||
if n > len(majorSl) {
|
||||
return majorSl
|
||||
}
|
||||
|
||||
return majorSl[len(majorSl)-n:]
|
||||
}
|
||||
|
||||
// CapVerLatest returns the n latest CapabilityVersions.
|
||||
func CapVerLatest(n int) []tailcfg.CapabilityVersion {
|
||||
if n <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
s := capVersSorted()
|
||||
|
||||
if n > len(s) {
|
||||
return s
|
||||
}
|
||||
|
||||
return s[len(s)-n:]
|
||||
}
|
54
hscontrol/capver/capver_generated.go
Normal file
54
hscontrol/capver/capver_generated.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
package capver
|
||||
|
||||
//Generated DO NOT EDIT
|
||||
|
||||
import "tailscale.com/tailcfg"
|
||||
|
||||
var tailscaleToCapVer = map[string]tailcfg.CapabilityVersion{
|
||||
"v1.44.3": 63,
|
||||
"v1.56.1": 82,
|
||||
"v1.58.0": 85,
|
||||
"v1.58.1": 85,
|
||||
"v1.58.2": 85,
|
||||
"v1.60.0": 87,
|
||||
"v1.60.1": 87,
|
||||
"v1.62.0": 88,
|
||||
"v1.62.1": 88,
|
||||
"v1.64.0": 90,
|
||||
"v1.64.1": 90,
|
||||
"v1.64.2": 90,
|
||||
"v1.66.0": 95,
|
||||
"v1.66.1": 95,
|
||||
"v1.66.2": 95,
|
||||
"v1.66.3": 95,
|
||||
"v1.66.4": 95,
|
||||
"v1.68.0": 97,
|
||||
"v1.68.1": 97,
|
||||
"v1.68.2": 97,
|
||||
"v1.70.0": 102,
|
||||
"v1.72.0": 104,
|
||||
"v1.72.1": 104,
|
||||
"v1.74.0": 106,
|
||||
"v1.74.1": 106,
|
||||
"v1.76.0": 106,
|
||||
"v1.76.1": 106,
|
||||
"v1.76.6": 106,
|
||||
"v1.78.0": 109,
|
||||
"v1.78.1": 109,
|
||||
}
|
||||
|
||||
|
||||
var capVerToTailscaleVer = map[tailcfg.CapabilityVersion]string{
|
||||
63: "v1.44.3",
|
||||
82: "v1.56.1",
|
||||
85: "v1.58.0",
|
||||
87: "v1.60.0",
|
||||
88: "v1.62.0",
|
||||
90: "v1.64.0",
|
||||
95: "v1.66.0",
|
||||
97: "v1.68.0",
|
||||
102: "v1.70.0",
|
||||
104: "v1.72.0",
|
||||
106: "v1.74.0",
|
||||
109: "v1.78.0",
|
||||
}
|
53
hscontrol/capver/capver_test.go
Normal file
53
hscontrol/capver/capver_test.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package capver
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func TestTailscaleLatestMajorMinor(t *testing.T) {
|
||||
tests := []struct {
|
||||
n int
|
||||
stripV bool
|
||||
expected []string
|
||||
}{
|
||||
{3, false, []string{"v1.74", "v1.76", "v1.78"}},
|
||||
{2, true, []string{"1.76", "1.78"}},
|
||||
{0, false, nil},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run("", func(t *testing.T) {
|
||||
output := TailscaleLatestMajorMinor(test.n, test.stripV)
|
||||
if diff := cmp.Diff(output, test.expected); diff != "" {
|
||||
t.Errorf("TailscaleLatestMajorMinor(%d, %v) mismatch (-want +got):\n%s", test.n, test.stripV, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapVerMinimumTailscaleVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
input tailcfg.CapabilityVersion
|
||||
expected string
|
||||
}{
|
||||
{85, "v1.58.0"},
|
||||
{90, "v1.64.0"},
|
||||
{95, "v1.66.0"},
|
||||
{106, "v1.74.0"},
|
||||
{109, "v1.78.0"},
|
||||
{9001, ""}, // Test case for a version higher than any in the map
|
||||
{60, ""}, // Test case for a version lower than any in the map
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run("", func(t *testing.T) {
|
||||
output := TailscaleVersion(test.input)
|
||||
if output != test.expected {
|
||||
t.Errorf("CapVerFromTailscaleVersion(%d) = %s; want %s", test.input, output, test.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
157
hscontrol/capver/gen/main.go
Normal file
157
hscontrol/capver/gen/main.go
Normal file
|
@ -0,0 +1,157 @@
|
|||
package main
|
||||
|
||||
//go:generate go run main.go
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
xmaps "golang.org/x/exp/maps"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
const (
|
||||
releasesURL = "https://api.github.com/repos/tailscale/tailscale/releases"
|
||||
rawFileURL = "https://github.com/tailscale/tailscale/raw/refs/tags/%s/tailcfg/tailcfg.go"
|
||||
outputFile = "../capver_generated.go"
|
||||
)
|
||||
|
||||
type Release struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func getCapabilityVersions() (map[string]tailcfg.CapabilityVersion, error) {
|
||||
// Fetch the releases
|
||||
resp, err := http.Get(releasesURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error fetching releases: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading response body: %w", err)
|
||||
}
|
||||
|
||||
var releases []Release
|
||||
err = json.Unmarshal(body, &releases)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error unmarshalling JSON: %w", err)
|
||||
}
|
||||
|
||||
// Regular expression to find the CurrentCapabilityVersion line
|
||||
re := regexp.MustCompile(`const CurrentCapabilityVersion CapabilityVersion = (\d+)`)
|
||||
|
||||
versions := make(map[string]tailcfg.CapabilityVersion)
|
||||
|
||||
for _, release := range releases {
|
||||
version := strings.TrimSpace(release.Name)
|
||||
if !strings.HasPrefix(version, "v") {
|
||||
version = "v" + version
|
||||
}
|
||||
|
||||
// Fetch the raw Go file
|
||||
rawURL := fmt.Sprintf(rawFileURL, version)
|
||||
resp, err := http.Get(rawURL)
|
||||
if err != nil {
|
||||
fmt.Printf("Error fetching raw file for version %s: %v\n", version, err)
|
||||
continue
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading raw file for version %s: %v\n", version, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Find the CurrentCapabilityVersion
|
||||
matches := re.FindStringSubmatch(string(body))
|
||||
if len(matches) > 1 {
|
||||
capabilityVersionStr := matches[1]
|
||||
capabilityVersion, _ := strconv.Atoi(capabilityVersionStr)
|
||||
versions[version] = tailcfg.CapabilityVersion(capabilityVersion)
|
||||
} else {
|
||||
fmt.Printf("Version: %s, CurrentCapabilityVersion not found\n", version)
|
||||
}
|
||||
}
|
||||
|
||||
return versions, nil
|
||||
}
|
||||
|
||||
func writeCapabilityVersionsToFile(versions map[string]tailcfg.CapabilityVersion) error {
|
||||
// Open the output file
|
||||
file, err := os.Create(outputFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Write the package declaration and variable
|
||||
file.WriteString("package capver\n\n")
|
||||
file.WriteString("//Generated DO NOT EDIT\n\n")
|
||||
file.WriteString(`import "tailscale.com/tailcfg"`)
|
||||
file.WriteString("\n\n")
|
||||
file.WriteString("var tailscaleToCapVer = map[string]tailcfg.CapabilityVersion{\n")
|
||||
|
||||
sortedVersions := xmaps.Keys(versions)
|
||||
sort.Strings(sortedVersions)
|
||||
for _, version := range sortedVersions {
|
||||
file.WriteString(fmt.Sprintf("\t\"%s\": %d,\n", version, versions[version]))
|
||||
}
|
||||
file.WriteString("}\n")
|
||||
|
||||
file.WriteString("\n\n")
|
||||
file.WriteString("var capVerToTailscaleVer = map[tailcfg.CapabilityVersion]string{\n")
|
||||
|
||||
capVarToTailscaleVer := make(map[tailcfg.CapabilityVersion]string)
|
||||
for _, v := range sortedVersions {
|
||||
cap := versions[v]
|
||||
log.Printf("cap for v: %d, %s", cap, v)
|
||||
|
||||
// If it is already set, skip and continue,
|
||||
// we only want the first tailscale vsion per
|
||||
// capability vsion.
|
||||
if _, ok := capVarToTailscaleVer[cap]; ok {
|
||||
log.Printf("Skipping %d, %s", cap, v)
|
||||
continue
|
||||
}
|
||||
log.Printf("Storing %d, %s", cap, v)
|
||||
capVarToTailscaleVer[cap] = v
|
||||
}
|
||||
|
||||
capsSorted := xmaps.Keys(capVarToTailscaleVer)
|
||||
sort.Slice(capsSorted, func(i, j int) bool {
|
||||
return capsSorted[i] < capsSorted[j]
|
||||
})
|
||||
for _, capVer := range capsSorted {
|
||||
file.WriteString(fmt.Sprintf("\t%d:\t\t\"%s\",\n", capVer, capVarToTailscaleVer[capVer]))
|
||||
}
|
||||
file.WriteString("}\n")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
versions, err := getCapabilityVersions()
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = writeCapabilityVersionsToFile(versions)
|
||||
if err != nil {
|
||||
fmt.Println("Error writing to file:", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Capability versions written to", outputFile)
|
||||
}
|
|
@ -8,6 +8,7 @@ import (
|
|||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/juanfont/headscale/hscontrol/capver"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/net/http2"
|
||||
|
@ -155,10 +156,19 @@ func isSupportedVersion(version tailcfg.CapabilityVersion) bool {
|
|||
return version >= MinimumCapVersion
|
||||
}
|
||||
|
||||
func rejectUnsupported(writer http.ResponseWriter, version tailcfg.CapabilityVersion) bool {
|
||||
func rejectUnsupported(writer http.ResponseWriter, version tailcfg.CapabilityVersion, mkey key.MachinePublic, nkey key.NodePublic) bool {
|
||||
// Reject unsupported versions
|
||||
if !isSupportedVersion(version) {
|
||||
httpError(writer, nil, "unsupported client version", http.StatusBadRequest)
|
||||
log.Error().
|
||||
Caller().
|
||||
Int("minimum_cap_ver", int(MinimumCapVersion)).
|
||||
Int("client_cap_ver", int(version)).
|
||||
Str("minimum_version", capver.TailscaleVersion(MinimumCapVersion)).
|
||||
Str("client_version", capver.TailscaleVersion(version)).
|
||||
Str("node_key", nkey.ShortString()).
|
||||
Str("machine_key", mkey.ShortString()).
|
||||
Msg("unsupported client connected")
|
||||
http.Error(writer, "unsupported client version", http.StatusBadRequest)
|
||||
|
||||
return true
|
||||
}
|
||||
|
@ -188,7 +198,7 @@ func (ns *noiseServer) NoisePollNetMapHandler(
|
|||
}
|
||||
|
||||
// Reject unsupported versions
|
||||
if rejectUnsupported(writer, mapRequest.Version) {
|
||||
if rejectUnsupported(writer, mapRequest.Version, ns.machineKey, mapRequest.NodeKey) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -233,7 +243,7 @@ func (ns *noiseServer) NoiseRegistrationHandler(
|
|||
}
|
||||
|
||||
// Reject unsupported versions
|
||||
if rejectUnsupported(writer, registerRequest.Version) {
|
||||
if rejectUnsupported(writer, registerRequest.Version, ns.machineKey, registerRequest.NodeKey) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue