Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
dc3c15b
control/controlclient: back out HW key attestation (#17664)
patrickod Oct 27, 2025
bad03ee
feature/identityfederation: strip query params on clientID (#17666)
mcoulombe Oct 27, 2025
033adc3
cmd/tailscale/cli: move JetKVM scripts to /userdata/init.d for persis…
srwareham Oct 27, 2025
53004dd
wgengine/magicsock: fix js/wasm crash regression loading non-existent…
bradfitz Oct 28, 2025
2dd72f6
Revert "logtail: avoid racing eventbus subscriptions with Shutdown (#…
creachadair Oct 28, 2025
68cba30
VERSION.txt: this is v1.90.4
nickkhyl Oct 28, 2025
1a6c315
sessionrecording: fix regression in recent http2 package change
bradfitz Oct 29, 2025
300e606
cmd/k8s-operator/generate: skip tests if no network or Helm is down
bradfitz Oct 29, 2025
6324200
VERSION.txt: this is v1.90.5
nickkhyl Oct 30, 2025
faca4c0
.github/workflows: pin the google/oss-fuzz GitHub Actions
alexwlchan Oct 21, 2025
6e2f2bb
ipn/ipnlocal: do not stall event processing for appc route updates (#…
creachadair Oct 29, 2025
b6eabd4
util/eventbus: allow logging of slow subscribers (#17705)
creachadair Oct 30, 2025
28f6c2d
VERSION.txt: this is v1.90.6
nickkhyl Oct 31, 2025
e602907
wgengine/magicsock: validate endpoint.derpAddr in Conn.onUDPRelayAllo…
jwhited Nov 11, 2025
771a9d2
wgengine/magicsock: fix UDPRelayAllocReq/Resp deadlock (#17831)
jwhited Nov 11, 2025
eb03b35
net/udprelay: replace VNI pool with selection algorithm (#17868)
jwhited Nov 12, 2025
0f421d3
feature/relayserver,ipn/ipnlocal,net/udprelay: plumb DERPMap (#17881)
jwhited Nov 14, 2025
ea8eeeb
feature/relayserver: fix Shutdown() deadlock (#17898)
jwhited Nov 14, 2025
fa514c7
net/netmon: do not abandon a subscriber when exiting early (#17899) (…
barnstar Nov 17, 2025
6b64718
tka: don't try to read AUMs which are partway through being written
alexwlchan Oct 21, 2025
43ab8b4
tka: rename a mutex to `mu` instead of single-letter `l`
alexwlchan Oct 29, 2025
37b63ef
ipn/ipnlocal: use an in-memory TKA store if FS is unavailable
alexwlchan Oct 29, 2025
90d3cb3
VERSION.txt: this is v1.90.7
nickkhyl Nov 18, 2025
6b0fbff
tka: move RemoveAll() to CompactableChonk
alexwlchan Nov 18, 2025
ccf4f3c
VERSION.txt: this is v1.90.8
nickkhyl Nov 18, 2025
7f69a3a
Merge branch 'release-branch/1.90' into cpierre/coreweave-1.90
ChandonPierre Nov 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.90.3
1.90.8
13 changes: 13 additions & 0 deletions client/local/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,19 @@ func (lc *Client) DebugResultJSON(ctx context.Context, action string) (any, erro
return x, nil
}

// QueryOptionalFeatures queries the optional features supported by the Tailscale daemon.
func (lc *Client) QueryOptionalFeatures(ctx context.Context) (*apitype.OptionalFeatures, error) {
body, err := lc.send(ctx, "POST", "/localapi/v0/debug-optional-features", 200, nil)
if err != nil {
return nil, fmt.Errorf("error %w: %s", err, body)
}
var x apitype.OptionalFeatures
if err := json.Unmarshal(body, &x); err != nil {
return nil, err
}
return &x, nil
}

// SetDevStoreKeyValue set a statestore key/value. It's only meant for development.
// The schema (including when keys are re-read) is not a stable interface.
func (lc *Client) SetDevStoreKeyValue(ctx context.Context, key, value string) error {
Expand Down
10 changes: 10 additions & 0 deletions client/tailscale/apitype/apitype.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,13 @@ type DNSQueryResponse struct {
// Resolvers is the list of resolvers that the forwarder deemed able to resolve the query.
Resolvers []*dnstype.Resolver
}

// OptionalFeatures describes which optional features are enabled in the build.
type OptionalFeatures struct {
// Features is the map of optional feature names to whether they are
// enabled.
//
// Disabled features may be absent from the map. (That is, false values
// are not guaranteed to be present.)
Features map[string]bool
}
2 changes: 1 addition & 1 deletion cmd/k8s-operator/generate/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ func generate(baseDir string) error {
if _, err := file.Write([]byte(helmConditionalEnd)); err != nil {
return fmt.Errorf("error writing helm if-statement end: %w", err)
}
return nil
return file.Close()
}
for _, crd := range []struct {
crdPath, templatePath string
Expand Down
26 changes: 25 additions & 1 deletion cmd/k8s-operator/generate/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,50 @@ package main

import (
"bytes"
"context"
"net"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"

"tailscale.com/tstest/nettest"
"tailscale.com/util/cibuild"
)

func Test_generate(t *testing.T) {
nettest.SkipIfNoNetwork(t)

ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
defer cancel()
if _, err := net.DefaultResolver.LookupIPAddr(ctx, "get.helm.sh"); err != nil {
// https://github.com/helm/helm/issues/31434
t.Skipf("get.helm.sh seems down or unreachable; skipping test")
}

base, err := os.Getwd()
base = filepath.Join(base, "../../../")
if err != nil {
t.Fatalf("error getting current working directory: %v", err)
}
defer cleanup(base)

helmCLIPath := filepath.Join(base, "tool/helm")
if out, err := exec.Command(helmCLIPath, "version").CombinedOutput(); err != nil && cibuild.On() {
// It's not just DNS. Azure is generating bogus certs within GitHub Actions at least for
// helm. So try to run it and see if we can even fetch it.
//
// https://github.com/helm/helm/issues/31434
t.Skipf("error fetching helm; skipping test in CI: %v, %s", err, out)
}

if err := generate(base); err != nil {
t.Fatalf("CRD template generation: %v", err)
}

tempDir := t.TempDir()
helmCLIPath := filepath.Join(base, "tool/helm")
helmChartTemplatesPath := filepath.Join(base, "cmd/k8s-operator/deploy/chart")
helmPackageCmd := exec.Command(helmCLIPath, "package", helmChartTemplatesPath, "--destination", tempDir, "--version", "0.0.1")
helmPackageCmd.Stderr = os.Stderr
Expand Down
7 changes: 5 additions & 2 deletions cmd/tailscale/cli/configure-jetkvm.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,12 @@ func runConfigureJetKVM(ctx context.Context, args []string) error {
if runtime.GOOS != "linux" || distro.Get() != distro.JetKVM {
return errors.New("only implemented on JetKVM")
}
err := os.WriteFile("/etc/init.d/S22tailscale", bytes.TrimLeft([]byte(`
if err := os.MkdirAll("/userdata/init.d", 0755); err != nil {
return errors.New("unable to create /userdata/init.d")
}
err := os.WriteFile("/userdata/init.d/S22tailscale", bytes.TrimLeft([]byte(`
#!/bin/sh
# /etc/init.d/S22tailscale
# /userdata/init.d/S22tailscale
# Start/stop tailscaled

case "$1" in
Expand Down
1 change: 1 addition & 0 deletions cmd/tailscale/cli/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,7 @@ func upWorthyWarning(s string) bool {
strings.Contains(s, healthmsg.WarnAcceptRoutesOff) ||
strings.Contains(s, healthmsg.LockedOut) ||
strings.Contains(s, healthmsg.WarnExitNodeUsage) ||
strings.Contains(s, healthmsg.InMemoryTailnetLockState) ||
strings.Contains(strings.ToLower(s), "update available: ")
}

Expand Down
22 changes: 0 additions & 22 deletions control/controlclient/direct.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import (
"bytes"
"cmp"
"context"
"crypto"
"crypto/sha256"
"encoding/binary"
"encoding/json"
"errors"
Expand Down Expand Up @@ -948,26 +946,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
ConnectionHandleForTest: connectionHandleForTest,
}

// If we have a hardware attestation key, sign the node key with it and send
// the key & signature in the map request.
if buildfeatures.HasTPM {
if k := persist.AsStruct().AttestationKey; k != nil && !k.IsZero() {
hwPub := key.HardwareAttestationPublicFromPlatformKey(k)
request.HardwareAttestationKey = hwPub

t := c.clock.Now()
msg := fmt.Sprintf("%d|%s", t.Unix(), nodeKey.String())
digest := sha256.Sum256([]byte(msg))
sig, err := k.Sign(nil, digest[:], crypto.SHA256)
if err != nil {
c.logf("failed to sign node key with hardware attestation key: %v", err)
} else {
request.HardwareAttestationKeySignature = sig
request.HardwareAttestationKeySignatureTimestamp = t
}
}
}

var extraDebugFlags []string
if buildfeatures.HasAdvertiseRoutes && hi != nil && c.netMon != nil && !c.skipIPForwardingCheck &&
ipForwardingBroken(hi.RoutableIPs, c.netMon.InterfaceState()) {
Expand Down
6 changes: 6 additions & 0 deletions feature/feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ var ErrUnavailable = errors.New("feature not included in this build")

var in = map[string]bool{}

// Registered reports the set of registered features.
//
// The returned map should not be modified by the caller,
// not accessed concurrently with calls to Register.
func Registered() map[string]bool { return in }

// Register notes that the named feature is linked into the binary.
func Register(name string) {
if _, ok := in[name]; ok {
Expand Down
19 changes: 11 additions & 8 deletions feature/identityfederation/identityfederation.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@ func resolveAuthKey(ctx context.Context, baseURL, clientID, idToken string, tags
baseURL = ipn.DefaultControlURL
}

ephemeral, preauth, err := parseOptionalAttributes(clientID)
strippedID, ephemeral, preauth, err := parseOptionalAttributes(clientID)
if err != nil {
return "", fmt.Errorf("failed to parse optional config attributes: %w", err)
}

accessToken, err := exchangeJWTForToken(ctx, baseURL, clientID, idToken)
accessToken, err := exchangeJWTForToken(ctx, baseURL, strippedID, idToken)
if err != nil {
return "", fmt.Errorf("failed to exchange JWT for access token: %w", err)
}
Expand Down Expand Up @@ -79,15 +79,15 @@ func resolveAuthKey(ctx context.Context, baseURL, clientID, idToken string, tags
return authkey, nil
}

func parseOptionalAttributes(clientID string) (ephemeral bool, preauthorized bool, err error) {
_, attrs, found := strings.Cut(clientID, "?")
func parseOptionalAttributes(clientID string) (strippedID string, ephemeral bool, preauthorized bool, err error) {
strippedID, attrs, found := strings.Cut(clientID, "?")
if !found {
return true, false, nil
return clientID, true, false, nil
}

parsed, err := url.ParseQuery(attrs)
if err != nil {
return false, false, fmt.Errorf("failed to parse optional config attributes: %w", err)
return "", false, false, fmt.Errorf("failed to parse optional config attributes: %w", err)
}

for k := range parsed {
Expand All @@ -97,11 +97,14 @@ func parseOptionalAttributes(clientID string) (ephemeral bool, preauthorized boo
case "preauthorized":
preauthorized, err = strconv.ParseBool(parsed.Get(k))
default:
return false, false, fmt.Errorf("unknown optional config attribute %q", k)
return "", false, false, fmt.Errorf("unknown optional config attribute %q", k)
}
}
if err != nil {
return "", false, false, err
}

return ephemeral, preauthorized, err
return strippedID, ephemeral, preauthorized, nil
}

// exchangeJWTForToken exchanges a JWT for a Tailscale access token.
Expand Down
10 changes: 9 additions & 1 deletion feature/identityfederation/identityfederation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,34 +87,39 @@ func TestParseOptionalAttributes(t *testing.T) {
tests := []struct {
name string
clientID string
wantClientID string
wantEphemeral bool
wantPreauth bool
wantErr string
}{
{
name: "default values",
clientID: "client-123",
wantClientID: "client-123",
wantEphemeral: true,
wantPreauth: false,
wantErr: "",
},
{
name: "custom values",
clientID: "client-123?ephemeral=false&preauthorized=true",
wantClientID: "client-123",
wantEphemeral: false,
wantPreauth: true,
wantErr: "",
},
{
name: "unknown attribute",
clientID: "client-123?unknown=value",
wantClientID: "",
wantEphemeral: false,
wantPreauth: false,
wantErr: `unknown optional config attribute "unknown"`,
},
{
name: "invalid value",
clientID: "client-123?ephemeral=invalid",
wantClientID: "",
wantEphemeral: false,
wantPreauth: false,
wantErr: `strconv.ParseBool: parsing "invalid": invalid syntax`,
Expand All @@ -123,7 +128,7 @@ func TestParseOptionalAttributes(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ephemeral, preauth, err := parseOptionalAttributes(tt.clientID)
strippedID, ephemeral, preauth, err := parseOptionalAttributes(tt.clientID)
if tt.wantErr != "" {
if err == nil {
t.Errorf("parseOptionalAttributes() error = nil, want %q", tt.wantErr)
Expand All @@ -138,6 +143,9 @@ func TestParseOptionalAttributes(t *testing.T) {
return
}
}
if strippedID != tt.wantClientID {
t.Errorf("parseOptionalAttributes() strippedID = %v, want %v", strippedID, tt.wantClientID)
}
if ephemeral != tt.wantEphemeral {
t.Errorf("parseOptionalAttributes() ephemeral = %v, want %v", ephemeral, tt.wantEphemeral)
}
Expand Down
2 changes: 2 additions & 0 deletions feature/portmapper/portmapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package portmapper

import (
"tailscale.com/feature"
"tailscale.com/net/netmon"
"tailscale.com/net/portmapper"
"tailscale.com/net/portmapper/portmappertype"
Expand All @@ -14,6 +15,7 @@ import (
)

func init() {
feature.Register("portmapper")
portmappertype.HookNewPortMapper.Set(newPortMapper)
}

Expand Down
Loading