Skip to content

Commit 89091a3

Browse files
committed
Merge tag 'v1.90.6' into sunos-1.90
Release 1.90.6
2 parents 8ae7205 + 28f6c2d commit 89091a3

File tree

37 files changed

+362
-174
lines changed

37 files changed

+362
-174
lines changed

.github/workflows/test.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -613,7 +613,9 @@ jobs:
613613
steps:
614614
- name: build fuzzers
615615
id: build
616-
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master
616+
# As of 21 October 2025, this repo doesn't tag releases, so this commit
617+
# hash is just the tip of master.
618+
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@1242ccb5b6352601e73c00f189ac2ae397242264
617619
# continue-on-error makes steps.build.conclusion be 'success' even if
618620
# steps.build.outcome is 'failure'. This means this step does not
619621
# contribute to the job's overall pass/fail evaluation.
@@ -643,7 +645,9 @@ jobs:
643645
# report a failure because TS_FUZZ_CURRENTLY_BROKEN is set to the wrong
644646
# value.
645647
if: steps.build.outcome == 'success'
646-
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
648+
# As of 21 October 2025, this repo doesn't tag releases, so this commit
649+
# hash is just the tip of master.
650+
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@1242ccb5b6352601e73c00f189ac2ae397242264
647651
with:
648652
oss-fuzz-project-name: 'tailscale'
649653
fuzz-seconds: 150

VERSION.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.90.3
1+
1.90.6

client/local/local.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,19 @@ func (lc *Client) DebugResultJSON(ctx context.Context, action string) (any, erro
596596
return x, nil
597597
}
598598

599+
// QueryOptionalFeatures queries the optional features supported by the Tailscale daemon.
600+
func (lc *Client) QueryOptionalFeatures(ctx context.Context) (*apitype.OptionalFeatures, error) {
601+
body, err := lc.send(ctx, "POST", "/localapi/v0/debug-optional-features", 200, nil)
602+
if err != nil {
603+
return nil, fmt.Errorf("error %w: %s", err, body)
604+
}
605+
var x apitype.OptionalFeatures
606+
if err := json.Unmarshal(body, &x); err != nil {
607+
return nil, err
608+
}
609+
return &x, nil
610+
}
611+
599612
// SetDevStoreKeyValue set a statestore key/value. It's only meant for development.
600613
// The schema (including when keys are re-read) is not a stable interface.
601614
func (lc *Client) SetDevStoreKeyValue(ctx context.Context, key, value string) error {

client/tailscale/apitype/apitype.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,13 @@ type DNSQueryResponse struct {
9494
// Resolvers is the list of resolvers that the forwarder deemed able to resolve the query.
9595
Resolvers []*dnstype.Resolver
9696
}
97+
98+
// OptionalFeatures describes which optional features are enabled in the build.
99+
type OptionalFeatures struct {
100+
// Features is the map of optional feature names to whether they are
101+
// enabled.
102+
//
103+
// Disabled features may be absent from the map. (That is, false values
104+
// are not guaranteed to be present.)
105+
Features map[string]bool
106+
}

cmd/k8s-operator/generate/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ func generate(baseDir string) error {
144144
if _, err := file.Write([]byte(helmConditionalEnd)); err != nil {
145145
return fmt.Errorf("error writing helm if-statement end: %w", err)
146146
}
147-
return nil
147+
return file.Close()
148148
}
149149
for _, crd := range []struct {
150150
crdPath, templatePath string

cmd/k8s-operator/generate/main_test.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,50 @@ package main
77

88
import (
99
"bytes"
10+
"context"
11+
"net"
1012
"os"
1113
"os/exec"
1214
"path/filepath"
1315
"strings"
1416
"testing"
17+
"time"
18+
19+
"tailscale.com/tstest/nettest"
20+
"tailscale.com/util/cibuild"
1521
)
1622

1723
func Test_generate(t *testing.T) {
24+
nettest.SkipIfNoNetwork(t)
25+
26+
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
27+
defer cancel()
28+
if _, err := net.DefaultResolver.LookupIPAddr(ctx, "get.helm.sh"); err != nil {
29+
// https://github.com/helm/helm/issues/31434
30+
t.Skipf("get.helm.sh seems down or unreachable; skipping test")
31+
}
32+
1833
base, err := os.Getwd()
1934
base = filepath.Join(base, "../../../")
2035
if err != nil {
2136
t.Fatalf("error getting current working directory: %v", err)
2237
}
2338
defer cleanup(base)
39+
40+
helmCLIPath := filepath.Join(base, "tool/helm")
41+
if out, err := exec.Command(helmCLIPath, "version").CombinedOutput(); err != nil && cibuild.On() {
42+
// It's not just DNS. Azure is generating bogus certs within GitHub Actions at least for
43+
// helm. So try to run it and see if we can even fetch it.
44+
//
45+
// https://github.com/helm/helm/issues/31434
46+
t.Skipf("error fetching helm; skipping test in CI: %v, %s", err, out)
47+
}
48+
2449
if err := generate(base); err != nil {
2550
t.Fatalf("CRD template generation: %v", err)
2651
}
2752

2853
tempDir := t.TempDir()
29-
helmCLIPath := filepath.Join(base, "tool/helm")
3054
helmChartTemplatesPath := filepath.Join(base, "cmd/k8s-operator/deploy/chart")
3155
helmPackageCmd := exec.Command(helmCLIPath, "package", helmChartTemplatesPath, "--destination", tempDir, "--version", "0.0.1")
3256
helmPackageCmd.Stderr = os.Stderr

cmd/tailscale/cli/configure-jetkvm.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,12 @@ func runConfigureJetKVM(ctx context.Context, args []string) error {
4848
if runtime.GOOS != "linux" || distro.Get() != distro.JetKVM {
4949
return errors.New("only implemented on JetKVM")
5050
}
51-
err := os.WriteFile("/etc/init.d/S22tailscale", bytes.TrimLeft([]byte(`
51+
if err := os.MkdirAll("/userdata/init.d", 0755); err != nil {
52+
return errors.New("unable to create /userdata/init.d")
53+
}
54+
err := os.WriteFile("/userdata/init.d/S22tailscale", bytes.TrimLeft([]byte(`
5255
#!/bin/sh
53-
# /etc/init.d/S22tailscale
56+
# /userdata/init.d/S22tailscale
5457
# Start/stop tailscaled
5558
5659
case "$1" in

control/controlclient/direct.go

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ import (
77
"bytes"
88
"cmp"
99
"context"
10-
"crypto"
11-
"crypto/sha256"
1210
"encoding/binary"
1311
"encoding/json"
1412
"errors"
@@ -948,26 +946,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
948946
ConnectionHandleForTest: connectionHandleForTest,
949947
}
950948

951-
// If we have a hardware attestation key, sign the node key with it and send
952-
// the key & signature in the map request.
953-
if buildfeatures.HasTPM {
954-
if k := persist.AsStruct().AttestationKey; k != nil && !k.IsZero() {
955-
hwPub := key.HardwareAttestationPublicFromPlatformKey(k)
956-
request.HardwareAttestationKey = hwPub
957-
958-
t := c.clock.Now()
959-
msg := fmt.Sprintf("%d|%s", t.Unix(), nodeKey.String())
960-
digest := sha256.Sum256([]byte(msg))
961-
sig, err := k.Sign(nil, digest[:], crypto.SHA256)
962-
if err != nil {
963-
c.logf("failed to sign node key with hardware attestation key: %v", err)
964-
} else {
965-
request.HardwareAttestationKeySignature = sig
966-
request.HardwareAttestationKeySignatureTimestamp = t
967-
}
968-
}
969-
}
970-
971949
var extraDebugFlags []string
972950
if buildfeatures.HasAdvertiseRoutes && hi != nil && c.netMon != nil && !c.skipIPForwardingCheck &&
973951
ipForwardingBroken(hi.RoutableIPs, c.netMon.InterfaceState()) {

feature/feature.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ var ErrUnavailable = errors.New("feature not included in this build")
1313

1414
var in = map[string]bool{}
1515

16+
// Registered reports the set of registered features.
17+
//
18+
// The returned map should not be modified by the caller,
19+
// not accessed concurrently with calls to Register.
20+
func Registered() map[string]bool { return in }
21+
1622
// Register notes that the named feature is linked into the binary.
1723
func Register(name string) {
1824
if _, ok := in[name]; ok {

feature/identityfederation/identityfederation.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,12 @@ func resolveAuthKey(ctx context.Context, baseURL, clientID, idToken string, tags
4242
baseURL = ipn.DefaultControlURL
4343
}
4444

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

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

82-
func parseOptionalAttributes(clientID string) (ephemeral bool, preauthorized bool, err error) {
83-
_, attrs, found := strings.Cut(clientID, "?")
82+
func parseOptionalAttributes(clientID string) (strippedID string, ephemeral bool, preauthorized bool, err error) {
83+
strippedID, attrs, found := strings.Cut(clientID, "?")
8484
if !found {
85-
return true, false, nil
85+
return clientID, true, false, nil
8686
}
8787

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

9393
for k := range parsed {
@@ -97,11 +97,14 @@ func parseOptionalAttributes(clientID string) (ephemeral bool, preauthorized boo
9797
case "preauthorized":
9898
preauthorized, err = strconv.ParseBool(parsed.Get(k))
9999
default:
100-
return false, false, fmt.Errorf("unknown optional config attribute %q", k)
100+
return "", false, false, fmt.Errorf("unknown optional config attribute %q", k)
101101
}
102102
}
103+
if err != nil {
104+
return "", false, false, err
105+
}
103106

104-
return ephemeral, preauthorized, err
107+
return strippedID, ephemeral, preauthorized, nil
105108
}
106109

107110
// exchangeJWTForToken exchanges a JWT for a Tailscale access token.

0 commit comments

Comments
 (0)