initial capver packet tracking version (#2391)
Some checks failed
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Tests / test (push) Has been cancelled

* 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:
Kristoffer Dalby 2025-01-30 21:49:09 +00:00 committed by GitHub
parent cd3b8e68ff
commit e172c29360
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 397 additions and 68 deletions

View file

@ -6,6 +6,8 @@
- `oidc.map_legacy_users` is now `false` by default - `oidc.map_legacy_users` is now `false` by default
[#2350](https://github.com/juanfont/headscale/pull/2350) [#2350](https://github.com/juanfont/headscale/pull/2350)
- Print Tailscale version instead of capability versions for outdated nodes
[#2391](https://github.com/juanfont/headscale/pull/2391)
## 0.24.2 (2025-01-30) ## 0.24.2 (2025-01-30)
@ -24,8 +26,8 @@
[#2367](https://github.com/juanfont/headscale/pull/2367) [#2367](https://github.com/juanfont/headscale/pull/2367)
- Relax username validation to allow emails - Relax username validation to allow emails
[#2364](https://github.com/juanfont/headscale/pull/2364) [#2364](https://github.com/juanfont/headscale/pull/2364)
- Remove invalid routes and add stronger constraints for routes to avoid API panic - Remove invalid routes and add stronger constraints for routes to avoid API
[#2371](https://github.com/juanfont/headscale/pull/2371) panic [#2371](https://github.com/juanfont/headscale/pull/2371)
- Fix panic when `derp.update_frequency` is 0 - Fix panic when `derp.update_frequency` is 0
[#2368](https://github.com/juanfont/headscale/pull/2368) [#2368](https://github.com/juanfont/headscale/pull/2368)
@ -60,8 +62,7 @@ and have it populate to Headscale automatically the next time they log in.
However, this may affect the way you reference users in policies. However, this may affect the way you reference users in policies.
Headscale v0.23.0 and earlier never recorded the `iss` and `sub` fields, so all Headscale v0.23.0 and earlier never recorded the `iss` and `sub` fields, so all
legacy (existing) OIDC accounts _need to be migrated_ to be properly legacy (existing) OIDC accounts _need to be migrated_ to be properly secured.
secured.
#### What do I need to do to migrate? #### What do I need to do to migrate?
@ -73,8 +74,8 @@ The migration will mostly be done automatically, with one exception. If your
OIDC does not provide an `email_verified` claim, Headscale will ignore the OIDC does not provide an `email_verified` claim, Headscale will ignore the
`email`. This means that either the administrator will have to mark the user `email`. This means that either the administrator will have to mark the user
emails as verified, or ensure the users verify their emails. Any unverified emails as verified, or ensure the users verify their emails. Any unverified
emails will be ignored, meaning that the users will get new accounts instead emails will be ignored, meaning that the users will get new accounts instead of
of being migrated. being migrated.
After this exception is ensured, make all users log into Headscale with their After this exception is ensured, make all users log into Headscale with their
account, and Headscale will automatically update the account record. This will account, and Headscale will automatically update the account record. This will
@ -175,7 +176,8 @@ This will also affect the way you
- User gRPC/API [#2261](https://github.com/juanfont/headscale/pull/2261): - User gRPC/API [#2261](https://github.com/juanfont/headscale/pull/2261):
- If you depend on a Headscale Web UI, you should wait with this update until - If you depend on a Headscale Web UI, you should wait with this update until
the UI have been updated to match the new API. the UI have been updated to match the new API.
- `GET /api/v1/user/{name}` and `GetUser` have been removed in favour of `ListUsers` with an ID parameter - `GET /api/v1/user/{name}` and `GetUser` have been removed in favour of
`ListUsers` with an ID parameter
- `RenameUser` and `DeleteUser` now require an ID instead of a name. - `RenameUser` and `DeleteUser` now require an ID instead of a name.
### Changes ### Changes
@ -197,9 +199,12 @@ This will also affect the way you
- CLI for managing users now accepts `--identifier` in addition to `--name`, - CLI for managing users now accepts `--identifier` in addition to `--name`,
usage of `--identifier` is recommended usage of `--identifier` is recommended
[#2261](https://github.com/juanfont/headscale/pull/2261) [#2261](https://github.com/juanfont/headscale/pull/2261)
- Add `dns.extra_records_path` configuration option [#2262](https://github.com/juanfont/headscale/issues/2262) - Add `dns.extra_records_path` configuration option
- Support client verify for DERP [#2046](https://github.com/juanfont/headscale/pull/2046) [#2262](https://github.com/juanfont/headscale/issues/2262)
- Add PKCE Verifier for OIDC [#2314](https://github.com/juanfont/headscale/pull/2314) - Support client verify for DERP
[#2046](https://github.com/juanfont/headscale/pull/2046)
- Add PKCE Verifier for OIDC
[#2314](https://github.com/juanfont/headscale/pull/2314)
## 0.23.0 (2024-09-18) ## 0.23.0 (2024-09-18)
@ -730,8 +735,8 @@ behaviour.
- All machines can communicate with all machines by default - All machines can communicate with all machines by default
- Tags should now work correctly and adding a host to Headscale should now - Tags should now work correctly and adding a host to Headscale should now
reload the rules. reload the rules.
- The documentation have a [fictional example](./docs/ref/acls.md) that should cover - The documentation have a [fictional example](./docs/ref/acls.md) that should
some use cases of the ACLs features cover some use cases of the ACLs features
### Features ### Features
@ -749,7 +754,8 @@ behaviour.
- Add IPv6 support to the prefix assigned to namespaces - Add IPv6 support to the prefix assigned to namespaces
- Add API Key support - Add API Key support
- Enable remote control of `headscale` via CLI [docs](./docs/ref/remote-cli.md) - Enable remote control of `headscale` via CLI
[docs](./docs/ref/remote-cli.md)
- Enable HTTP API (beta, subject to change) - Enable HTTP API (beta, subject to change)
- OpenID Connect users will be mapped per namespaces - OpenID Connect users will be mapped per namespaces
- Each user will get its own namespace, created if it does not exist - Each user will get its own namespace, created if it does not exist

View file

@ -24,6 +24,7 @@ import (
grpcRuntime "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" grpcRuntime "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/juanfont/headscale" "github.com/juanfont/headscale"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1" 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/db"
"github.com/juanfont/headscale/hscontrol/derp" "github.com/juanfont/headscale/hscontrol/derp"
derpServer "github.com/juanfont/headscale/hscontrol/derp/server" derpServer "github.com/juanfont/headscale/hscontrol/derp/server"
@ -560,6 +561,11 @@ func (h *Headscale) Serve() error {
spew.Dump(h.cfg) 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 // Fetch an initial DERP Map before we start serving
h.DERPMap = derp.GetDERPMap(h.cfg.DERP) h.DERPMap = derp.GetDERPMap(h.cfg.DERP)
h.mapper = mapper.NewMapper(h.db, h.cfg, h.DERPMap, h.nodeNotifier, h.polMan) h.mapper = mapper.NewMapper(h.db, h.cfg, h.DERPMap, h.nodeNotifier, h.polMan)

View 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:]
}

View 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",
}

View 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)
}
})
}
}

View 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)
}

View file

@ -8,6 +8,7 @@ import (
"net/http" "net/http"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/juanfont/headscale/hscontrol/capver"
"github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/types"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"golang.org/x/net/http2" "golang.org/x/net/http2"
@ -155,10 +156,19 @@ func isSupportedVersion(version tailcfg.CapabilityVersion) bool {
return version >= MinimumCapVersion 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 // Reject unsupported versions
if !isSupportedVersion(version) { 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 return true
} }
@ -188,7 +198,7 @@ func (ns *noiseServer) NoisePollNetMapHandler(
} }
// Reject unsupported versions // Reject unsupported versions
if rejectUnsupported(writer, mapRequest.Version) { if rejectUnsupported(writer, mapRequest.Version, ns.machineKey, mapRequest.NodeKey) {
return return
} }
@ -233,7 +243,7 @@ func (ns *noiseServer) NoiseRegistrationHandler(
} }
// Reject unsupported versions // Reject unsupported versions
if rejectUnsupported(writer, registerRequest.Version) { if rejectUnsupported(writer, registerRequest.Version, ns.machineKey, registerRequest.NodeKey) {
return return
} }

View file

@ -12,6 +12,7 @@ import (
"time" "time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1" v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/juanfont/headscale/hscontrol/capver"
"github.com/juanfont/headscale/hscontrol/util" "github.com/juanfont/headscale/hscontrol/util"
"github.com/juanfont/headscale/integration/dockertestutil" "github.com/juanfont/headscale/integration/dockertestutil"
"github.com/juanfont/headscale/integration/dsic" "github.com/juanfont/headscale/integration/dsic"
@ -51,53 +52,6 @@ var (
errNoUserAvailable = errors.New("no user available") errNoUserAvailable = errors.New("no user available")
errNoClientFound = errors.New("client not found") errNoClientFound = errors.New("client not found")
// Tailscale started adding TS2021 support in CapabilityVersion>=28 (v1.24.0), but
// proper support in Headscale was only added for CapabilityVersion>=39 clients (v1.30.0).
tailscaleVersions2021 = map[string]bool{
"head": true,
"unstable": true,
"1.74": true, // CapVer: 106
"1.72": true, // CapVer: 104
"1.70": true, // CapVer: 102
"1.68": true, // CapVer: 97
"1.66": true, // CapVer: 95
"1.64": true, // CapVer: 90
"1.62": true, // CapVer: 88
"1.60": true, // CapVer: 87
"1.58": true, // CapVer: 85
"1.56": true, // Oldest supported version, CapVer: 82
"1.54": false, // CapVer: 79
"1.52": false, // CapVer: 79
"1.50": false, // CapVer: 74
"1.48": false, // CapVer: 68
"1.46": false, // CapVer: 65
"1.44": false, // CapVer: 63
"1.42": false, // CapVer: 61
"1.40": false, // CapVer: 61
"1.38": false, // CapVer: 58
"1.36": false, // CapVer: 56
"1.34": false, // CapVer: 51
"1.32": false, // CapVer: 46
"1.30": false,
}
tailscaleVersions2019 = map[string]bool{
"1.28": false,
"1.26": false,
"1.24": false, // Tailscale SSH
"1.22": false,
"1.20": false,
"1.18": false,
}
// tailscaleVersionsUnavailable = []string{
// // These versions seem to fail when fetching from apt.
// "1.14.6",
// "1.12.4",
// "1.10.2",
// "1.8.7",
// }.
// AllVersions represents a list of Tailscale versions the suite // AllVersions represents a list of Tailscale versions the suite
// uses to test compatibility with the ControlServer. // uses to test compatibility with the ControlServer.
// //
@ -107,10 +61,7 @@ var (
// //
// The rest of the version represents Tailscale versions that can be // The rest of the version represents Tailscale versions that can be
// found in Tailscale's apt repository. // found in Tailscale's apt repository.
AllVersions = append( AllVersions = append([]string{"head", "unstable"}, capver.TailscaleLatestMajorMinor(10, true)...)
enabledVersions(tailscaleVersions2021),
enabledVersions(tailscaleVersions2019)...,
)
// MustTestVersions is the minimum set of versions we should test. // MustTestVersions is the minimum set of versions we should test.
// At the moment, this is arbitrarily chosen as: // At the moment, this is arbitrarily chosen as: