Skip to content
This repository was archived by the owner on Jul 13, 2025. It is now read-only.

Commit fa823ab

Browse files
committed
Merge remote-tracking branch 'upstream/main'
2 parents d793228 + 5eafce7 commit fa823ab

File tree

104 files changed

+3547
-882
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

104 files changed

+3547
-882
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
# $ docker exec tailscaled tailscale status
2828

2929

30-
FROM golang:1.23-alpine AS build-env
30+
FROM golang:1.24-alpine AS build-env
3131

3232
WORKDIR /go/src/tailscale
3333

appc/appconnector.go

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -289,9 +289,11 @@ func (e *AppConnector) updateDomains(domains []string) {
289289
toRemove = append(toRemove, netip.PrefixFrom(a, a.BitLen()))
290290
}
291291
}
292-
if err := e.routeAdvertiser.UnadvertiseRoute(toRemove...); err != nil {
293-
e.logf("failed to unadvertise routes on domain removal: %v: %v: %v", slicesx.MapKeys(oldDomains), toRemove, err)
294-
}
292+
e.queue.Add(func() {
293+
if err := e.routeAdvertiser.UnadvertiseRoute(toRemove...); err != nil {
294+
e.logf("failed to unadvertise routes on domain removal: %v: %v: %v", slicesx.MapKeys(oldDomains), toRemove, err)
295+
}
296+
})
295297
}
296298

297299
e.logf("handling domains: %v and wildcards: %v", slicesx.MapKeys(e.domains), e.wildcards)
@@ -310,11 +312,6 @@ func (e *AppConnector) updateRoutes(routes []netip.Prefix) {
310312
return
311313
}
312314

313-
if err := e.routeAdvertiser.AdvertiseRoute(routes...); err != nil {
314-
e.logf("failed to advertise routes: %v: %v", routes, err)
315-
return
316-
}
317-
318315
var toRemove []netip.Prefix
319316

320317
// If we're storing routes and know e.controlRoutes is a good
@@ -338,9 +335,14 @@ nextRoute:
338335
}
339336
}
340337

341-
if err := e.routeAdvertiser.UnadvertiseRoute(toRemove...); err != nil {
342-
e.logf("failed to unadvertise routes: %v: %v", toRemove, err)
343-
}
338+
e.queue.Add(func() {
339+
if err := e.routeAdvertiser.AdvertiseRoute(routes...); err != nil {
340+
e.logf("failed to advertise routes: %v: %v", routes, err)
341+
}
342+
if err := e.routeAdvertiser.UnadvertiseRoute(toRemove...); err != nil {
343+
e.logf("failed to unadvertise routes: %v: %v", toRemove, err)
344+
}
345+
})
344346

345347
e.controlRoutes = routes
346348
if err := e.storeRoutesLocked(); err != nil {

appc/appconnector_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"net/netip"
99
"reflect"
1010
"slices"
11+
"sync/atomic"
1112
"testing"
1213
"time"
1314

@@ -86,6 +87,7 @@ func TestUpdateRoutes(t *testing.T) {
8687

8788
routes := []netip.Prefix{netip.MustParsePrefix("192.0.2.0/24"), netip.MustParsePrefix("192.0.0.1/32")}
8889
a.updateRoutes(routes)
90+
a.Wait(ctx)
8991

9092
slices.SortFunc(rc.Routes(), prefixCompare)
9193
rc.SetRoutes(slices.Compact(rc.Routes()))
@@ -105,6 +107,7 @@ func TestUpdateRoutes(t *testing.T) {
105107
}
106108

107109
func TestUpdateRoutesUnadvertisesContainedRoutes(t *testing.T) {
110+
ctx := context.Background()
108111
for _, shouldStore := range []bool{false, true} {
109112
rc := &appctest.RouteCollector{}
110113
var a *AppConnector
@@ -117,6 +120,7 @@ func TestUpdateRoutesUnadvertisesContainedRoutes(t *testing.T) {
117120
rc.SetRoutes([]netip.Prefix{netip.MustParsePrefix("192.0.2.1/32")})
118121
routes := []netip.Prefix{netip.MustParsePrefix("192.0.2.0/24")}
119122
a.updateRoutes(routes)
123+
a.Wait(ctx)
120124

121125
if !slices.EqualFunc(routes, rc.Routes(), prefixEqual) {
122126
t.Fatalf("got %v, want %v", rc.Routes(), routes)
@@ -636,3 +640,57 @@ func TestMetricBucketsAreSorted(t *testing.T) {
636640
t.Errorf("metricStoreRoutesNBuckets must be in order")
637641
}
638642
}
643+
644+
// TestUpdateRoutesDeadlock is a regression test for a deadlock in
645+
// LocalBackend<->AppConnector interaction. When using real LocalBackend as the
646+
// routeAdvertiser, calls to Advertise/UnadvertiseRoutes can end up calling
647+
// back into AppConnector via authReconfig. If everything is called
648+
// synchronously, this results in a deadlock on AppConnector.mu.
649+
func TestUpdateRoutesDeadlock(t *testing.T) {
650+
ctx := context.Background()
651+
rc := &appctest.RouteCollector{}
652+
a := NewAppConnector(t.Logf, rc, &RouteInfo{}, fakeStoreRoutes)
653+
654+
advertiseCalled := new(atomic.Bool)
655+
unadvertiseCalled := new(atomic.Bool)
656+
rc.AdvertiseCallback = func() {
657+
// Call something that requires a.mu to be held.
658+
a.DomainRoutes()
659+
advertiseCalled.Store(true)
660+
}
661+
rc.UnadvertiseCallback = func() {
662+
// Call something that requires a.mu to be held.
663+
a.DomainRoutes()
664+
unadvertiseCalled.Store(true)
665+
}
666+
667+
a.updateDomains([]string{"example.com"})
668+
a.Wait(ctx)
669+
670+
// Trigger rc.AdveriseRoute.
671+
a.updateRoutes(
672+
[]netip.Prefix{
673+
netip.MustParsePrefix("127.0.0.1/32"),
674+
netip.MustParsePrefix("127.0.0.2/32"),
675+
},
676+
)
677+
a.Wait(ctx)
678+
// Trigger rc.UnadveriseRoute.
679+
a.updateRoutes(
680+
[]netip.Prefix{
681+
netip.MustParsePrefix("127.0.0.1/32"),
682+
},
683+
)
684+
a.Wait(ctx)
685+
686+
if !advertiseCalled.Load() {
687+
t.Error("AdvertiseRoute was not called")
688+
}
689+
if !unadvertiseCalled.Load() {
690+
t.Error("UnadvertiseRoute was not called")
691+
}
692+
693+
if want := []netip.Prefix{netip.MustParsePrefix("127.0.0.1/32")}; !slices.Equal(slices.Compact(rc.Routes()), want) {
694+
t.Fatalf("got %v, want %v", rc.Routes(), want)
695+
}
696+
}

appc/appctest/appctest.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,22 @@ import (
1111

1212
// RouteCollector is a test helper that collects the list of routes advertised
1313
type RouteCollector struct {
14+
// AdvertiseCallback (optional) is called synchronously from
15+
// AdvertiseRoute.
16+
AdvertiseCallback func()
17+
// UnadvertiseCallback (optional) is called synchronously from
18+
// UnadvertiseRoute.
19+
UnadvertiseCallback func()
20+
1421
routes []netip.Prefix
1522
removedRoutes []netip.Prefix
1623
}
1724

1825
func (rc *RouteCollector) AdvertiseRoute(pfx ...netip.Prefix) error {
1926
rc.routes = append(rc.routes, pfx...)
27+
if rc.AdvertiseCallback != nil {
28+
rc.AdvertiseCallback()
29+
}
2030
return nil
2131
}
2232

@@ -30,6 +40,9 @@ func (rc *RouteCollector) UnadvertiseRoute(toRemove ...netip.Prefix) error {
3040
rc.removedRoutes = append(rc.removedRoutes, r)
3141
}
3242
}
43+
if rc.UnadvertiseCallback != nil {
44+
rc.UnadvertiseCallback()
45+
}
3346
return nil
3447
}
3548

client/systray/systray.go

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ type Menu struct {
7272
curProfile ipn.LoginProfile
7373
allProfiles []ipn.LoginProfile
7474

75+
// readonly is whether the systray app is running in read-only mode.
76+
// This is set if LocalAPI returns a permission error,
77+
// typically because the user needs to run `tailscale set --operator=$USER`.
78+
readonly bool
79+
7580
bgCtx context.Context // ctx for background tasks not involving menu item clicks
7681
bgCancel context.CancelFunc
7782

@@ -153,13 +158,18 @@ func (menu *Menu) updateState() {
153158
defer menu.mu.Unlock()
154159
menu.init()
155160

161+
menu.readonly = false
162+
156163
var err error
157164
menu.status, err = menu.lc.Status(menu.bgCtx)
158165
if err != nil {
159166
log.Print(err)
160167
}
161168
menu.curProfile, menu.allProfiles, err = menu.lc.ProfileStatus(menu.bgCtx)
162169
if err != nil {
170+
if local.IsAccessDeniedError(err) {
171+
menu.readonly = true
172+
}
163173
log.Print(err)
164174
}
165175
}
@@ -182,6 +192,15 @@ func (menu *Menu) rebuild() {
182192

183193
systray.ResetMenu()
184194

195+
if menu.readonly {
196+
const readonlyMsg = "No permission to manage Tailscale.\nSee tailscale.com/s/cli-operator"
197+
m := systray.AddMenuItem(readonlyMsg, "")
198+
onClick(ctx, m, func(_ context.Context) {
199+
webbrowser.Open("https://tailscale.com/s/cli-operator")
200+
})
201+
systray.AddSeparator()
202+
}
203+
185204
menu.connect = systray.AddMenuItem("Connect", "")
186205
menu.disconnect = systray.AddMenuItem("Disconnect", "")
187206
menu.disconnect.Hide()
@@ -222,28 +241,35 @@ func (menu *Menu) rebuild() {
222241
setAppIcon(disconnected)
223242
}
224243

244+
if menu.readonly {
245+
menu.connect.Disable()
246+
menu.disconnect.Disable()
247+
}
248+
225249
account := "Account"
226250
if pt := profileTitle(menu.curProfile); pt != "" {
227251
account = pt
228252
}
229-
accounts := systray.AddMenuItem(account, "")
230-
setRemoteIcon(accounts, menu.curProfile.UserProfile.ProfilePicURL)
231-
time.Sleep(newMenuDelay)
232-
for _, profile := range menu.allProfiles {
233-
title := profileTitle(profile)
234-
var item *systray.MenuItem
235-
if profile.ID == menu.curProfile.ID {
236-
item = accounts.AddSubMenuItemCheckbox(title, "", true)
237-
} else {
238-
item = accounts.AddSubMenuItem(title, "")
239-
}
240-
setRemoteIcon(item, profile.UserProfile.ProfilePicURL)
241-
onClick(ctx, item, func(ctx context.Context) {
242-
select {
243-
case <-ctx.Done():
244-
case menu.accountsCh <- profile.ID:
253+
if !menu.readonly {
254+
accounts := systray.AddMenuItem(account, "")
255+
setRemoteIcon(accounts, menu.curProfile.UserProfile.ProfilePicURL)
256+
time.Sleep(newMenuDelay)
257+
for _, profile := range menu.allProfiles {
258+
title := profileTitle(profile)
259+
var item *systray.MenuItem
260+
if profile.ID == menu.curProfile.ID {
261+
item = accounts.AddSubMenuItemCheckbox(title, "", true)
262+
} else {
263+
item = accounts.AddSubMenuItem(title, "")
245264
}
246-
})
265+
setRemoteIcon(item, profile.UserProfile.ProfilePicURL)
266+
onClick(ctx, item, func(ctx context.Context) {
267+
select {
268+
case <-ctx.Done():
269+
case menu.accountsCh <- profile.ID:
270+
}
271+
})
272+
}
247273
}
248274

249275
if menu.status != nil && menu.status.Self != nil && len(menu.status.Self.TailscaleIPs) > 0 {
@@ -255,7 +281,9 @@ func (menu *Menu) rebuild() {
255281
}
256282
systray.AddSeparator()
257283

258-
menu.rebuildExitNodeMenu(ctx)
284+
if !menu.readonly {
285+
menu.rebuildExitNodeMenu(ctx)
286+
}
259287

260288
if menu.status != nil {
261289
menu.more = systray.AddMenuItem("More settings", "")

client/tailscale/acl.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"fmt"
1313
"net/http"
1414
"net/netip"
15+
"net/url"
1516
)
1617

1718
// ACLRow defines a rule that grants access by a set of users or groups to a set
@@ -83,7 +84,7 @@ func (c *Client) ACL(ctx context.Context) (acl *ACL, err error) {
8384
}
8485
}()
8586

86-
path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl", c.baseURL(), c.tailnet)
87+
path := c.BuildTailnetURL("acl")
8788
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
8889
if err != nil {
8990
return nil, err
@@ -97,7 +98,7 @@ func (c *Client) ACL(ctx context.Context) (acl *ACL, err error) {
9798
// If status code was not successful, return the error.
9899
// TODO: Change the check for the StatusCode to include other 2XX success codes.
99100
if resp.StatusCode != http.StatusOK {
100-
return nil, handleErrorResponse(b, resp)
101+
return nil, HandleErrorResponse(b, resp)
101102
}
102103

103104
// Otherwise, try to decode the response.
@@ -126,7 +127,7 @@ func (c *Client) ACLHuJSON(ctx context.Context) (acl *ACLHuJSON, err error) {
126127
}
127128
}()
128129

129-
path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl?details=1", c.baseURL(), c.tailnet)
130+
path := c.BuildTailnetURL("acl", url.Values{"details": {"1"}})
130131
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
131132
if err != nil {
132133
return nil, err
@@ -138,15 +139,15 @@ func (c *Client) ACLHuJSON(ctx context.Context) (acl *ACLHuJSON, err error) {
138139
}
139140

140141
if resp.StatusCode != http.StatusOK {
141-
return nil, handleErrorResponse(b, resp)
142+
return nil, HandleErrorResponse(b, resp)
142143
}
143144

144145
data := struct {
145146
ACL []byte `json:"acl"`
146147
Warnings []string `json:"warnings"`
147148
}{}
148149
if err := json.Unmarshal(b, &data); err != nil {
149-
return nil, err
150+
return nil, fmt.Errorf("json.Unmarshal %q: %w", b, err)
150151
}
151152

152153
acl = &ACLHuJSON{
@@ -184,7 +185,7 @@ func (e ACLTestError) Error() string {
184185
}
185186

186187
func (c *Client) aclPOSTRequest(ctx context.Context, body []byte, avoidCollisions bool, etag, acceptHeader string) ([]byte, string, error) {
187-
path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl", c.baseURL(), c.tailnet)
188+
path := c.BuildTailnetURL("acl")
188189
req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewBuffer(body))
189190
if err != nil {
190191
return nil, "", err
@@ -328,7 +329,7 @@ type ACLPreview struct {
328329
}
329330

330331
func (c *Client) previewACLPostRequest(ctx context.Context, body []byte, previewType string, previewFor string) (res *ACLPreviewResponse, err error) {
331-
path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl/preview", c.baseURL(), c.tailnet)
332+
path := c.BuildTailnetURL("acl", "preview")
332333
req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewBuffer(body))
333334
if err != nil {
334335
return nil, err
@@ -350,7 +351,7 @@ func (c *Client) previewACLPostRequest(ctx context.Context, body []byte, preview
350351
// If status code was not successful, return the error.
351352
// TODO: Change the check for the StatusCode to include other 2XX success codes.
352353
if resp.StatusCode != http.StatusOK {
353-
return nil, handleErrorResponse(b, resp)
354+
return nil, HandleErrorResponse(b, resp)
354355
}
355356
if err = json.Unmarshal(b, &res); err != nil {
356357
return nil, err
@@ -488,7 +489,7 @@ func (c *Client) ValidateACLJSON(ctx context.Context, source, dest string) (test
488489
return nil, err
489490
}
490491

491-
path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl/validate", c.baseURL(), c.tailnet)
492+
path := c.BuildTailnetURL("acl", "validate")
492493
req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewBuffer(postData))
493494
if err != nil {
494495
return nil, err

0 commit comments

Comments
 (0)