Compare commits

...

23 Commits

Author SHA1 Message Date
Nick Khyl 28f6c2dbfc
VERSION.txt: this is v1.90.6
Signed-off-by: Nick Khyl <nickk@tailscale.com>
1 month ago
M. J. Fromberger b6eabd4038 util/eventbus: allow logging of slow subscribers (#17705)
Add options to the eventbus.Bus to plumb in a logger.

Route that logger in to the subscriber machinery, and trigger a log message to
it when a subscriber fails to respond to its delivered events for 5s or more.

The log message includes the package, filename, and line number of the call
site that created the subscription.

Add tests that verify this works.

Updates #17680

Change-Id: I0546516476b1e13e6a9cf79f19db2fe55e56c698
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
(cherry picked from commit 061e6266cf)
1 month ago
M. J. Fromberger 6e2f2bb31a ipn/ipnlocal: do not stall event processing for appc route updates (#17663)
A follow-up to #17411. Put AppConnector events into a task queue, as they may
take some time to process. Ensure that the queue is stopped at shutdown so that
cleanup will remain orderly.

Because events are delivered on a separate goroutine, slow processing of an
event does not cause an immediate problem; however, a subscriber that blocks
for a long time will push back on the bus as a whole. See
https://godoc.org/tailscale.com/util/eventbus#hdr-Expected_subscriber_behavior
for more discussion.

Updates #17192
Updates #15160

Change-Id: Ib313cc68aec273daf2b1ad79538266c81ef063e3
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
(cherry picked from commit 06b092388e)
1 month ago
Alex Chan faca4c08b7
.github/workflows: pin the google/oss-fuzz GitHub Actions
Updates https://github.com/tailscale/corp/issues/31017

Signed-off-by: Alex Chan <alexc@tailscale.com>
(cherry picked from commit 3944809a11)
1 month ago
Nick Khyl 63242007ae
VERSION.txt: this is v1.90.5
Signed-off-by: Nick Khyl <nickk@tailscale.com>
1 month ago
Brad Fitzpatrick 300e6062bf cmd/k8s-operator/generate: skip tests if no network or Helm is down
Updates helm/helm#31434

Change-Id: I5eb20e97ff543f883d5646c9324f50f54180851d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit d5a40c01ab)
1 month ago
Brad Fitzpatrick 1a6c31538e sessionrecording: fix regression in recent http2 package change
In 3f5c560fd4 I changed to use std net/http's HTTP/2 support,
instead of pulling in x/net/http2.

But I forgot to update DialTLSContext to DialContext, which meant it
was falling back to using the std net.Dialer for its dials, instead
of the passed-in one.

The tests only passed because they were using localhost addresses, so
the std net.Dialer worked. But in prod, where a tsnet Dialer would be
needed, it didn't work, and would time out for 10 seconds before
resorting to the old protocol.

So this fixes the tests to use an isolated in-memory network to prevent
that class of problem in the future. With the test change, the old code
fails and the new code passes.

Thanks to @jasonodonnell for debugging!

Updates #17304
Updates 3f5c560fd4

Change-Id: I3602bafd07dc6548e2c62985af9ac0afb3a0e967
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 8996254647)
1 month ago
Nick Khyl 68cba300e4
VERSION.txt: this is v1.90.4
Signed-off-by: Nick Khyl <nickk@tailscale.com>
1 month ago
M. J. Fromberger 2dd72f6ec2
Revert "logtail: avoid racing eventbus subscriptions with Shutdown (#17639)" (#17684)
This reverts commit 4346615d77.
We averted the shutdown race, but will need to service the subscriber even when
we are not waiting for a change so that we do not delay the bus as a whole.

Updates #17638

Change-Id: I5488466ed83f5ad1141c95267f5ae54878a24657
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
(cherry picked from commit db5815fb97)
1 month ago
Brad Fitzpatrick 53004dded1 wgengine/magicsock: fix js/wasm crash regression loading non-existent portmapper
Thanks for the report, @Need-an-AwP!

Fixes #17681
Updates #9394

Change-Id: I2e0b722ef9b460bd7e79499192d1a315504ca84c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit edb11e0e60)
1 month ago
srwareham 033adc398c cmd/tailscale/cli: move JetKVM scripts to /userdata/init.d for persistence (#17610)
Updates #16524
Updates jetkvm/rv1106-system#34

Signed-off-by: srwareham <ebriouscoding@gmail.com>
(cherry picked from commit f4e2720821)
1 month ago
Max Coulombe bad03eefa1 feature/identityfederation: strip query params on clientID (#17666)
Updates #9192

Change-Id: I35c88df8a0242ecc19a23265d392ef78ac176b9d
Signed-off-by: mcoulombe <max@tailscale.com>
(cherry picked from commit 34e992f59d)
1 month ago
Patrick O'Doherty dc3c15b4c6
control/controlclient: back out HW key attestation (#17664)
Temporarily back out the TPM-based hw attestation code while we debug
Windows exceptions.

Updates tailscale/corp#31269

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
(cherry picked from commit a760cbe33f)
1 month ago
Nick Khyl c50fe71822
VERSION.txt: this is v1.90.3
Signed-off-by: Nick Khyl <nickk@tailscale.com>
1 month ago
M. J. Fromberger 597acd8663
logtail: avoid racing eventbus subscriptions with Shutdown (#17639)
When the eventbus is enabled, set up the subscription for change deltas at the
beginning when the client is created, rather than waiting for the first
awaitInternetUp check.

Otherwise, it is possible for a check to race with the client close in
Shutdown, which triggers a panic.

Updates #17638

Change-Id: I461c07939eca46699072b14b1814ecf28eec750c
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
(cherry picked from commit 4346615d77)
1 month ago
Claus Lensbøl e6a3669277
net/tsdial: do not panic if setting the same eventbus twice (#17640)
Updates #17638

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
(cherry picked from commit fd0e541e5d)
1 month ago
Nick Khyl 8bcd44ecf0
VERSION.txt: this is v1.90.2
Signed-off-by: Nick Khyl <nickk@tailscale.com>
1 month ago
Claus Lensbøl b0f0bce928 health: compare warnable codes to avoid errors on release branch (#17637)
This compares the warnings we actually care about and skips the unstable
warnings and the changes with no warnings.

Fixes #17635

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
(cherry picked from commit 7418583e47)
1 month ago
Brad Fitzpatrick c81ef9055b util/linuxfw: fix 32-bit arm regression with iptables
This fixes a regression from dd615c8fdd that moved the
newIPTablesRunner constructor from a any-Linux-GOARCH file to one that
was only amd64 and arm64, thus breaking iptables on other platforms
(notably 32-bit "arm", as seen on older Pis running Buster with
iptables)

Tested by hand on a Raspberry Pi 2 w/ Buster + iptables for now, for
lack of automated 32-bit arm tests at the moment. But filed #17629.

Fixes #17623
Updates #17629

Change-Id: Iac1a3d78f35d8428821b46f0fed3f3717891c1bd
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 8576a802ca)
1 month ago
Patrick O'Doherty 9fe44b3718 feature/tpm: use withSRK to probe TPM availability (#17627)
On some platforms e.g. ChromeOS the owner hierarchy might not always be
available to us. To avoid stale sealing exceptions later we probe to
confirm it's working rather than rely solely on family indicator status.

Updates #17622

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
(cherry picked from commit 672b1f0e76)
1 month ago
Patrick O'Doherty a8ae316858 feature/tpm: check TPM family data for compatibility (#17624)
Check that the TPM we have opened is advertised as a 2.0 family device
before using it for state sealing / hardware attestation.

Updates #17622

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
(cherry picked from commit 36ad24b20f)
1 month ago
Nick Khyl 75b0c6f164 VERSION.txt: this is v1.90.1
Signed-off-by: Nick Khyl <nickk@tailscale.com>
1 month ago
Nick Khyl 3c78146ece VERSION.txt: this is v1.90.0
Signed-off-by: Nick Khyl <nickk@tailscale.com>
1 month ago

@ -613,7 +613,9 @@ jobs:
steps:
- name: build fuzzers
id: build
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master
# As of 21 October 2025, this repo doesn't tag releases, so this commit
# hash is just the tip of master.
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@1242ccb5b6352601e73c00f189ac2ae397242264
# continue-on-error makes steps.build.conclusion be 'success' even if
# steps.build.outcome is 'failure'. This means this step does not
# contribute to the job's overall pass/fail evaluation.
@ -643,7 +645,9 @@ jobs:
# report a failure because TS_FUZZ_CURRENTLY_BROKEN is set to the wrong
# value.
if: steps.build.outcome == 'success'
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
# As of 21 October 2025, this repo doesn't tag releases, so this commit
# hash is just the tip of master.
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@1242ccb5b6352601e73c00f189ac2ae397242264
with:
oss-fuzz-project-name: 'tailscale'
fuzz-seconds: 150

@ -1 +1 @@
1.89.0
1.90.6

@ -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 {

@ -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
}

@ -116,7 +116,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/syncs from tailscale.com/cmd/derper+
tailscale.com/tailcfg from tailscale.com/client/local+
tailscale.com/tka from tailscale.com/client/local+
LW tailscale.com/tsconst from tailscale.com/net/netmon+
tailscale.com/tsconst from tailscale.com/net/netmon+
tailscale.com/tstime from tailscale.com/derp+
tailscale.com/tstime/mono from tailscale.com/tstime/rate
tailscale.com/tstime/rate from tailscale.com/derp/derpserver

@ -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

@ -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

@ -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

@ -116,7 +116,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/tailcfg from tailscale.com/client/tailscale/apitype+
tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock
tailscale.com/tka from tailscale.com/control/controlclient+
tailscale.com/tsconst from tailscale.com/net/netns
tailscale.com/tsconst from tailscale.com/net/netns+
tailscale.com/tsd from tailscale.com/cmd/tailscaled+
tailscale.com/tstime from tailscale.com/control/controlclient+
tailscale.com/tstime/mono from tailscale.com/net/tstun+

@ -7,8 +7,6 @@ import (
"bytes"
"cmp"
"context"
"crypto"
"crypto/sha256"
"encoding/binary"
"encoding/json"
"errors"
@ -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()) {

@ -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 {

@ -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)
}
@ -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 {
@ -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.

@ -87,6 +87,7 @@ func TestParseOptionalAttributes(t *testing.T) {
tests := []struct {
name string
clientID string
wantClientID string
wantEphemeral bool
wantPreauth bool
wantErr string
@ -94,6 +95,7 @@ func TestParseOptionalAttributes(t *testing.T) {
{
name: "default values",
clientID: "client-123",
wantClientID: "client-123",
wantEphemeral: true,
wantPreauth: false,
wantErr: "",
@ -101,6 +103,7 @@ func TestParseOptionalAttributes(t *testing.T) {
{
name: "custom values",
clientID: "client-123?ephemeral=false&preauthorized=true",
wantClientID: "client-123",
wantEphemeral: false,
wantPreauth: true,
wantErr: "",
@ -108,6 +111,7 @@ func TestParseOptionalAttributes(t *testing.T) {
{
name: "unknown attribute",
clientID: "client-123?unknown=value",
wantClientID: "",
wantEphemeral: false,
wantPreauth: false,
wantErr: `unknown optional config attribute "unknown"`,
@ -115,6 +119,7 @@ func TestParseOptionalAttributes(t *testing.T) {
{
name: "invalid value",
clientID: "client-123?ephemeral=invalid",
wantClientID: "",
wantEphemeral: false,
wantPreauth: false,
wantErr: `strconv.ParseBool: parsing "invalid": invalid syntax`,
@ -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)
@ -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)
}

@ -6,6 +6,7 @@
package portmapper
import (
"tailscale.com/feature"
"tailscale.com/net/netmon"
"tailscale.com/net/portmapper"
"tailscale.com/net/portmapper/portmappertype"
@ -14,6 +15,7 @@ import (
)
func init() {
feature.Register("portmapper")
portmappertype.HookNewPortMapper.Set(newPortMapper)
}

@ -55,11 +55,25 @@ func init() {
}
func tpmSupported() bool {
hi := infoOnce()
if hi == nil {
return false
}
if hi.FamilyIndicator != "2.0" {
return false
}
tpm, err := open()
if err != nil {
return false
}
tpm.Close()
defer tpm.Close()
if err := withSRK(logger.Discard, tpm, func(srk tpm2.AuthHandle) error {
return nil
}); err != nil {
return false
}
return true
}
@ -104,6 +118,7 @@ func info() *tailcfg.TPMInfo {
{tpm2.TPMPTVendorTPMType, func(info *tailcfg.TPMInfo, value uint32) { info.Model = int(value) }},
{tpm2.TPMPTFirmwareVersion1, func(info *tailcfg.TPMInfo, value uint32) { info.FirmwareVersion += uint64(value) << 32 }},
{tpm2.TPMPTFirmwareVersion2, func(info *tailcfg.TPMInfo, value uint32) { info.FirmwareVersion += uint64(value) }},
{tpm2.TPMPTFamilyIndicator, toStr(&info.FamilyIndicator)},
} {
resp, err := tpm2.GetCapability{
Capability: tpm2.TPMCapTPMProperties,

@ -133,6 +133,31 @@ func TestStore(t *testing.T) {
})
}
func BenchmarkInfo(b *testing.B) {
b.StopTimer()
skipWithoutTPM(b)
b.StartTimer()
for i := 0; i < b.N; i++ {
hi := info()
if hi == nil {
b.Fatalf("tpm info error")
}
}
b.StopTimer()
}
func BenchmarkTPMSupported(b *testing.B) {
b.StopTimer()
skipWithoutTPM(b)
b.StartTimer()
for i := 0; i < b.N; i++ {
if !tpmSupported() {
b.Fatalf("tpmSupported returned false")
}
}
b.StopTimer()
}
func BenchmarkStore(b *testing.B) {
skipWithoutTPM(b)
b.StopTimer()

@ -151,5 +151,5 @@
});
};
}
# nix-direnv cache busting line: sha256-rV3C2Vi48FCifGt58OdEO4+Av0HRIs8sUJVvp/gEBLw=
# nix-direnv cache busting line: sha256-AUOjLomba75qfzb9Vxc0Sktyeces6hBSuOMgboWcDnE=

@ -42,7 +42,7 @@ require (
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
github.com/golang/snappy v0.0.4
github.com/golangci/golangci-lint v1.57.1
github.com/google/go-cmp v0.6.0
github.com/google/go-cmp v0.7.0
github.com/google/go-containerregistry v0.20.3
github.com/google/go-tpm v0.9.4
github.com/google/gopacket v1.1.19

@ -1 +1 @@
sha256-rV3C2Vi48FCifGt58OdEO4+Av0HRIs8sUJVvp/gEBLw=
sha256-AUOjLomba75qfzb9Vxc0Sktyeces6hBSuOMgboWcDnE=

@ -490,8 +490,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI=
github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI=
github.com/google/go-github/v66 v66.0.0 h1:ADJsaXj9UotwdgK8/iFZtv7MLc8E8WBl62WLd/D/9+M=

@ -19,6 +19,7 @@ import (
"github.com/google/go-cmp/cmp/cmpopts"
"tailscale.com/metrics"
"tailscale.com/tailcfg"
"tailscale.com/tsconst"
"tailscale.com/tstest"
"tailscale.com/tstime"
"tailscale.com/types/opt"
@ -739,21 +740,27 @@ func TestControlHealthNotifies(t *testing.T) {
ht.SetIPNState("NeedsLogin", true)
ht.GotStreamedMapResponse()
// Expect events at starup, before doing anything else
// Expect events at starup, before doing anything else, skip unstable
// event and no warning event as they show up at different times.
synctest.Wait()
if err := eventbustest.ExpectExactly(tw,
eventbustest.Type[Change](), // warming-up
eventbustest.Type[Change](), // is-using-unstable-version
eventbustest.Type[Change](), // not-in-map-poll
if err := eventbustest.Expect(tw,
CompareWarnableCode(t, tsconst.HealthWarnableWarmingUp),
CompareWarnableCode(t, tsconst.HealthWarnableNotInMapPoll),
CompareWarnableCode(t, tsconst.HealthWarnableWarmingUp),
); err != nil {
t.Errorf("startup error: %v", err)
}
// Only set initial state if we need to
if len(test.initialState) != 0 {
t.Log("Setting initial state")
ht.SetControlHealth(test.initialState)
synctest.Wait()
if err := eventbustest.ExpectExactly(tw, eventbustest.Type[Change]()); err != nil {
if err := eventbustest.Expect(tw,
CompareWarnableCode(t, tsconst.HealthWarnableMagicsockReceiveFuncError),
// Skip event with no warnable
CompareWarnableCode(t, tsconst.HealthWarnableNoDERPHome),
); err != nil {
t.Errorf("initial state error: %v", err)
}
}
@ -771,6 +778,22 @@ func TestControlHealthNotifies(t *testing.T) {
}
}
func CompareWarnableCode(t *testing.T, code string) func(Change) bool {
t.Helper()
return func(c Change) bool {
t.Helper()
if c.Warnable != nil {
t.Logf("Warnable code: %s", c.Warnable.Code)
if string(c.Warnable.Code) == code {
return true
}
} else {
t.Log("No Warnable")
}
return false
}
}
func TestControlHealthIgnoredOutsideMapPoll(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
bus := eventbustest.NewBus(t)

@ -9,6 +9,7 @@ import (
"time"
"tailscale.com/feature/buildfeatures"
"tailscale.com/tsconst"
"tailscale.com/version"
)
@ -26,7 +27,7 @@ This file contains definitions for the Warnables maintained within this `health`
// updateAvailableWarnable is a Warnable that warns the user that an update is available.
var updateAvailableWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: "update-available",
Code: tsconst.HealthWarnableUpdateAvailable,
Title: "Update available",
Severity: SeverityLow,
Text: func(args Args) string {
@ -42,7 +43,7 @@ var updateAvailableWarnable = condRegister(func() *Warnable {
// securityUpdateAvailableWarnable is a Warnable that warns the user that an important security update is available.
var securityUpdateAvailableWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: "security-update-available",
Code: tsconst.HealthWarnableSecurityUpdateAvailable,
Title: "Security update available",
Severity: SeverityMedium,
Text: func(args Args) string {
@ -59,7 +60,7 @@ var securityUpdateAvailableWarnable = condRegister(func() *Warnable {
// so they won't be surprised by all the issues that may arise.
var unstableWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: "is-using-unstable-version",
Code: tsconst.HealthWarnableIsUsingUnstableVersion,
Title: "Using an unstable version",
Severity: SeverityLow,
Text: StaticMessage("This is an unstable version of Tailscale meant for testing and development purposes. Please report any issues to Tailscale."),
@ -69,7 +70,7 @@ var unstableWarnable = condRegister(func() *Warnable {
// NetworkStatusWarnable is a Warnable that warns the user that the network is down.
var NetworkStatusWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: "network-status",
Code: tsconst.HealthWarnableNetworkStatus,
Title: "Network down",
Severity: SeverityMedium,
Text: StaticMessage("Tailscale cannot connect because the network is down. Check your Internet connection."),
@ -81,7 +82,7 @@ var NetworkStatusWarnable = condRegister(func() *Warnable {
// IPNStateWarnable is a Warnable that warns the user that Tailscale is stopped.
var IPNStateWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: "wantrunning-false",
Code: tsconst.HealthWarnableWantRunningFalse,
Title: "Tailscale off",
Severity: SeverityLow,
Text: StaticMessage("Tailscale is stopped."),
@ -91,7 +92,7 @@ var IPNStateWarnable = condRegister(func() *Warnable {
// localLogWarnable is a Warnable that warns the user that the local log is misconfigured.
var localLogWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: "local-log-config-error",
Code: tsconst.HealthWarnableLocalLogConfigError,
Title: "Local log misconfiguration",
Severity: SeverityLow,
Text: func(args Args) string {
@ -104,7 +105,7 @@ var localLogWarnable = condRegister(func() *Warnable {
// and provides the last login error if available.
var LoginStateWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: "login-state",
Code: tsconst.HealthWarnableLoginState,
Title: "Logged out",
Severity: SeverityMedium,
Text: func(args Args) string {
@ -121,7 +122,7 @@ var LoginStateWarnable = condRegister(func() *Warnable {
// notInMapPollWarnable is a Warnable that warns the user that we are using a stale network map.
var notInMapPollWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: "not-in-map-poll",
Code: tsconst.HealthWarnableNotInMapPoll,
Title: "Out of sync",
Severity: SeverityMedium,
DependsOn: []*Warnable{NetworkStatusWarnable, IPNStateWarnable},
@ -134,7 +135,7 @@ var notInMapPollWarnable = condRegister(func() *Warnable {
// noDERPHomeWarnable is a Warnable that warns the user that Tailscale doesn't have a home DERP.
var noDERPHomeWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: "no-derp-home",
Code: tsconst.HealthWarnableNoDERPHome,
Title: "No home relay server",
Severity: SeverityMedium,
DependsOn: []*Warnable{NetworkStatusWarnable},
@ -147,7 +148,7 @@ var noDERPHomeWarnable = condRegister(func() *Warnable {
// noDERPConnectionWarnable is a Warnable that warns the user that Tailscale couldn't connect to a specific DERP server.
var noDERPConnectionWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: "no-derp-connection",
Code: tsconst.HealthWarnableNoDERPConnection,
Title: "Relay server unavailable",
Severity: SeverityMedium,
DependsOn: []*Warnable{
@ -177,7 +178,7 @@ var noDERPConnectionWarnable = condRegister(func() *Warnable {
// heard from the home DERP region for a while.
var derpTimeoutWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: "derp-timed-out",
Code: tsconst.HealthWarnableDERPTimedOut,
Title: "Relay server timed out",
Severity: SeverityMedium,
DependsOn: []*Warnable{
@ -198,7 +199,7 @@ var derpTimeoutWarnable = condRegister(func() *Warnable {
// derpRegionErrorWarnable is a Warnable that warns the user that a DERP region is reporting an issue.
var derpRegionErrorWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: "derp-region-error",
Code: tsconst.HealthWarnableDERPRegionError,
Title: "Relay server error",
Severity: SeverityLow,
DependsOn: []*Warnable{NetworkStatusWarnable},
@ -211,7 +212,7 @@ var derpRegionErrorWarnable = condRegister(func() *Warnable {
// noUDP4BindWarnable is a Warnable that warns the user that Tailscale couldn't listen for incoming UDP connections.
var noUDP4BindWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: "no-udp4-bind",
Code: tsconst.HealthWarnableNoUDP4Bind,
Title: "NAT traversal setup failure",
Severity: SeverityMedium,
DependsOn: []*Warnable{NetworkStatusWarnable, IPNStateWarnable},
@ -223,7 +224,7 @@ var noUDP4BindWarnable = condRegister(func() *Warnable {
// mapResponseTimeoutWarnable is a Warnable that warns the user that Tailscale hasn't received a network map from the coordination server in a while.
var mapResponseTimeoutWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: "mapresponse-timeout",
Code: tsconst.HealthWarnableMapResponseTimeout,
Title: "Network map response timeout",
Severity: SeverityMedium,
DependsOn: []*Warnable{NetworkStatusWarnable, IPNStateWarnable},
@ -236,7 +237,7 @@ var mapResponseTimeoutWarnable = condRegister(func() *Warnable {
// tlsConnectionFailedWarnable is a Warnable that warns the user that Tailscale could not establish an encrypted connection with a server.
var tlsConnectionFailedWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: "tls-connection-failed",
Code: tsconst.HealthWarnableTLSConnectionFailed,
Title: "Encrypted connection failed",
Severity: SeverityMedium,
DependsOn: []*Warnable{NetworkStatusWarnable},
@ -249,7 +250,7 @@ var tlsConnectionFailedWarnable = condRegister(func() *Warnable {
// magicsockReceiveFuncWarnable is a Warnable that warns the user that one of the Magicsock functions is not running.
var magicsockReceiveFuncWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: "magicsock-receive-func-error",
Code: tsconst.HealthWarnableMagicsockReceiveFuncError,
Title: "MagicSock function not running",
Severity: SeverityMedium,
Text: func(args Args) string {
@ -261,7 +262,7 @@ var magicsockReceiveFuncWarnable = condRegister(func() *Warnable {
// testWarnable is a Warnable that is used within this package for testing purposes only.
var testWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: "test-warnable",
Code: tsconst.HealthWarnableTestWarnable,
Title: "Test warnable",
Severity: SeverityLow,
Text: func(args Args) string {
@ -273,7 +274,7 @@ var testWarnable = condRegister(func() *Warnable {
// applyDiskConfigWarnable is a Warnable that warns the user that there was an error applying the envknob config stored on disk.
var applyDiskConfigWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: "apply-disk-config",
Code: tsconst.HealthWarnableApplyDiskConfig,
Title: "Could not apply configuration",
Severity: SeverityMedium,
Text: func(args Args) string {
@ -291,7 +292,7 @@ const warmingUpWarnableDuration = 5 * time.Second
// the backend is fully started.
var warmingUpWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: "warming-up",
Code: tsconst.HealthWarnableWarmingUp,
Title: "Tailscale is starting",
Severity: SeverityLow,
Text: StaticMessage("Tailscale is starting. Please wait."),

@ -384,6 +384,7 @@ func TestRedactNetmapPrivateKeys(t *testing.T) {
f(tailcfg.Service{}, "Port"): false,
f(tailcfg.Service{}, "Proto"): false,
f(tailcfg.Service{}, "_"): false,
f(tailcfg.TPMInfo{}, "FamilyIndicator"): false,
f(tailcfg.TPMInfo{}, "FirmwareVersion"): false,
f(tailcfg.TPMInfo{}, "Manufacturer"): false,
f(tailcfg.TPMInfo{}, "Model"): false,

@ -1,48 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_tpm
package ipnlocal
import (
"errors"
"tailscale.com/feature"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/persist"
)
func init() {
feature.HookGenerateAttestationKeyIfEmpty.Set(generateAttestationKeyIfEmpty)
}
// generateAttestationKeyIfEmpty generates a new hardware attestation key if
// none exists. It returns true if a new key was generated and stored in
// p.AttestationKey.
func generateAttestationKeyIfEmpty(p *persist.Persist, logf logger.Logf) (bool, error) {
// attempt to generate a new hardware attestation key if none exists
var ak key.HardwareAttestationKey
if p != nil {
ak = p.AttestationKey
}
if ak == nil || ak.IsZero() {
var err error
ak, err = key.NewHardwareAttestationKey()
if err != nil {
if !errors.Is(err, key.ErrUnsupported) {
logf("failed to create hardware attestation key: %v", err)
}
} else if ak != nil {
logf("using new hardware attestation key: %v", ak.Public())
if p == nil {
p = &persist.Persist{}
}
p.AttestationKey = ak
return true, nil
}
}
return false, nil
}

@ -87,6 +87,7 @@ import (
"tailscale.com/util/clientmetric"
"tailscale.com/util/dnsname"
"tailscale.com/util/eventbus"
"tailscale.com/util/execqueue"
"tailscale.com/util/goroutines"
"tailscale.com/util/mak"
"tailscale.com/util/osuser"
@ -187,6 +188,7 @@ type LocalBackend struct {
statsLogf logger.Logf // for printing peers stats on change
sys *tsd.System
eventSubs eventbus.Monitor
appcTask execqueue.ExecQueue // handles updates from appc
health *health.Tracker // always non-nil
polc policyclient.Client // always non-nil
@ -642,12 +644,14 @@ func (b *LocalBackend) consumeEventbusTopics(ec *eventbus.Client) func(*eventbus
// We need to find a way to ensure that changes to the backend state are applied
// consistently in the presnce of profile changes, which currently may not happen in
// a single atomic step. See: https://github.com/tailscale/tailscale/issues/17414
if err := b.AdvertiseRoute(ru.Advertise...); err != nil {
b.logf("appc: failed to advertise routes: %v: %v", ru.Advertise, err)
}
if err := b.UnadvertiseRoute(ru.Unadvertise...); err != nil {
b.logf("appc: failed to unadvertise routes: %v: %v", ru.Unadvertise, err)
}
b.appcTask.Add(func() {
if err := b.AdvertiseRoute(ru.Advertise...); err != nil {
b.logf("appc: failed to advertise routes: %v: %v", ru.Advertise, err)
}
if err := b.UnadvertiseRoute(ru.Unadvertise...); err != nil {
b.logf("appc: failed to unadvertise routes: %v: %v", ru.Unadvertise, err)
}
})
case ri := <-storeRoutesSub.Events():
// Whether or not routes should be stored can change over time.
shouldStoreRoutes := b.ControlKnobs().AppCStoreRoutes.Load()
@ -1113,6 +1117,7 @@ func (b *LocalBackend) Shutdown() {
// they can deadlock with c.Shutdown().
// 2. LocalBackend.consumeEventbusTopics event handlers may not guard against
// undesirable post/in-progress LocalBackend.Shutdown() behaviors.
b.appcTask.Shutdown()
b.eventSubs.Close()
b.em.close()
@ -1216,7 +1221,6 @@ func stripKeysFromPrefs(p ipn.PrefsView) ipn.PrefsView {
p2.Persist.PrivateNodeKey = key.NodePrivate{}
p2.Persist.OldPrivateNodeKey = key.NodePrivate{}
p2.Persist.NetworkLockKey = key.NLPrivate{}
p2.Persist.AttestationKey = nil
return p2.View()
}

@ -19,9 +19,7 @@ import (
"tailscale.com/ipn"
"tailscale.com/ipn/ipnext"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/persist"
"tailscale.com/util/clientmetric"
"tailscale.com/util/eventbus"
)
@ -656,14 +654,6 @@ func (pm *profileManager) loadSavedPrefs(k ipn.StateKey) (ipn.PrefsView, error)
return ipn.PrefsView{}, err
}
savedPrefs := ipn.NewPrefs()
// if supported by the platform, create an empty hardware attestation key to use when deserializing
// to avoid type exceptions from json.Unmarshaling into an interface{}.
hw, _ := key.NewEmptyHardwareAttestationKey()
savedPrefs.Persist = &persist.Persist{
AttestationKey: hw,
}
if err := ipn.PrefsFromBytes(bs, savedPrefs); err != nil {
return ipn.PrefsView{}, fmt.Errorf("parsing saved prefs: %v", err)
}

@ -151,7 +151,6 @@ func TestProfileDupe(t *testing.T) {
ID: tailcfg.UserID(user),
LoginName: fmt.Sprintf("user%d@example.com", user),
},
AttestationKey: nil,
}
}
user1Node1 := newPersist(1, 1)

@ -19,6 +19,7 @@ import (
"sync"
"time"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/feature"
"tailscale.com/feature/buildfeatures"
"tailscale.com/ipn"
@ -39,6 +40,7 @@ func init() {
Register("debug-packet-filter-matches", (*Handler).serveDebugPacketFilterMatches)
Register("debug-packet-filter-rules", (*Handler).serveDebugPacketFilterRules)
Register("debug-peer-endpoint-changes", (*Handler).serveDebugPeerEndpointChanges)
Register("debug-optional-features", (*Handler).serveDebugOptionalFeatures)
}
func (h *Handler) serveDebugPeerEndpointChanges(w http.ResponseWriter, r *http.Request) {
@ -463,3 +465,11 @@ func (h *Handler) serveDebugLog(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}
func (h *Handler) serveDebugOptionalFeatures(w http.ResponseWriter, r *http.Request) {
of := &apitype.OptionalFeatures{
Features: feature.Registered(),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(of)
}

@ -501,7 +501,7 @@ func TestPrefsPretty(t *testing.T) {
},
},
"linux",
`Prefs{ra=false dns=false want=false routes=[] nf=off update=off Persist{o=, n=[B1VKl] u="" ak=-}}`,
`Prefs{ra=false dns=false want=false routes=[] nf=off update=off Persist{o=, n=[B1VKl] u=""}}`,
},
{
Prefs{

@ -96,6 +96,7 @@ type Dialer struct {
dnsCache *dnscache.MessageCache // nil until first non-empty SetExitDNSDoH
nextSysConnID int
activeSysConns map[int]net.Conn // active connections not yet closed
bus *eventbus.Bus // only used for comparison with already set bus.
eventClient *eventbus.Client
eventBusSubs eventbus.Monitor
}
@ -226,14 +227,17 @@ func (d *Dialer) NetMon() *netmon.Monitor {
func (d *Dialer) SetBus(bus *eventbus.Bus) {
d.mu.Lock()
defer d.mu.Unlock()
if d.eventClient != nil {
panic("eventbus has already been set")
if d.bus == bus {
return
} else if d.bus != nil {
panic("different eventbus has already been set")
}
// Having multiple watchers could lead to problems,
// so unregister the callback if it exists.
if d.netMonUnregister != nil {
d.netMonUnregister()
}
d.bus = bus
d.eventClient = bus.Client("tsdial.Dialer")
d.eventBusSubs = d.eventClient.Monitor(d.linkChangeWatcher(d.eventClient))
}

@ -405,10 +405,7 @@ func clientHTTP2(dialCtx context.Context, dial netx.DialFunc) *http.Client {
return &http.Client{
Transport: &http.Transport{
Protocols: &p,
// Pretend like we're using TLS, but actually use the provided
// DialFunc underneath. This is necessary to convince the transport
// to actually dial.
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
perAttemptCtx, cancel := context.WithTimeout(ctx, perDialAttemptTimeout)
defer cancel()
go func() {

@ -21,6 +21,7 @@ import (
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"tailscale.com/net/memnet"
)
func TestConnectToRecorder(t *testing.T) {
@ -145,7 +146,14 @@ func TestConnectToRecorder(t *testing.T) {
t.Run(tt.desc, func(t *testing.T) {
mux, uploadHash := tt.setup(t)
srv := httptest.NewUnstartedServer(mux)
memNet := &memnet.Network{}
ln := memNet.NewLocalTCPListener()
srv := &httptest.Server{
Config: &http.Server{Handler: mux},
Listener: ln,
}
if tt.http2 {
// Wire up h2c-compatible HTTP/2 server. This is optional
// because the v1 recorder didn't support HTTP/2 and we try to
@ -159,10 +167,8 @@ func TestConnectToRecorder(t *testing.T) {
srv.Start()
t.Cleanup(srv.Close)
d := new(net.Dialer)
ctx := context.Background()
w, _, errc, err := ConnectToRecorder(ctx, []netip.AddrPort{netip.MustParseAddrPort(srv.Listener.Addr().String())}, d.DialContext)
w, _, errc, err := ConnectToRecorder(ctx, []netip.AddrPort{netip.MustParseAddrPort(ln.Addr().String())}, memNet.Dial)
if err != nil {
t.Fatalf("ConnectToRecorder: %v", err)
}

@ -16,4 +16,4 @@
) {
src = ./.;
}).shellNix
# nix-direnv cache busting line: sha256-rV3C2Vi48FCifGt58OdEO4+Av0HRIs8sUJVvp/gEBLw=
# nix-direnv cache busting line: sha256-AUOjLomba75qfzb9Vxc0Sktyeces6hBSuOMgboWcDnE=

@ -928,6 +928,10 @@ type TPMInfo struct {
// https://trustedcomputinggroup.org/resource/tpm-library-specification/.
// Before revision 184, TCG used the "01.83" format for revision 183.
SpecRevision int `json:",omitempty"`
// FamilyIndicator is the TPM spec family, like "2.0".
// Read from TPM_PT_FAMILY_INDICATOR.
FamilyIndicator string `json:",omitempty"`
}
// Present reports whether a TPM device is present on this machine.

@ -0,0 +1,26 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package tsconst
const (
HealthWarnableUpdateAvailable = "update-available"
HealthWarnableSecurityUpdateAvailable = "security-update-available"
HealthWarnableIsUsingUnstableVersion = "is-using-unstable-version"
HealthWarnableNetworkStatus = "network-status"
HealthWarnableWantRunningFalse = "wantrunning-false"
HealthWarnableLocalLogConfigError = "local-log-config-error"
HealthWarnableLoginState = "login-state"
HealthWarnableNotInMapPoll = "not-in-map-poll"
HealthWarnableNoDERPHome = "no-derp-home"
HealthWarnableNoDERPConnection = "no-derp-connection"
HealthWarnableDERPTimedOut = "derp-timed-out"
HealthWarnableDERPRegionError = "derp-region-error"
HealthWarnableNoUDP4Bind = "no-udp4-bind"
HealthWarnableMapResponseTimeout = "mapresponse-timeout"
HealthWarnableTLSConnectionFailed = "tls-connection-failed"
HealthWarnableMagicsockReceiveFuncError = "magicsock-receive-func-error"
HealthWarnableTestWarnable = "test-warnable"
HealthWarnableApplyDiskConfig = "apply-disk-config"
HealthWarnableWarmingUp = "warming-up"
)

@ -175,6 +175,28 @@ func TestControlKnobs(t *testing.T) {
}
}
func TestExpectedFeaturesLinked(t *testing.T) {
tstest.Shard(t)
tstest.Parallel(t)
env := NewTestEnv(t)
n1 := NewTestNode(t, env)
d1 := n1.StartDaemon()
n1.AwaitResponding()
lc := n1.LocalClient()
got, err := lc.QueryOptionalFeatures(t.Context())
if err != nil {
t.Fatal(err)
}
if !got.Features["portmapper"] {
t.Errorf("optional feature portmapper unexpectedly not found: got %v", got.Features)
}
d1.MustCleanShutdown(t)
t.Logf("number of HTTP logcatcher requests: %v", env.LogCatcher.numRequests())
}
func TestCollectPanic(t *testing.T) {
flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/15865")
tstest.Shard(t)

@ -26,7 +26,6 @@ type Persist struct {
UserProfile tailcfg.UserProfile
NetworkLockKey key.NLPrivate
NodeID tailcfg.StableNodeID
AttestationKey key.HardwareAttestationKey `json:",omitempty"`
// DisallowedTKAStateIDs stores the tka.State.StateID values which
// this node will not operate network lock on. This is used to
@ -85,20 +84,11 @@ func (p *Persist) Equals(p2 *Persist) bool {
return false
}
var pub, p2Pub key.HardwareAttestationPublic
if p.AttestationKey != nil && !p.AttestationKey.IsZero() {
pub = key.HardwareAttestationPublicFromPlatformKey(p.AttestationKey)
}
if p2.AttestationKey != nil && !p2.AttestationKey.IsZero() {
p2Pub = key.HardwareAttestationPublicFromPlatformKey(p2.AttestationKey)
}
return p.PrivateNodeKey.Equal(p2.PrivateNodeKey) &&
p.OldPrivateNodeKey.Equal(p2.OldPrivateNodeKey) &&
p.UserProfile.Equal(&p2.UserProfile) &&
p.NetworkLockKey.Equal(p2.NetworkLockKey) &&
p.NodeID == p2.NodeID &&
pub.Equal(p2Pub) &&
reflect.DeepEqual(nilIfEmpty(p.DisallowedTKAStateIDs), nilIfEmpty(p2.DisallowedTKAStateIDs))
}
@ -106,16 +96,12 @@ func (p *Persist) Pretty() string {
var (
ok, nk key.NodePublic
)
akString := "-"
if !p.OldPrivateNodeKey.IsZero() {
ok = p.OldPrivateNodeKey.Public()
}
if !p.PrivateNodeKey.IsZero() {
nk = p.PublicNodeKey()
}
if p.AttestationKey != nil && !p.AttestationKey.IsZero() {
akString = fmt.Sprintf("%v", p.AttestationKey.Public())
}
return fmt.Sprintf("Persist{o=%v, n=%v u=%#v ak=%s}",
ok.ShortString(), nk.ShortString(), p.UserProfile.LoginName, akString)
return fmt.Sprintf("Persist{o=%v, n=%v u=%#v}",
ok.ShortString(), nk.ShortString(), p.UserProfile.LoginName)
}

@ -19,9 +19,6 @@ func (src *Persist) Clone() *Persist {
}
dst := new(Persist)
*dst = *src
if src.AttestationKey != nil {
dst.AttestationKey = src.AttestationKey.Clone()
}
dst.DisallowedTKAStateIDs = append(src.DisallowedTKAStateIDs[:0:0], src.DisallowedTKAStateIDs...)
return dst
}
@ -34,6 +31,5 @@ var _PersistCloneNeedsRegeneration = Persist(struct {
UserProfile tailcfg.UserProfile
NetworkLockKey key.NLPrivate
NodeID tailcfg.StableNodeID
AttestationKey key.HardwareAttestationKey
DisallowedTKAStateIDs []string
}{})

@ -21,7 +21,7 @@ func fieldsOf(t reflect.Type) (fields []string) {
}
func TestPersistEqual(t *testing.T) {
persistHandles := []string{"PrivateNodeKey", "OldPrivateNodeKey", "UserProfile", "NetworkLockKey", "NodeID", "AttestationKey", "DisallowedTKAStateIDs"}
persistHandles := []string{"PrivateNodeKey", "OldPrivateNodeKey", "UserProfile", "NetworkLockKey", "NodeID", "DisallowedTKAStateIDs"}
if have := fieldsOf(reflect.TypeFor[Persist]()); !reflect.DeepEqual(have, persistHandles) {
t.Errorf("Persist.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
have, persistHandles)

@ -89,11 +89,10 @@ func (v *PersistView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
func (v PersistView) PrivateNodeKey() key.NodePrivate { return v.ж.PrivateNodeKey }
// needed to request key rotation
func (v PersistView) OldPrivateNodeKey() key.NodePrivate { return v.ж.OldPrivateNodeKey }
func (v PersistView) UserProfile() tailcfg.UserProfile { return v.ж.UserProfile }
func (v PersistView) NetworkLockKey() key.NLPrivate { return v.ж.NetworkLockKey }
func (v PersistView) NodeID() tailcfg.StableNodeID { return v.ж.NodeID }
func (v PersistView) AttestationKey() tailcfg.StableNodeID { panic("unsupported") }
func (v PersistView) OldPrivateNodeKey() key.NodePrivate { return v.ж.OldPrivateNodeKey }
func (v PersistView) UserProfile() tailcfg.UserProfile { return v.ж.UserProfile }
func (v PersistView) NetworkLockKey() key.NLPrivate { return v.ж.NetworkLockKey }
func (v PersistView) NodeID() tailcfg.StableNodeID { return v.ж.NodeID }
// DisallowedTKAStateIDs stores the tka.State.StateID values which
// this node will not operate network lock on. This is used to
@ -111,6 +110,5 @@ var _PersistViewNeedsRegeneration = Persist(struct {
UserProfile tailcfg.UserProfile
NetworkLockKey key.NLPrivate
NodeID tailcfg.StableNodeID
AttestationKey key.HardwareAttestationKey
DisallowedTKAStateIDs []string
}{})

@ -5,10 +5,12 @@ package eventbus
import (
"context"
"log"
"reflect"
"slices"
"sync"
"tailscale.com/types/logger"
"tailscale.com/util/set"
)
@ -30,6 +32,7 @@ type Bus struct {
write chan PublishedEvent
snapshot chan chan []PublishedEvent
routeDebug hook[RoutedEvent]
logf logger.Logf
topicsMu sync.Mutex
topics map[reflect.Type][]*subscribeState
@ -40,19 +43,42 @@ type Bus struct {
clients set.Set[*Client]
}
// New returns a new bus. Use [Publish] to make event publishers,
// and [Subscribe] and [SubscribeFunc] to make event subscribers.
func New() *Bus {
// New returns a new bus with default options. It is equivalent to
// calling [NewWithOptions] with zero [BusOptions].
func New() *Bus { return NewWithOptions(BusOptions{}) }
// NewWithOptions returns a new [Bus] with the specified [BusOptions].
// Use [Bus.Client] to construct clients on the bus.
// Use [Publish] to make event publishers.
// Use [Subscribe] and [SubscribeFunc] to make event subscribers.
func NewWithOptions(opts BusOptions) *Bus {
ret := &Bus{
write: make(chan PublishedEvent),
snapshot: make(chan chan []PublishedEvent),
topics: map[reflect.Type][]*subscribeState{},
clients: set.Set[*Client]{},
logf: opts.logger(),
}
ret.router = runWorker(ret.pump)
return ret
}
// BusOptions are optional parameters for a [Bus]. A zero value is ready for
// use and provides defaults as described.
type BusOptions struct {
// Logf, if non-nil, is used for debug logs emitted by the bus and clients,
// publishers, and subscribers under its care. If it is nil, logs are sent
// to [log.Printf].
Logf logger.Logf
}
func (o BusOptions) logger() logger.Logf {
if o.Logf == nil {
return log.Printf
}
return o.Logf
}
// Client returns a new client with no subscriptions. Use [Subscribe]
// to receive events, and [Publish] to emit events.
//
@ -166,6 +192,9 @@ func (b *Bus) pump(ctx context.Context) {
}
}
// logger returns a [logger.Logf] to which logs related to bus activity should be written.
func (b *Bus) logger() logger.Logf { return b.logf }
func (b *Bus) dest(t reflect.Type) []*subscribeState {
b.topicsMu.Lock()
defer b.topicsMu.Unlock()

@ -4,8 +4,11 @@
package eventbus_test
import (
"bytes"
"errors"
"fmt"
"log"
"regexp"
"testing"
"testing/synctest"
"time"
@ -436,6 +439,76 @@ func TestMonitor(t *testing.T) {
t.Run("Wait", testMon(t, func(c *eventbus.Client, m eventbus.Monitor) { c.Close(); m.Wait() }))
}
func TestSlowSubs(t *testing.T) {
swapLogBuf := func(t *testing.T) *bytes.Buffer {
logBuf := new(bytes.Buffer)
save := log.Writer()
log.SetOutput(logBuf)
t.Cleanup(func() { log.SetOutput(save) })
return logBuf
}
t.Run("Subscriber", func(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
buf := swapLogBuf(t)
b := eventbus.New()
defer b.Close()
pc := b.Client("pub")
p := eventbus.Publish[EventA](pc)
sc := b.Client("sub")
s := eventbus.Subscribe[EventA](sc)
go func() {
time.Sleep(6 * time.Second) // trigger the slow check at 5s.
t.Logf("Subscriber accepted %v", <-s.Events())
}()
p.Publish(EventA{12345})
time.Sleep(7 * time.Second) // advance time...
synctest.Wait() // subscriber is done
want := regexp.MustCompile(`^.* tailscale.com/util/eventbus_test bus_test.go:\d+: ` +
`subscriber for eventbus_test.EventA is slow.*`)
if got := buf.String(); !want.MatchString(got) {
t.Errorf("Wrong log output\ngot: %q\nwant: %s", got, want)
}
})
})
t.Run("SubscriberFunc", func(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
buf := swapLogBuf(t)
b := eventbus.New()
defer b.Close()
pc := b.Client("pub")
p := eventbus.Publish[EventB](pc)
sc := b.Client("sub")
eventbus.SubscribeFunc[EventB](sc, func(e EventB) {
time.Sleep(6 * time.Second) // trigger the slow check at 5s.
t.Logf("SubscriberFunc processed %v", e)
})
p.Publish(EventB{67890})
time.Sleep(7 * time.Second) // advance time...
synctest.Wait() // subscriber is done
want := regexp.MustCompile(`^.* tailscale.com/util/eventbus_test bus_test.go:\d+: ` +
`subscriber for eventbus_test.EventB is slow.*`)
if got := buf.String(); !want.MatchString(got) {
t.Errorf("Wrong log output\ngot: %q\nwant: %s", got, want)
}
})
})
}
func TestRegression(t *testing.T) {
bus := eventbus.New()
t.Cleanup(bus.Close)

@ -7,6 +7,7 @@ import (
"reflect"
"sync"
"tailscale.com/types/logger"
"tailscale.com/util/set"
)
@ -29,6 +30,8 @@ type Client struct {
func (c *Client) Name() string { return c.name }
func (c *Client) logger() logger.Logf { return c.bus.logger() }
// Close closes the client. It implicitly closes all publishers and
// subscribers obtained from this client.
func (c *Client) Close() {
@ -142,7 +145,7 @@ func Subscribe[T any](c *Client) *Subscriber[T] {
}
r := c.subscribeStateLocked()
s := newSubscriber[T](r)
s := newSubscriber[T](r, logfForCaller(c.logger()))
r.addSubscriber(s)
return s
}
@ -165,7 +168,7 @@ func SubscribeFunc[T any](c *Client, f func(T)) *SubscriberFunc[T] {
}
r := c.subscribeStateLocked()
s := newSubscriberFunc[T](r, f)
s := newSubscriberFunc[T](r, f, logfForCaller(c.logger()))
r.addSubscriber(s)
return s
}

@ -6,12 +6,22 @@ package eventbus
import (
"cmp"
"fmt"
"path/filepath"
"reflect"
"runtime"
"slices"
"strings"
"sync"
"sync/atomic"
"time"
"tailscale.com/types/logger"
)
// slowSubscriberTimeout is a timeout after which a subscriber that does not
// accept a pending event will be flagged as being slow.
const slowSubscriberTimeout = 5 * time.Second
// A Debugger offers access to a bus's privileged introspection and
// debugging facilities.
//
@ -204,3 +214,29 @@ type DebugTopic struct {
Publisher string
Subscribers []string
}
// logfForCaller returns a [logger.Logf] that prefixes its output with the
// package, filename, and line number of the caller's caller.
// If logf == nil, it returns [logger.Discard].
// If the caller location could not be determined, it returns logf unmodified.
func logfForCaller(logf logger.Logf) logger.Logf {
if logf == nil {
return logger.Discard
}
pc, fpath, line, _ := runtime.Caller(2) // +1 for my caller, +1 for theirs
if f := runtime.FuncForPC(pc); f != nil {
return logger.WithPrefix(logf, fmt.Sprintf("%s %s:%d: ", funcPackageName(f.Name()), filepath.Base(fpath), line))
}
return logf
}
func funcPackageName(funcName string) string {
ls := max(strings.LastIndex(funcName, "/"), 0)
for {
i := strings.LastIndex(funcName, ".")
if i <= ls {
return funcName
}
funcName = funcName[:i]
}
}

@ -8,6 +8,9 @@ import (
"fmt"
"reflect"
"sync"
"time"
"tailscale.com/types/logger"
)
type DeliveredEvent struct {
@ -182,12 +185,18 @@ type Subscriber[T any] struct {
stop stopFlag
read chan T
unregister func()
logf logger.Logf
slow *time.Timer // used to detect slow subscriber service
}
func newSubscriber[T any](r *subscribeState) *Subscriber[T] {
func newSubscriber[T any](r *subscribeState, logf logger.Logf) *Subscriber[T] {
slow := time.NewTimer(0)
slow.Stop() // reset in dispatch
return &Subscriber[T]{
read: make(chan T),
unregister: func() { r.deleteSubscriber(reflect.TypeFor[T]()) },
logf: logf,
slow: slow,
}
}
@ -212,6 +221,11 @@ func (s *Subscriber[T]) monitor(debugEvent T) {
func (s *Subscriber[T]) dispatch(ctx context.Context, vals *queue[DeliveredEvent], acceptCh func() chan DeliveredEvent, snapshot chan chan []DeliveredEvent) bool {
t := vals.Peek().Event.(T)
start := time.Now()
s.slow.Reset(slowSubscriberTimeout)
defer s.slow.Stop()
for {
// Keep the cases in this select in sync with subscribeState.pump
// above. The only difference should be that this select
@ -226,6 +240,9 @@ func (s *Subscriber[T]) dispatch(ctx context.Context, vals *queue[DeliveredEvent
return false
case ch := <-snapshot:
ch <- vals.Snapshot()
case <-s.slow.C:
s.logf("subscriber for %T is slow (%v elapsed)", t, time.Since(start))
s.slow.Reset(slowSubscriberTimeout)
}
}
}
@ -260,12 +277,18 @@ type SubscriberFunc[T any] struct {
stop stopFlag
read func(T)
unregister func()
logf logger.Logf
slow *time.Timer // used to detect slow subscriber service
}
func newSubscriberFunc[T any](r *subscribeState, f func(T)) *SubscriberFunc[T] {
func newSubscriberFunc[T any](r *subscribeState, f func(T), logf logger.Logf) *SubscriberFunc[T] {
slow := time.NewTimer(0)
slow.Stop() // reset in dispatch
return &SubscriberFunc[T]{
read: f,
unregister: func() { r.deleteSubscriber(reflect.TypeFor[T]()) },
logf: logf,
slow: slow,
}
}
@ -285,6 +308,11 @@ func (s *SubscriberFunc[T]) dispatch(ctx context.Context, vals *queue[DeliveredE
t := vals.Peek().Event.(T)
callDone := make(chan struct{})
go s.runCallback(t, callDone)
start := time.Now()
s.slow.Reset(slowSubscriberTimeout)
defer s.slow.Stop()
// Keep the cases in this select in sync with subscribeState.pump
// above. The only difference should be that this select
// delivers a value by calling s.read.
@ -299,6 +327,9 @@ func (s *SubscriberFunc[T]) dispatch(ctx context.Context, vals *queue[DeliveredE
return false
case ch := <-snapshot:
ch <- vals.Snapshot()
case <-s.slow.C:
s.logf("subscriber for %T is slow (%v elapsed)", t, time.Since(start))
s.slow.Reset(slowSubscriberTimeout)
}
}
}

@ -1,9 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && (arm64 || amd64) && !ts_omit_iptables
// TODO(#8502): add support for more architectures
//go:build linux && !ts_omit_iptables
package linuxfw

@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build (linux && !(arm64 || amd64)) || ts_omit_iptables
//go:build linux && ts_omit_iptables
package linuxfw

@ -719,9 +719,13 @@ func NewConn(opts Options) (*Conn, error) {
newPortMapper, ok := portmappertype.HookNewPortMapper.GetOk()
if ok {
c.portMapper = newPortMapper(portmapperLogf, opts.EventBus, opts.NetMon, disableUPnP, c.onlyTCP443.Load)
} else if !testenv.InTest() {
panic("unexpected: HookNewPortMapper not set")
}
// If !ok, the HookNewPortMapper hook is not set (so feature/portmapper
// isn't linked), but the build tag to explicitly omit the portmapper
// isn't set either. This should only happen to js/wasm builds, where
// the portmapper is a no-op even if linked (but it's no longer linked,
// since the move to feature/portmapper), or if people are wiring up
// their own Tailscale build from pieces.
}
c.netMon = opts.NetMon

Loading…
Cancel
Save