Compare commits

..

1 Commits

Author SHA1 Message Date
Paul Scott
99865276a7 cmd/testwrapper: wip idea
Signed-off-by: Paul Scott <paul@tailscale.com>
2025-02-18 08:29:55 -08:00
67 changed files with 831 additions and 1521 deletions

View File

@@ -33,7 +33,7 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@2e788936b09dd82dc280e845628a40d2ba6b204c # v6.3.1
with:
version: v1.64
version: v1.60
# Show only new issues if it's a pull request.
only-new-issues: true

View File

@@ -27,7 +27,7 @@
# $ docker exec tailscaled tailscale status
FROM golang:1.24-alpine AS build-env
FROM golang:1.23-alpine AS build-env
WORKDIR /go/src/tailscale

View File

@@ -289,11 +289,9 @@ func (e *AppConnector) updateDomains(domains []string) {
toRemove = append(toRemove, netip.PrefixFrom(a, a.BitLen()))
}
}
e.queue.Add(func() {
if err := e.routeAdvertiser.UnadvertiseRoute(toRemove...); err != nil {
e.logf("failed to unadvertise routes on domain removal: %v: %v: %v", slicesx.MapKeys(oldDomains), toRemove, err)
}
})
if err := e.routeAdvertiser.UnadvertiseRoute(toRemove...); err != nil {
e.logf("failed to unadvertise routes on domain removal: %v: %v: %v", slicesx.MapKeys(oldDomains), toRemove, err)
}
}
e.logf("handling domains: %v and wildcards: %v", slicesx.MapKeys(e.domains), e.wildcards)
@@ -312,6 +310,11 @@ func (e *AppConnector) updateRoutes(routes []netip.Prefix) {
return
}
if err := e.routeAdvertiser.AdvertiseRoute(routes...); err != nil {
e.logf("failed to advertise routes: %v: %v", routes, err)
return
}
var toRemove []netip.Prefix
// If we're storing routes and know e.controlRoutes is a good
@@ -335,14 +338,9 @@ nextRoute:
}
}
e.queue.Add(func() {
if err := e.routeAdvertiser.AdvertiseRoute(routes...); err != nil {
e.logf("failed to advertise routes: %v: %v", routes, err)
}
if err := e.routeAdvertiser.UnadvertiseRoute(toRemove...); err != nil {
e.logf("failed to unadvertise routes: %v: %v", toRemove, err)
}
})
if err := e.routeAdvertiser.UnadvertiseRoute(toRemove...); err != nil {
e.logf("failed to unadvertise routes: %v: %v", toRemove, err)
}
e.controlRoutes = routes
if err := e.storeRoutesLocked(); err != nil {

View File

@@ -8,7 +8,6 @@ import (
"net/netip"
"reflect"
"slices"
"sync/atomic"
"testing"
"time"
@@ -87,7 +86,6 @@ func TestUpdateRoutes(t *testing.T) {
routes := []netip.Prefix{netip.MustParsePrefix("192.0.2.0/24"), netip.MustParsePrefix("192.0.0.1/32")}
a.updateRoutes(routes)
a.Wait(ctx)
slices.SortFunc(rc.Routes(), prefixCompare)
rc.SetRoutes(slices.Compact(rc.Routes()))
@@ -107,7 +105,6 @@ func TestUpdateRoutes(t *testing.T) {
}
func TestUpdateRoutesUnadvertisesContainedRoutes(t *testing.T) {
ctx := context.Background()
for _, shouldStore := range []bool{false, true} {
rc := &appctest.RouteCollector{}
var a *AppConnector
@@ -120,7 +117,6 @@ func TestUpdateRoutesUnadvertisesContainedRoutes(t *testing.T) {
rc.SetRoutes([]netip.Prefix{netip.MustParsePrefix("192.0.2.1/32")})
routes := []netip.Prefix{netip.MustParsePrefix("192.0.2.0/24")}
a.updateRoutes(routes)
a.Wait(ctx)
if !slices.EqualFunc(routes, rc.Routes(), prefixEqual) {
t.Fatalf("got %v, want %v", rc.Routes(), routes)
@@ -640,57 +636,3 @@ func TestMetricBucketsAreSorted(t *testing.T) {
t.Errorf("metricStoreRoutesNBuckets must be in order")
}
}
// TestUpdateRoutesDeadlock is a regression test for a deadlock in
// LocalBackend<->AppConnector interaction. When using real LocalBackend as the
// routeAdvertiser, calls to Advertise/UnadvertiseRoutes can end up calling
// back into AppConnector via authReconfig. If everything is called
// synchronously, this results in a deadlock on AppConnector.mu.
func TestUpdateRoutesDeadlock(t *testing.T) {
ctx := context.Background()
rc := &appctest.RouteCollector{}
a := NewAppConnector(t.Logf, rc, &RouteInfo{}, fakeStoreRoutes)
advertiseCalled := new(atomic.Bool)
unadvertiseCalled := new(atomic.Bool)
rc.AdvertiseCallback = func() {
// Call something that requires a.mu to be held.
a.DomainRoutes()
advertiseCalled.Store(true)
}
rc.UnadvertiseCallback = func() {
// Call something that requires a.mu to be held.
a.DomainRoutes()
unadvertiseCalled.Store(true)
}
a.updateDomains([]string{"example.com"})
a.Wait(ctx)
// Trigger rc.AdveriseRoute.
a.updateRoutes(
[]netip.Prefix{
netip.MustParsePrefix("127.0.0.1/32"),
netip.MustParsePrefix("127.0.0.2/32"),
},
)
a.Wait(ctx)
// Trigger rc.UnadveriseRoute.
a.updateRoutes(
[]netip.Prefix{
netip.MustParsePrefix("127.0.0.1/32"),
},
)
a.Wait(ctx)
if !advertiseCalled.Load() {
t.Error("AdvertiseRoute was not called")
}
if !unadvertiseCalled.Load() {
t.Error("UnadvertiseRoute was not called")
}
if want := []netip.Prefix{netip.MustParsePrefix("127.0.0.1/32")}; !slices.Equal(slices.Compact(rc.Routes()), want) {
t.Fatalf("got %v, want %v", rc.Routes(), want)
}
}

View File

@@ -11,22 +11,12 @@ import (
// RouteCollector is a test helper that collects the list of routes advertised
type RouteCollector struct {
// AdvertiseCallback (optional) is called synchronously from
// AdvertiseRoute.
AdvertiseCallback func()
// UnadvertiseCallback (optional) is called synchronously from
// UnadvertiseRoute.
UnadvertiseCallback func()
routes []netip.Prefix
removedRoutes []netip.Prefix
}
func (rc *RouteCollector) AdvertiseRoute(pfx ...netip.Prefix) error {
rc.routes = append(rc.routes, pfx...)
if rc.AdvertiseCallback != nil {
rc.AdvertiseCallback()
}
return nil
}
@@ -40,9 +30,6 @@ func (rc *RouteCollector) UnadvertiseRoute(toRemove ...netip.Prefix) error {
rc.removedRoutes = append(rc.removedRoutes, r)
}
}
if rc.UnadvertiseCallback != nil {
rc.UnadvertiseCallback()
}
return nil
}

View File

@@ -72,11 +72,6 @@ type Menu struct {
curProfile ipn.LoginProfile
allProfiles []ipn.LoginProfile
// readonly is whether the systray app is running in read-only mode.
// This is set if LocalAPI returns a permission error,
// typically because the user needs to run `tailscale set --operator=$USER`.
readonly bool
bgCtx context.Context // ctx for background tasks not involving menu item clicks
bgCancel context.CancelFunc
@@ -158,8 +153,6 @@ func (menu *Menu) updateState() {
defer menu.mu.Unlock()
menu.init()
menu.readonly = false
var err error
menu.status, err = menu.lc.Status(menu.bgCtx)
if err != nil {
@@ -167,9 +160,6 @@ func (menu *Menu) updateState() {
}
menu.curProfile, menu.allProfiles, err = menu.lc.ProfileStatus(menu.bgCtx)
if err != nil {
if local.IsAccessDeniedError(err) {
menu.readonly = true
}
log.Print(err)
}
}
@@ -192,15 +182,6 @@ func (menu *Menu) rebuild() {
systray.ResetMenu()
if menu.readonly {
const readonlyMsg = "No permission to manage Tailscale.\nSee tailscale.com/s/cli-operator"
m := systray.AddMenuItem(readonlyMsg, "")
onClick(ctx, m, func(_ context.Context) {
webbrowser.Open("https://tailscale.com/s/cli-operator")
})
systray.AddSeparator()
}
menu.connect = systray.AddMenuItem("Connect", "")
menu.disconnect = systray.AddMenuItem("Disconnect", "")
menu.disconnect.Hide()
@@ -241,35 +222,28 @@ func (menu *Menu) rebuild() {
setAppIcon(disconnected)
}
if menu.readonly {
menu.connect.Disable()
menu.disconnect.Disable()
}
account := "Account"
if pt := profileTitle(menu.curProfile); pt != "" {
account = pt
}
if !menu.readonly {
accounts := systray.AddMenuItem(account, "")
setRemoteIcon(accounts, menu.curProfile.UserProfile.ProfilePicURL)
time.Sleep(newMenuDelay)
for _, profile := range menu.allProfiles {
title := profileTitle(profile)
var item *systray.MenuItem
if profile.ID == menu.curProfile.ID {
item = accounts.AddSubMenuItemCheckbox(title, "", true)
} else {
item = accounts.AddSubMenuItem(title, "")
}
setRemoteIcon(item, profile.UserProfile.ProfilePicURL)
onClick(ctx, item, func(ctx context.Context) {
select {
case <-ctx.Done():
case menu.accountsCh <- profile.ID:
}
})
accounts := systray.AddMenuItem(account, "")
setRemoteIcon(accounts, menu.curProfile.UserProfile.ProfilePicURL)
time.Sleep(newMenuDelay)
for _, profile := range menu.allProfiles {
title := profileTitle(profile)
var item *systray.MenuItem
if profile.ID == menu.curProfile.ID {
item = accounts.AddSubMenuItemCheckbox(title, "", true)
} else {
item = accounts.AddSubMenuItem(title, "")
}
setRemoteIcon(item, profile.UserProfile.ProfilePicURL)
onClick(ctx, item, func(ctx context.Context) {
select {
case <-ctx.Done():
case menu.accountsCh <- profile.ID:
}
})
}
if menu.status != nil && menu.status.Self != nil && len(menu.status.Self.TailscaleIPs) > 0 {
@@ -281,9 +255,7 @@ func (menu *Menu) rebuild() {
}
systray.AddSeparator()
if !menu.readonly {
menu.rebuildExitNodeMenu(ctx)
}
menu.rebuildExitNodeMenu(ctx)
if menu.status != nil {
menu.more = systray.AddMenuItem("More settings", "")

View File

@@ -12,7 +12,6 @@ import (
"fmt"
"net/http"
"net/netip"
"net/url"
)
// ACLRow defines a rule that grants access by a set of users or groups to a set
@@ -84,7 +83,7 @@ func (c *Client) ACL(ctx context.Context) (acl *ACL, err error) {
}
}()
path := c.BuildTailnetURL("acl")
path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl", c.baseURL(), c.tailnet)
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
if err != nil {
return nil, err
@@ -98,7 +97,7 @@ func (c *Client) ACL(ctx context.Context) (acl *ACL, err error) {
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, HandleErrorResponse(b, resp)
return nil, handleErrorResponse(b, resp)
}
// Otherwise, try to decode the response.
@@ -127,7 +126,7 @@ func (c *Client) ACLHuJSON(ctx context.Context) (acl *ACLHuJSON, err error) {
}
}()
path := c.BuildTailnetURL("acl", url.Values{"details": {"1"}})
path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl?details=1", c.baseURL(), c.tailnet)
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
if err != nil {
return nil, err
@@ -139,7 +138,7 @@ func (c *Client) ACLHuJSON(ctx context.Context) (acl *ACLHuJSON, err error) {
}
if resp.StatusCode != http.StatusOK {
return nil, HandleErrorResponse(b, resp)
return nil, handleErrorResponse(b, resp)
}
data := struct {
@@ -147,7 +146,7 @@ func (c *Client) ACLHuJSON(ctx context.Context) (acl *ACLHuJSON, err error) {
Warnings []string `json:"warnings"`
}{}
if err := json.Unmarshal(b, &data); err != nil {
return nil, fmt.Errorf("json.Unmarshal %q: %w", b, err)
return nil, err
}
acl = &ACLHuJSON{
@@ -185,7 +184,7 @@ func (e ACLTestError) Error() string {
}
func (c *Client) aclPOSTRequest(ctx context.Context, body []byte, avoidCollisions bool, etag, acceptHeader string) ([]byte, string, error) {
path := c.BuildTailnetURL("acl")
path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl", c.baseURL(), c.tailnet)
req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewBuffer(body))
if err != nil {
return nil, "", err
@@ -329,7 +328,7 @@ type ACLPreview struct {
}
func (c *Client) previewACLPostRequest(ctx context.Context, body []byte, previewType string, previewFor string) (res *ACLPreviewResponse, err error) {
path := c.BuildTailnetURL("acl", "preview")
path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl/preview", c.baseURL(), c.tailnet)
req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewBuffer(body))
if err != nil {
return nil, err
@@ -351,7 +350,7 @@ func (c *Client) previewACLPostRequest(ctx context.Context, body []byte, preview
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, HandleErrorResponse(b, resp)
return nil, handleErrorResponse(b, resp)
}
if err = json.Unmarshal(b, &res); err != nil {
return nil, err
@@ -489,7 +488,7 @@ func (c *Client) ValidateACLJSON(ctx context.Context, source, dest string) (test
return nil, err
}
path := c.BuildTailnetURL("acl", "validate")
path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl/validate", c.baseURL(), c.tailnet)
req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewBuffer(postData))
if err != nil {
return nil, err

View File

@@ -131,7 +131,7 @@ func (c *Client) Devices(ctx context.Context, fields *DeviceFieldsOpts) (deviceL
}
}()
path := c.BuildTailnetURL("devices")
path := fmt.Sprintf("%s/api/v2/tailnet/%s/devices", c.baseURL(), c.tailnet)
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
if err != nil {
return nil, err
@@ -149,7 +149,7 @@ func (c *Client) Devices(ctx context.Context, fields *DeviceFieldsOpts) (deviceL
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, HandleErrorResponse(b, resp)
return nil, handleErrorResponse(b, resp)
}
var devices GetDevicesResponse
@@ -188,7 +188,7 @@ func (c *Client) Device(ctx context.Context, deviceID string, fields *DeviceFiel
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, HandleErrorResponse(b, resp)
return nil, handleErrorResponse(b, resp)
}
err = json.Unmarshal(b, &device)
@@ -221,7 +221,7 @@ func (c *Client) DeleteDevice(ctx context.Context, deviceID string) (err error)
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return HandleErrorResponse(b, resp)
return handleErrorResponse(b, resp)
}
return nil
}
@@ -253,7 +253,7 @@ func (c *Client) SetAuthorized(ctx context.Context, deviceID string, authorized
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return HandleErrorResponse(b, resp)
return handleErrorResponse(b, resp)
}
return nil
@@ -281,7 +281,7 @@ func (c *Client) SetTags(ctx context.Context, deviceID string, tags []string) er
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return HandleErrorResponse(b, resp)
return handleErrorResponse(b, resp)
}
return nil

View File

@@ -44,7 +44,7 @@ type DNSPreferences struct {
}
func (c *Client) dnsGETRequest(ctx context.Context, endpoint string) ([]byte, error) {
path := c.BuildTailnetURL("dns", endpoint)
path := fmt.Sprintf("%s/api/v2/tailnet/%s/dns/%s", c.baseURL(), c.tailnet, endpoint)
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
if err != nil {
return nil, err
@@ -57,14 +57,14 @@ func (c *Client) dnsGETRequest(ctx context.Context, endpoint string) ([]byte, er
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, HandleErrorResponse(b, resp)
return nil, handleErrorResponse(b, resp)
}
return b, nil
}
func (c *Client) dnsPOSTRequest(ctx context.Context, endpoint string, postData any) ([]byte, error) {
path := c.BuildTailnetURL("dns", endpoint)
path := fmt.Sprintf("%s/api/v2/tailnet/%s/dns/%s", c.baseURL(), c.tailnet, endpoint)
data, err := json.Marshal(&postData)
if err != nil {
return nil, err
@@ -84,7 +84,7 @@ func (c *Client) dnsPOSTRequest(ctx context.Context, endpoint string, postData a
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, HandleErrorResponse(b, resp)
return nil, handleErrorResponse(b, resp)
}
return b, nil

View File

@@ -40,7 +40,7 @@ type KeyDeviceCreateCapabilities struct {
// Keys returns the list of keys for the current user.
func (c *Client) Keys(ctx context.Context) ([]string, error) {
path := c.BuildTailnetURL("keys")
path := fmt.Sprintf("%s/api/v2/tailnet/%s/keys", c.baseURL(), c.tailnet)
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
if err != nil {
return nil, err
@@ -51,7 +51,7 @@ func (c *Client) Keys(ctx context.Context) ([]string, error) {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, HandleErrorResponse(b, resp)
return nil, handleErrorResponse(b, resp)
}
var keys struct {
@@ -99,7 +99,7 @@ func (c *Client) CreateKeyWithExpiry(ctx context.Context, caps KeyCapabilities,
return "", nil, err
}
path := c.BuildTailnetURL("keys")
path := fmt.Sprintf("%s/api/v2/tailnet/%s/keys", c.baseURL(), c.tailnet)
req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewReader(bs))
if err != nil {
return "", nil, err
@@ -110,7 +110,7 @@ func (c *Client) CreateKeyWithExpiry(ctx context.Context, caps KeyCapabilities,
return "", nil, err
}
if resp.StatusCode != http.StatusOK {
return "", nil, HandleErrorResponse(b, resp)
return "", nil, handleErrorResponse(b, resp)
}
var key struct {
@@ -126,7 +126,7 @@ func (c *Client) CreateKeyWithExpiry(ctx context.Context, caps KeyCapabilities,
// Key returns the metadata for the given key ID. Currently, capabilities are
// only returned for auth keys, API keys only return general metadata.
func (c *Client) Key(ctx context.Context, id string) (*Key, error) {
path := c.BuildTailnetURL("keys", id)
path := fmt.Sprintf("%s/api/v2/tailnet/%s/keys/%s", c.baseURL(), c.tailnet, id)
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
if err != nil {
return nil, err
@@ -137,7 +137,7 @@ func (c *Client) Key(ctx context.Context, id string) (*Key, error) {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, HandleErrorResponse(b, resp)
return nil, handleErrorResponse(b, resp)
}
var key Key
@@ -149,7 +149,7 @@ func (c *Client) Key(ctx context.Context, id string) (*Key, error) {
// DeleteKey deletes the key with the given ID.
func (c *Client) DeleteKey(ctx context.Context, id string) error {
path := c.BuildTailnetURL("keys", id)
path := fmt.Sprintf("%s/api/v2/tailnet/%s/keys/%s", c.baseURL(), c.tailnet, id)
req, err := http.NewRequestWithContext(ctx, "DELETE", path, nil)
if err != nil {
return err
@@ -160,7 +160,7 @@ func (c *Client) DeleteKey(ctx context.Context, id string) error {
return err
}
if resp.StatusCode != http.StatusOK {
return HandleErrorResponse(b, resp)
return handleErrorResponse(b, resp)
}
return nil
}

View File

@@ -44,7 +44,7 @@ func (c *Client) Routes(ctx context.Context, deviceID string) (routes *Routes, e
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, HandleErrorResponse(b, resp)
return nil, handleErrorResponse(b, resp)
}
var sr Routes
@@ -84,7 +84,7 @@ func (c *Client) SetRoutes(ctx context.Context, deviceID string, subnets []netip
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, HandleErrorResponse(b, resp)
return nil, handleErrorResponse(b, resp)
}
var srr *Routes

View File

@@ -9,6 +9,7 @@ import (
"context"
"fmt"
"net/http"
"net/url"
"tailscale.com/util/httpm"
)
@@ -21,7 +22,7 @@ func (c *Client) TailnetDeleteRequest(ctx context.Context, tailnetID string) (er
}
}()
path := c.BuildTailnetURL("tailnet")
path := fmt.Sprintf("%s/api/v2/tailnet/%s", c.baseURL(), url.PathEscape(string(tailnetID)))
req, err := http.NewRequestWithContext(ctx, httpm.DELETE, path, nil)
if err != nil {
return err
@@ -34,7 +35,7 @@ func (c *Client) TailnetDeleteRequest(ctx context.Context, tailnetID string) (er
}
if resp.StatusCode != http.StatusOK {
return HandleErrorResponse(b, resp)
return handleErrorResponse(b, resp)
}
return nil

View File

@@ -17,8 +17,6 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"path"
)
// I_Acknowledge_This_API_Is_Unstable must be set true to use this package
@@ -65,46 +63,6 @@ func (c *Client) httpClient() *http.Client {
return http.DefaultClient
}
// BuildURL builds a url to http(s)://<apiserver>/api/v2/<slash-separated-pathElements>
// using the given pathElements. It url escapes each path element, so the
// caller doesn't need to worry about that. The last item of pathElements can
// be of type url.Values to add a query string to the URL.
//
// For example, BuildURL(devices, 5) with the default server URL would result in
// https://api.tailscale.com/api/v2/devices/5.
func (c *Client) BuildURL(pathElements ...any) string {
elem := make([]string, 1, len(pathElements)+1)
elem[0] = "/api/v2"
var query string
for i, pathElement := range pathElements {
if uv, ok := pathElement.(url.Values); ok && i == len(pathElements)-1 {
query = uv.Encode()
} else {
elem = append(elem, url.PathEscape(fmt.Sprint(pathElement)))
}
}
url := c.baseURL() + path.Join(elem...)
if query != "" {
url += "?" + query
}
return url
}
// BuildTailnetURL builds a url to http(s)://<apiserver>/api/v2/tailnet/<tailnet>/<slash-separated-pathElements>
// using the given pathElements. It url escapes each path element, so the
// caller doesn't need to worry about that. The last item of pathElements can
// be of type url.Values to add a query string to the URL.
//
// For example, BuildTailnetURL(policy, validate) with the default server URL and a tailnet of "example.com"
// would result in https://api.tailscale.com/api/v2/tailnet/example.com/policy/validate.
func (c *Client) BuildTailnetURL(pathElements ...any) string {
allElements := make([]any, 2, len(pathElements)+2)
allElements[0] = "tailnet"
allElements[1] = c.tailnet
allElements = append(allElements, pathElements...)
return c.BuildURL(allElements...)
}
func (c *Client) baseURL() string {
if c.BaseURL != "" {
return c.BaseURL
@@ -192,14 +150,12 @@ func (e ErrResponse) Error() string {
return fmt.Sprintf("Status: %d, Message: %q", e.Status, e.Message)
}
// HandleErrorResponse decodes the error message from the server and returns
// handleErrorResponse decodes the error message from the server and returns
// an ErrResponse from it.
//
// Deprecated: use tailscale.com/client/tailscale/v2 instead.
func HandleErrorResponse(b []byte, resp *http.Response) error {
func handleErrorResponse(b []byte, resp *http.Response) error {
var errResp ErrResponse
if err := json.Unmarshal(b, &errResp); err != nil {
return fmt.Errorf("json.Unmarshal %q: %w", b, err)
return err
}
errResp.Status = resp.StatusCode
return errResp

View File

@@ -1,86 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package tailscale
import (
"net/url"
"testing"
)
func TestClientBuildURL(t *testing.T) {
c := Client{BaseURL: "http://127.0.0.1:1234"}
for _, tt := range []struct {
desc string
elements []any
want string
}{
{
desc: "single-element",
elements: []any{"devices"},
want: "http://127.0.0.1:1234/api/v2/devices",
},
{
desc: "multiple-elements",
elements: []any{"tailnet", "example.com"},
want: "http://127.0.0.1:1234/api/v2/tailnet/example.com",
},
{
desc: "escape-element",
elements: []any{"tailnet", "example dot com?foo=bar"},
want: `http://127.0.0.1:1234/api/v2/tailnet/example%20dot%20com%3Ffoo=bar`,
},
{
desc: "url.Values",
elements: []any{"tailnet", "example.com", "acl", url.Values{"details": {"1"}}},
want: `http://127.0.0.1:1234/api/v2/tailnet/example.com/acl?details=1`,
},
} {
t.Run(tt.desc, func(t *testing.T) {
got := c.BuildURL(tt.elements...)
if got != tt.want {
t.Errorf("got %q, want %q", got, tt.want)
}
})
}
}
func TestClientBuildTailnetURL(t *testing.T) {
c := Client{
BaseURL: "http://127.0.0.1:1234",
tailnet: "example.com",
}
for _, tt := range []struct {
desc string
elements []any
want string
}{
{
desc: "single-element",
elements: []any{"devices"},
want: "http://127.0.0.1:1234/api/v2/tailnet/example.com/devices",
},
{
desc: "multiple-elements",
elements: []any{"devices", 123},
want: "http://127.0.0.1:1234/api/v2/tailnet/example.com/devices/123",
},
{
desc: "escape-element",
elements: []any{"foo bar?baz=qux"},
want: `http://127.0.0.1:1234/api/v2/tailnet/example.com/foo%20bar%3Fbaz=qux`,
},
{
desc: "url.Values",
elements: []any{"acl", url.Values{"details": {"1"}}},
want: `http://127.0.0.1:1234/api/v2/tailnet/example.com/acl?details=1`,
},
} {
t.Run(tt.desc, func(t *testing.T) {
got := c.BuildTailnetURL(tt.elements...)
if got != tt.want {
t.Errorf("got %q, want %q", got, tt.want)
}
})
}
}

View File

@@ -203,9 +203,35 @@ func NewServer(opts ServerOpts) (s *Server, err error) {
}
s.assetsHandler, s.assetsCleanup = assetsHandler(s.devMode)
var metric string
s.apiHandler, metric = s.modeAPIHandler(s.mode)
s.apiHandler = s.withCSRF(s.apiHandler)
var metric string // clientmetric to report on startup
// Create handler for "/api" requests with CSRF protection.
// We don't require secure cookies, since the web client is regularly used
// on network appliances that are served on local non-https URLs.
// The client is secured by limiting the interface it listens on,
// or by authenticating requests before they reach the web client.
csrfProtect := csrf.Protect(s.csrfKey(), csrf.Secure(false))
// signal to the CSRF middleware that the request is being served over
// plaintext HTTP to skip TLS-only header checks.
withSetPlaintext := func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r = csrf.PlaintextHTTPRequest(r)
h.ServeHTTP(w, r)
})
}
switch s.mode {
case LoginServerMode:
s.apiHandler = csrfProtect(withSetPlaintext(http.HandlerFunc(s.serveLoginAPI)))
metric = "web_login_client_initialization"
case ReadOnlyServerMode:
s.apiHandler = csrfProtect(withSetPlaintext(http.HandlerFunc(s.serveLoginAPI)))
metric = "web_readonly_client_initialization"
case ManageServerMode:
s.apiHandler = csrfProtect(withSetPlaintext(http.HandlerFunc(s.serveAPI)))
metric = "web_client_initialization"
}
// Don't block startup on reporting metric.
// Report in separate go routine with 5 second timeout.
@@ -218,39 +244,6 @@ func NewServer(opts ServerOpts) (s *Server, err error) {
return s, nil
}
func (s *Server) withCSRF(h http.Handler) http.Handler {
csrfProtect := csrf.Protect(s.csrfKey(), csrf.Secure(false))
// ref https://github.com/tailscale/tailscale/pull/14822
// signal to the CSRF middleware that the request is being served over
// plaintext HTTP to skip TLS-only header checks.
withSetPlaintext := func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r = csrf.PlaintextHTTPRequest(r)
h.ServeHTTP(w, r)
})
}
// NB: the order of the withSetPlaintext and csrfProtect calls is important
// to ensure that we signal to the CSRF middleware that the request is being
// served over plaintext HTTP and not over TLS as it presumes by default.
return withSetPlaintext(csrfProtect(h))
}
func (s *Server) modeAPIHandler(mode ServerMode) (http.Handler, string) {
switch mode {
case LoginServerMode:
return http.HandlerFunc(s.serveLoginAPI), "web_login_client_initialization"
case ReadOnlyServerMode:
return http.HandlerFunc(s.serveLoginAPI), "web_readonly_client_initialization"
case ManageServerMode:
return http.HandlerFunc(s.serveAPI), "web_client_initialization"
default: // invalid mode
log.Fatalf("invalid mode: %v", mode)
}
return nil, ""
}
func (s *Server) Shutdown() {
s.logf("web.Server: shutting down")
if s.assetsCleanup != nil {

View File

@@ -11,7 +11,6 @@ import (
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"net/http/httptest"
"net/netip"
"net/url"
@@ -21,7 +20,6 @@ import (
"time"
"github.com/google/go-cmp/cmp"
"github.com/gorilla/csrf"
"tailscale.com/client/local"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn"
@@ -1479,83 +1477,3 @@ func mockWaitAuthURL(_ context.Context, id string, src tailcfg.NodeID) (*tailcfg
return nil, errors.New("unknown id")
}
}
func TestCSRFProtect(t *testing.T) {
s := &Server{}
mux := http.NewServeMux()
mux.HandleFunc("GET /test/csrf-token", func(w http.ResponseWriter, r *http.Request) {
token := csrf.Token(r)
_, err := io.WriteString(w, token)
if err != nil {
t.Fatal(err)
}
})
mux.HandleFunc("POST /test/csrf-protected", func(w http.ResponseWriter, r *http.Request) {
_, err := io.WriteString(w, "ok")
if err != nil {
t.Fatal(err)
}
})
h := s.withCSRF(mux)
ser := httptest.NewServer(h)
defer ser.Close()
jar, err := cookiejar.New(nil)
if err != nil {
t.Fatalf("unable to construct cookie jar: %v", err)
}
client := ser.Client()
client.Jar = jar
// make GET request to populate cookie jar
resp, err := client.Get(ser.URL + "/test/csrf-token")
if err != nil {
t.Fatalf("unable to make request: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("unexpected status: %v", resp.Status)
}
tokenBytes, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("unable to read body: %v", err)
}
csrfToken := strings.TrimSpace(string(tokenBytes))
if csrfToken == "" {
t.Fatal("empty csrf token")
}
// make a POST request without the CSRF header; ensure it fails
resp, err = client.Post(ser.URL+"/test/csrf-protected", "text/plain", nil)
if err != nil {
t.Fatalf("unable to make request: %v", err)
}
if resp.StatusCode != http.StatusForbidden {
t.Fatalf("unexpected status: %v", resp.Status)
}
// make a POST request with the CSRF header; ensure it succeeds
req, err := http.NewRequest("POST", ser.URL+"/test/csrf-protected", nil)
if err != nil {
t.Fatalf("error building request: %v", err)
}
req.Header.Set("X-CSRF-Token", csrfToken)
resp, err = client.Do(req)
if err != nil {
t.Fatalf("unable to make request: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("unexpected status: %v", resp.Status)
}
defer resp.Body.Close()
out, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("unable to read body: %v", err)
}
if string(out) != "ok" {
t.Fatalf("unexpected body: %q", out)
}
}

View File

@@ -191,11 +191,13 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
golang.org/x/crypto/curve25519 from golang.org/x/crypto/nacl/box+
golang.org/x/crypto/hkdf from crypto/tls+
golang.org/x/crypto/internal/alias from golang.org/x/crypto/chacha20+
golang.org/x/crypto/internal/poly1305 from golang.org/x/crypto/chacha20poly1305+
golang.org/x/crypto/nacl/box from tailscale.com/types/key
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
golang.org/x/crypto/sha3 from crypto/internal/mlkem768+
W golang.org/x/exp/constraints from tailscale.com/util/winutil
golang.org/x/exp/maps from tailscale.com/util/syspolicy/setting+
L golang.org/x/net/bpf from github.com/mdlayher/netlink+
@@ -228,7 +230,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
container/list from crypto/tls+
context from crypto/tls+
crypto from crypto/ecdh+
crypto/aes from crypto/internal/hpke+
crypto/aes from crypto/ecdsa+
crypto/cipher from crypto/aes+
crypto/des from crypto/tls+
crypto/dsa from crypto/x509
@@ -237,58 +239,31 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
crypto/ed25519 from crypto/tls+
crypto/elliptic from crypto/ecdsa+
crypto/hmac from crypto/tls+
crypto/internal/alias from crypto/aes+
crypto/internal/bigmod from crypto/ecdsa+
crypto/internal/boring from crypto/aes+
crypto/internal/boring/bbig from crypto/ecdsa+
crypto/internal/boring/sig from crypto/internal/boring
crypto/internal/entropy from crypto/internal/fips140/drbg
crypto/internal/fips140 from crypto/internal/fips140/aes+
crypto/internal/fips140/aes from crypto/aes+
crypto/internal/fips140/aes/gcm from crypto/cipher+
crypto/internal/fips140/alias from crypto/cipher+
crypto/internal/fips140/bigmod from crypto/internal/fips140/ecdsa+
crypto/internal/fips140/check from crypto/internal/fips140/aes+
crypto/internal/fips140/drbg from crypto/internal/fips140/aes/gcm+
crypto/internal/fips140/ecdh from crypto/ecdh
crypto/internal/fips140/ecdsa from crypto/ecdsa
crypto/internal/fips140/ed25519 from crypto/ed25519
crypto/internal/fips140/edwards25519 from crypto/internal/fips140/ed25519
crypto/internal/fips140/edwards25519/field from crypto/ecdh+
crypto/internal/fips140/hkdf from crypto/internal/fips140/tls13+
crypto/internal/fips140/hmac from crypto/hmac+
crypto/internal/fips140/mlkem from crypto/tls
crypto/internal/fips140/nistec from crypto/elliptic+
crypto/internal/fips140/nistec/fiat from crypto/internal/fips140/nistec
crypto/internal/fips140/rsa from crypto/rsa
crypto/internal/fips140/sha256 from crypto/internal/fips140/check+
crypto/internal/fips140/sha3 from crypto/internal/fips140/hmac+
crypto/internal/fips140/sha512 from crypto/internal/fips140/ecdsa+
crypto/internal/fips140/subtle from crypto/internal/fips140/aes+
crypto/internal/fips140/tls12 from crypto/tls
crypto/internal/fips140/tls13 from crypto/tls
crypto/internal/fips140deps/byteorder from crypto/internal/fips140/aes+
crypto/internal/fips140deps/cpu from crypto/internal/fips140/aes+
crypto/internal/fips140deps/godebug from crypto/internal/fips140+
crypto/internal/fips140hash from crypto/ecdsa+
crypto/internal/fips140only from crypto/cipher+
crypto/internal/edwards25519 from crypto/ed25519
crypto/internal/edwards25519/field from crypto/ecdh+
crypto/internal/hpke from crypto/tls
crypto/internal/impl from crypto/internal/fips140/aes+
crypto/internal/mlkem768 from crypto/tls
crypto/internal/nistec from crypto/ecdh+
crypto/internal/nistec/fiat from crypto/internal/nistec
crypto/internal/randutil from crypto/dsa+
crypto/internal/sysrand from crypto/internal/entropy+
crypto/md5 from crypto/tls+
crypto/rand from crypto/ed25519+
crypto/rc4 from crypto/tls
crypto/rsa from crypto/tls+
crypto/sha1 from crypto/tls+
crypto/sha256 from crypto/tls+
crypto/sha3 from crypto/internal/fips140hash
crypto/sha512 from crypto/ecdsa+
crypto/subtle from crypto/cipher+
crypto/subtle from crypto/aes+
crypto/tls from golang.org/x/crypto/acme+
crypto/tls/internal/fips140tls from crypto/tls
crypto/x509 from crypto/tls+
D crypto/x509/internal/macos from crypto/x509
crypto/x509/pkix from crypto/x509+
embed from google.golang.org/protobuf/internal/editiondefaults+
embed from crypto/internal/nistec+
encoding from encoding/json+
encoding/asn1 from crypto/x509+
encoding/base32 from github.com/fxamacker/cbor/v2+
@@ -309,22 +284,23 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
html from net/http/pprof+
html/template from tailscale.com/cmd/derper
internal/abi from crypto/x509/internal/macos+
internal/asan from syscall+
internal/asan from syscall
internal/bisect from internal/godebug
internal/bytealg from bytes+
internal/byteorder from crypto/cipher+
internal/byteorder from crypto/aes+
internal/chacha8rand from math/rand/v2+
internal/concurrent from unique
internal/coverage/rtcov from runtime
internal/cpu from crypto/internal/fips140deps/cpu+
internal/cpu from crypto/aes+
internal/filepathlite from os+
internal/fmtsort from fmt+
internal/goarch from crypto/internal/fips140deps/cpu+
internal/goarch from crypto/aes+
internal/godebug from crypto/tls+
internal/godebugs from internal/godebug+
internal/goexperiment from runtime+
internal/goexperiment from runtime
internal/goos from crypto/x509+
internal/itoa from internal/poll+
internal/msan from syscall+
internal/msan from syscall
internal/nettrace from net+
internal/oserror from io/fs+
internal/poll from net+
@@ -334,20 +310,17 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
internal/reflectlite from context+
internal/runtime/atomic from internal/runtime/exithook+
internal/runtime/exithook from runtime
internal/runtime/maps from reflect+
internal/runtime/math from internal/runtime/maps+
internal/runtime/sys from crypto/subtle+
L internal/runtime/syscall from runtime+
internal/singleflight from net
internal/stringslite from embed+
internal/sync from sync+
internal/syscall/execenv from os+
LD internal/syscall/unix from crypto/internal/sysrand+
W internal/syscall/windows from crypto/internal/sysrand+
LD internal/syscall/unix from crypto/rand+
W internal/syscall/windows from crypto/rand+
W internal/syscall/windows/registry from mime+
W internal/syscall/windows/sysdll from internal/syscall/windows+
internal/testlog from os
internal/unsafeheader from internal/reflectlite+
internal/weak from unique
io from bufio+
io/fs from crypto/x509+
L io/ioutil from github.com/mitchellh/go-ps+
@@ -359,7 +332,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
math/big from crypto/dsa+
math/bits from compress/flate+
math/rand from github.com/mdlayher/netlink+
math/rand/v2 from crypto/ecdsa+
math/rand/v2 from internal/concurrent+
mime from github.com/prometheus/common/expfmt+
mime/multipart from net/http
mime/quotedprintable from mime/multipart
@@ -372,7 +345,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
net/netip from go4.org/netipx+
net/textproto from golang.org/x/net/http/httpguts+
net/url from crypto/x509+
os from crypto/internal/sysrand+
os from crypto/rand+
os/exec from github.com/coreos/go-iptables/iptables+
os/signal from tailscale.com/cmd/derper
W os/user from tailscale.com/util/winutil+
@@ -381,8 +354,10 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
reflect from crypto/x509+
regexp from github.com/coreos/go-iptables/iptables+
regexp/syntax from regexp
runtime from crypto/internal/fips140+
runtime from crypto/internal/nistec+
runtime/debug from github.com/prometheus/client_golang/prometheus+
runtime/internal/math from runtime
runtime/internal/sys from runtime
runtime/metrics from github.com/prometheus/client_golang/prometheus+
runtime/pprof from net/http/pprof
runtime/trace from net/http/pprof
@@ -392,7 +367,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
strings from bufio+
sync from compress/flate+
sync/atomic from context+
syscall from crypto/internal/sysrand+
syscall from crypto/rand+
text/tabwriter from runtime/pprof
text/template from html/template
text/template/parse from html/template+
@@ -402,4 +377,3 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
unicode/utf8 from bufio+
unique from net/netip
unsafe from bytes+
weak from unique

View File

@@ -71,13 +71,10 @@ var (
secretsCacheDir = flag.String("secrets-cache-dir", defaultSetecCacheDir(), "directory to cache setec secrets in (required if --secrets-url is set)")
bootstrapDNS = flag.String("bootstrap-dns-names", "", "optional comma-separated list of hostnames to make available at /bootstrap-dns")
unpublishedDNS = flag.String("unpublished-bootstrap-dns-names", "", "optional comma-separated list of hostnames to make available at /bootstrap-dns and not publish in the list. If an entry contains a slash, the second part names a DNS record to poll for its TXT record with a `0` to `100` value for rollout percentage.")
verifyClients = flag.Bool("verify-clients", false, "verify clients to this DERP server through a local tailscaled instance.")
verifyClientURL = flag.String("verify-client-url", "", "if non-empty, an admission controller URL for permitting client connections; see tailcfg.DERPAdmitClientRequest")
verifyFailOpen = flag.Bool("verify-client-url-fail-open", true, "whether we fail open if --verify-client-url is unreachable")
socket = flag.String("socket", "", "optional alternate path to tailscaled socket (only relevant when using --verify-clients)")
acceptConnLimit = flag.Float64("accept-connection-limit", math.Inf(+1), "rate limit for accepting new connection")
acceptConnBurst = flag.Int("accept-connection-burst", math.MaxInt, "burst limit for accepting new connection")
@@ -195,7 +192,6 @@ func main() {
s := derp.NewServer(cfg.PrivateKey, log.Printf)
s.SetVerifyClient(*verifyClients)
s.SetTailscaledSocketPath(*socket)
s.SetVerifyClientURL(*verifyClientURL)
s.SetVerifyClientURLFailOpen(*verifyFailOpen)
s.SetTCPWriteTimeout(*tcpWriteTimeout)
@@ -328,9 +324,6 @@ func main() {
Control: ktimeout.UserTimeout(*tcpUserTimeout),
KeepAlive: *tcpKeepAlive,
}
// As of 2025-02-19, MPTCP does not support TCP_USER_TIMEOUT socket option
// set in ktimeout.UserTimeout above.
lc.SetMultipathTCP(false)
quietLogger := log.New(logger.HTTPServerLogFilter{Inner: log.Printf}, "", 0)
httpsrv := &http.Server{

View File

@@ -13,7 +13,6 @@ import (
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
@@ -406,8 +405,7 @@ func getACLETag(ctx context.Context, client *http.Client, tailnet, apiKey string
got := resp.StatusCode
want := http.StatusOK
if got != want {
errorDetails, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("wanted HTTP status code %d but got %d: %#q", want, got, string(errorDetails))
return "", fmt.Errorf("wanted HTTP status code %d but got %d", want, got)
}
return Shuck(resp.Header.Get("ETag")), nil

View File

@@ -997,13 +997,14 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
golang.org/x/crypto/curve25519 from golang.org/x/crypto/ssh+
golang.org/x/crypto/hkdf from tailscale.com/control/controlbase
golang.org/x/crypto/hkdf from crypto/tls+
golang.org/x/crypto/internal/alias from golang.org/x/crypto/chacha20+
golang.org/x/crypto/internal/poly1305 from golang.org/x/crypto/chacha20poly1305+
golang.org/x/crypto/nacl/box from tailscale.com/types/key
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
golang.org/x/crypto/sha3 from crypto/internal/mlkem768+
LD golang.org/x/crypto/ssh from tailscale.com/ipn/ipnlocal
LD golang.org/x/crypto/ssh/internal/bcrypt_pbkdf from golang.org/x/crypto/ssh
golang.org/x/exp/constraints from github.com/dblohm7/wingoes/pe+
@@ -1054,7 +1055,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
container/list from crypto/tls+
context from crypto/tls+
crypto from crypto/ecdh+
crypto/aes from crypto/internal/hpke+
crypto/aes from crypto/ecdsa+
crypto/cipher from crypto/aes+
crypto/des from crypto/tls+
crypto/dsa from crypto/x509+
@@ -1063,54 +1064,27 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
crypto/ed25519 from crypto/tls+
crypto/elliptic from crypto/ecdsa+
crypto/hmac from crypto/tls+
crypto/internal/alias from crypto/aes+
crypto/internal/bigmod from crypto/ecdsa+
crypto/internal/boring from crypto/aes+
crypto/internal/boring/bbig from crypto/ecdsa+
crypto/internal/boring/sig from crypto/internal/boring
crypto/internal/entropy from crypto/internal/fips140/drbg
crypto/internal/fips140 from crypto/internal/fips140/aes+
crypto/internal/fips140/aes from crypto/aes+
crypto/internal/fips140/aes/gcm from crypto/cipher+
crypto/internal/fips140/alias from crypto/cipher+
crypto/internal/fips140/bigmod from crypto/internal/fips140/ecdsa+
crypto/internal/fips140/check from crypto/internal/fips140/aes+
crypto/internal/fips140/drbg from crypto/internal/fips140/aes/gcm+
crypto/internal/fips140/ecdh from crypto/ecdh
crypto/internal/fips140/ecdsa from crypto/ecdsa
crypto/internal/fips140/ed25519 from crypto/ed25519
crypto/internal/fips140/edwards25519 from crypto/internal/fips140/ed25519
crypto/internal/fips140/edwards25519/field from crypto/ecdh+
crypto/internal/fips140/hkdf from crypto/internal/fips140/tls13+
crypto/internal/fips140/hmac from crypto/hmac+
crypto/internal/fips140/mlkem from crypto/tls
crypto/internal/fips140/nistec from crypto/elliptic+
crypto/internal/fips140/nistec/fiat from crypto/internal/fips140/nistec
crypto/internal/fips140/rsa from crypto/rsa
crypto/internal/fips140/sha256 from crypto/internal/fips140/check+
crypto/internal/fips140/sha3 from crypto/internal/fips140/hmac+
crypto/internal/fips140/sha512 from crypto/internal/fips140/ecdsa+
crypto/internal/fips140/subtle from crypto/internal/fips140/aes+
crypto/internal/fips140/tls12 from crypto/tls
crypto/internal/fips140/tls13 from crypto/tls
crypto/internal/fips140deps/byteorder from crypto/internal/fips140/aes+
crypto/internal/fips140deps/cpu from crypto/internal/fips140/aes+
crypto/internal/fips140deps/godebug from crypto/internal/fips140+
crypto/internal/fips140hash from crypto/ecdsa+
crypto/internal/fips140only from crypto/cipher+
crypto/internal/edwards25519 from crypto/ed25519
crypto/internal/edwards25519/field from crypto/ecdh+
crypto/internal/hpke from crypto/tls
crypto/internal/impl from crypto/internal/fips140/aes+
crypto/internal/mlkem768 from crypto/tls
crypto/internal/nistec from crypto/ecdh+
crypto/internal/nistec/fiat from crypto/internal/nistec
crypto/internal/randutil from crypto/dsa+
crypto/internal/sysrand from crypto/internal/entropy+
crypto/md5 from crypto/tls+
crypto/rand from crypto/ed25519+
crypto/rc4 from crypto/tls+
crypto/rsa from crypto/tls+
crypto/sha1 from crypto/tls+
crypto/sha256 from crypto/tls+
crypto/sha3 from crypto/internal/fips140hash
crypto/sha512 from crypto/ecdsa+
crypto/subtle from crypto/cipher+
crypto/subtle from crypto/aes+
crypto/tls from github.com/aws/aws-sdk-go-v2/aws/transport/http+
crypto/tls/internal/fips140tls from crypto/tls
crypto/x509 from crypto/tls+
D crypto/x509/internal/macos from crypto/x509
crypto/x509/pkix from crypto/x509+
@@ -1118,7 +1092,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
database/sql/driver from database/sql+
W debug/dwarf from debug/pe
W debug/pe from github.com/dblohm7/wingoes/pe
embed from github.com/tailscale/web-client-prebuilt+
embed from crypto/internal/nistec+
encoding from encoding/gob+
encoding/asn1 from crypto/x509+
encoding/base32 from github.com/fxamacker/cbor/v2+
@@ -1138,6 +1112,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
go/build/constraint from go/parser
go/doc from k8s.io/apimachinery/pkg/runtime
go/doc/comment from go/doc
go/internal/typeparams from go/parser
go/parser from k8s.io/apimachinery/pkg/runtime
go/scanner from go/ast+
go/token from go/ast+
@@ -1149,23 +1124,24 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
html from html/template+
html/template from github.com/gorilla/csrf
internal/abi from crypto/x509/internal/macos+
internal/asan from syscall+
internal/asan from syscall
internal/bisect from internal/godebug
internal/bytealg from bytes+
internal/byteorder from crypto/cipher+
internal/byteorder from crypto/aes+
internal/chacha8rand from math/rand/v2+
internal/concurrent from unique
internal/coverage/rtcov from runtime
internal/cpu from crypto/internal/fips140deps/cpu+
internal/cpu from crypto/aes+
internal/filepathlite from os+
internal/fmtsort from fmt+
internal/goarch from crypto/internal/fips140deps/cpu+
internal/goarch from crypto/aes+
internal/godebug from archive/tar+
internal/godebugs from internal/godebug+
internal/goexperiment from runtime+
internal/goexperiment from runtime
internal/goos from crypto/x509+
internal/itoa from internal/poll+
internal/lazyregexp from go/doc
internal/msan from syscall+
internal/msan from syscall
internal/nettrace from net+
internal/oserror from io/fs+
internal/poll from net+
@@ -1175,21 +1151,18 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
internal/reflectlite from context+
internal/runtime/atomic from internal/runtime/exithook+
internal/runtime/exithook from runtime
internal/runtime/maps from reflect+
internal/runtime/math from internal/runtime/maps+
internal/runtime/sys from crypto/subtle+
L internal/runtime/syscall from runtime+
internal/saferio from debug/pe+
internal/singleflight from net
internal/stringslite from embed+
internal/sync from sync+
internal/syscall/execenv from os+
LD internal/syscall/unix from crypto/internal/sysrand+
W internal/syscall/windows from crypto/internal/sysrand+
LD internal/syscall/unix from crypto/rand+
W internal/syscall/windows from crypto/rand+
W internal/syscall/windows/registry from mime+
W internal/syscall/windows/sysdll from internal/syscall/windows+
internal/testlog from os
internal/unsafeheader from internal/reflectlite+
internal/weak from unique
io from archive/tar+
io/fs from archive/tar+
io/ioutil from github.com/aws/aws-sdk-go-v2/aws/protocol/query+
@@ -1218,7 +1191,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
net/netip from github.com/gaissmai/bart+
net/textproto from github.com/aws/aws-sdk-go-v2/aws/signer/v4+
net/url from crypto/x509+
os from crypto/internal/sysrand+
os from crypto/rand+
os/exec from github.com/aws/aws-sdk-go-v2/credentials/processcreds+
os/signal from sigs.k8s.io/controller-runtime/pkg/manager/signals
os/user from archive/tar+
@@ -1229,6 +1202,8 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
regexp/syntax from regexp
runtime from archive/tar+
runtime/debug from github.com/aws/aws-sdk-go-v2/internal/sync/singleflight+
runtime/internal/math from runtime
runtime/internal/sys from runtime
runtime/metrics from github.com/prometheus/client_golang/prometheus+
runtime/pprof from net/http/pprof+
runtime/trace from net/http/pprof
@@ -1248,4 +1223,3 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
unicode/utf8 from bufio+
unique from net/netip
unsafe from bytes+
weak from unique

View File

@@ -26,7 +26,7 @@ import (
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"tailscale.com/internal/client/tailscale"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
tsoperator "tailscale.com/k8s-operator"
@@ -186,7 +186,7 @@ func (a *IngressPGReconciler) maybeProvision(ctx context.Context, hostname strin
}
dnsName := hostname + "." + tcd
serviceName := tailcfg.ServiceName("svc:" + hostname)
existingVIPSvc, err := a.tsClient.GetVIPService(ctx, serviceName)
existingVIPSvc, err := a.tsClient.getVIPService(ctx, serviceName)
// TODO(irbekrm): here and when creating the VIPService, verify if the error is not terminal (and therefore
// should not be reconciled). For example, if the hostname is already a hostname of a Tailscale node, the GET
// here will fail.
@@ -269,7 +269,7 @@ func (a *IngressPGReconciler) maybeProvision(ctx context.Context, hostname strin
vipPorts = append(vipPorts, "80")
}
vipSvc := &tailscale.VIPService{
vipSvc := &VIPService{
Name: serviceName,
Tags: tags,
Ports: vipPorts,
@@ -282,7 +282,7 @@ func (a *IngressPGReconciler) maybeProvision(ctx context.Context, hostname strin
!reflect.DeepEqual(vipSvc.Tags, existingVIPSvc.Tags) ||
!reflect.DeepEqual(vipSvc.Ports, existingVIPSvc.Ports) {
logger.Infof("Ensuring VIPService %q exists and is up to date", hostname)
if err := a.tsClient.CreateOrUpdateVIPService(ctx, vipSvc); err != nil {
if err := a.tsClient.createOrUpdateVIPService(ctx, vipSvc); err != nil {
logger.Infof("error creating VIPService: %v", err)
return fmt.Errorf("error creating VIPService: %w", err)
}
@@ -361,7 +361,7 @@ func (a *IngressPGReconciler) maybeCleanupProxyGroup(ctx context.Context, proxyG
}
if isVIPServiceForAnyIngress(svc) {
logger.Infof("cleaning up orphaned VIPService %q", vipServiceName)
if err := a.tsClient.DeleteVIPService(ctx, vipServiceName); err != nil {
if err := a.tsClient.deleteVIPService(ctx, vipServiceName); err != nil {
errResp := &tailscale.ErrResponse{}
if !errors.As(err, &errResp) || errResp.Status != http.StatusNotFound {
return fmt.Errorf("deleting VIPService %q: %w", vipServiceName, err)
@@ -509,8 +509,8 @@ func (a *IngressPGReconciler) shouldExpose(ing *networkingv1.Ingress) bool {
return isTSIngress && pgAnnot != ""
}
func (a *IngressPGReconciler) getVIPService(ctx context.Context, name tailcfg.ServiceName, logger *zap.SugaredLogger) (*tailscale.VIPService, error) {
svc, err := a.tsClient.GetVIPService(ctx, name)
func (a *IngressPGReconciler) getVIPService(ctx context.Context, name tailcfg.ServiceName, logger *zap.SugaredLogger) (*VIPService, error) {
svc, err := a.tsClient.getVIPService(ctx, name)
if err != nil {
errResp := &tailscale.ErrResponse{}
if ok := errors.As(err, errResp); ok && errResp.Status != http.StatusNotFound {
@@ -521,14 +521,14 @@ func (a *IngressPGReconciler) getVIPService(ctx context.Context, name tailcfg.Se
return svc, nil
}
func isVIPServiceForIngress(svc *tailscale.VIPService, ing *networkingv1.Ingress) bool {
func isVIPServiceForIngress(svc *VIPService, ing *networkingv1.Ingress) bool {
if svc == nil || ing == nil {
return false
}
return strings.EqualFold(svc.Comment, fmt.Sprintf(VIPSvcOwnerRef, ing.UID))
}
func isVIPServiceForAnyIngress(svc *tailscale.VIPService) bool {
func isVIPServiceForAnyIngress(svc *VIPService) bool {
if svc == nil {
return false
}
@@ -593,7 +593,7 @@ func (a *IngressPGReconciler) deleteVIPServiceIfExists(ctx context.Context, name
}
logger.Infof("Deleting VIPService %q", name)
if err = a.tsClient.DeleteVIPService(ctx, name); err != nil {
if err = a.tsClient.deleteVIPService(ctx, name); err != nil {
return fmt.Errorf("error deleting VIPService: %w", err)
}
return nil

View File

@@ -70,7 +70,7 @@ func TestIngressPGReconciler(t *testing.T) {
expectReconciled(t, ingPGR, "default", "test-ingress")
// Verify VIPService uses custom tags
vipSvc, err := ft.GetVIPService(context.Background(), "svc:my-svc")
vipSvc, err := ft.getVIPService(context.Background(), "svc:my-svc")
if err != nil {
t.Fatalf("getting VIPService: %v", err)
}
@@ -398,7 +398,7 @@ func TestIngressPGReconciler_HTTPEndpoint(t *testing.T) {
func verifyVIPService(t *testing.T, ft *fakeTSClient, serviceName string, wantPorts []string) {
t.Helper()
vipSvc, err := ft.GetVIPService(context.Background(), tailcfg.ServiceName(serviceName))
vipSvc, err := ft.getVIPService(context.Background(), tailcfg.ServiceName(serviceName))
if err != nil {
t.Fatalf("getting VIPService %q: %v", serviceName, err)
}

View File

@@ -28,7 +28,7 @@ import (
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"tailscale.com/internal/client/tailscale"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
@@ -768,7 +768,7 @@ type fakeTSClient struct {
sync.Mutex
keyRequests []tailscale.KeyCapabilities
deleted []string
vipServices map[tailcfg.ServiceName]*tailscale.VIPService
vipServices map[tailcfg.ServiceName]*VIPService
}
type fakeTSNetServer struct {
certDomains []string
@@ -875,7 +875,7 @@ func removeAuthKeyIfExistsModifier(t *testing.T) func(s *corev1.Secret) {
}
}
func (c *fakeTSClient) GetVIPService(ctx context.Context, name tailcfg.ServiceName) (*tailscale.VIPService, error) {
func (c *fakeTSClient) getVIPService(ctx context.Context, name tailcfg.ServiceName) (*VIPService, error) {
c.Lock()
defer c.Unlock()
if c.vipServices == nil {
@@ -888,17 +888,17 @@ func (c *fakeTSClient) GetVIPService(ctx context.Context, name tailcfg.ServiceNa
return svc, nil
}
func (c *fakeTSClient) CreateOrUpdateVIPService(ctx context.Context, svc *tailscale.VIPService) error {
func (c *fakeTSClient) createOrUpdateVIPService(ctx context.Context, svc *VIPService) error {
c.Lock()
defer c.Unlock()
if c.vipServices == nil {
c.vipServices = make(map[tailcfg.ServiceName]*tailscale.VIPService)
c.vipServices = make(map[tailcfg.ServiceName]*VIPService)
}
c.vipServices[svc.Name] = svc
return nil
}
func (c *fakeTSClient) DeleteVIPService(ctx context.Context, name tailcfg.ServiceName) error {
func (c *fakeTSClient) deleteVIPService(ctx context.Context, name tailcfg.ServiceName) error {
c.Lock()
defer c.Unlock()
if c.vipServices != nil {

View File

@@ -6,13 +6,19 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"golang.org/x/oauth2/clientcredentials"
"tailscale.com/internal/client/tailscale"
"tailscale.com/tailcfg"
"tailscale.com/util/httpm"
)
// defaultTailnet is a value that can be used in Tailscale API calls instead of tailnet name to indicate that the API
@@ -39,14 +45,141 @@ func newTSClient(ctx context.Context, clientIDPath, clientSecretPath string) (ts
c := tailscale.NewClient(defaultTailnet, nil)
c.UserAgent = "tailscale-k8s-operator"
c.HTTPClient = credentials.Client(ctx)
return c, nil
tsc := &tsClientImpl{
Client: c,
baseURL: defaultBaseURL,
tailnet: defaultTailnet,
}
return tsc, nil
}
type tsClient interface {
CreateKey(ctx context.Context, caps tailscale.KeyCapabilities) (string, *tailscale.Key, error)
Device(ctx context.Context, deviceID string, fields *tailscale.DeviceFieldsOpts) (*tailscale.Device, error)
DeleteDevice(ctx context.Context, nodeStableID string) error
GetVIPService(ctx context.Context, name tailcfg.ServiceName) (*tailscale.VIPService, error)
CreateOrUpdateVIPService(ctx context.Context, svc *tailscale.VIPService) error
DeleteVIPService(ctx context.Context, name tailcfg.ServiceName) error
getVIPService(ctx context.Context, name tailcfg.ServiceName) (*VIPService, error)
createOrUpdateVIPService(ctx context.Context, svc *VIPService) error
deleteVIPService(ctx context.Context, name tailcfg.ServiceName) error
}
type tsClientImpl struct {
*tailscale.Client
baseURL string
tailnet string
}
// VIPService is a Tailscale VIPService with Tailscale API JSON representation.
type VIPService struct {
// Name is a VIPService name in form svc:<leftmost-label-of-service-DNS-name>.
Name tailcfg.ServiceName `json:"name,omitempty"`
// Addrs are the IP addresses of the VIP Service. There are two addresses:
// the first is IPv4 and the second is IPv6.
// When creating a new VIP Service, the IP addresses are optional: if no
// addresses are specified then they will be selected. If an IPv4 address is
// specified at index 0, then that address will attempt to be used. An IPv6
// address can not be specified upon creation.
Addrs []string `json:"addrs,omitempty"`
// Comment is an optional text string for display in the admin panel.
Comment string `json:"comment,omitempty"`
// Ports are the ports of a VIPService that will be configured via Tailscale serve config.
// If set, any node wishing to advertise this VIPService must have this port configured via Tailscale serve.
Ports []string `json:"ports,omitempty"`
// Tags are optional ACL tags that will be applied to the VIPService.
Tags []string `json:"tags,omitempty"`
}
// GetVIPServiceByName retrieves a VIPService by its name. It returns 404 if the VIPService is not found.
func (c *tsClientImpl) getVIPService(ctx context.Context, name tailcfg.ServiceName) (*VIPService, error) {
path := fmt.Sprintf("%s/api/v2/tailnet/%s/vip-services/%s", c.baseURL, c.tailnet, url.PathEscape(name.String()))
req, err := http.NewRequestWithContext(ctx, httpm.GET, path, nil)
if err != nil {
return nil, fmt.Errorf("error creating new HTTP request: %w", err)
}
b, resp, err := c.sendRequest(req)
if err != nil {
return nil, fmt.Errorf("error making Tailsale API request: %w", err)
}
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
}
svc := &VIPService{}
if err := json.Unmarshal(b, svc); err != nil {
return nil, err
}
return svc, nil
}
// createOrUpdateVIPService creates or updates a VIPService by its name. Caller must ensure that, if the
// VIPService already exists, the VIPService is fetched first to ensure that any auto-allocated IP addresses are not
// lost during the update. If the VIPService was created without any IP addresses explicitly set (so that they were
// auto-allocated by Tailscale) any subsequent request to this function that does not set any IP addresses will error.
func (c *tsClientImpl) createOrUpdateVIPService(ctx context.Context, svc *VIPService) error {
data, err := json.Marshal(svc)
if err != nil {
return err
}
path := fmt.Sprintf("%s/api/v2/tailnet/%s/vip-services/%s", c.baseURL, c.tailnet, url.PathEscape(svc.Name.String()))
req, err := http.NewRequestWithContext(ctx, httpm.PUT, path, bytes.NewBuffer(data))
if err != nil {
return fmt.Errorf("error creating new HTTP request: %w", err)
}
b, resp, err := c.sendRequest(req)
if err != nil {
return fmt.Errorf("error making Tailscale API request: %w", err)
}
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return handleErrorResponse(b, resp)
}
return nil
}
// DeleteVIPServiceByName deletes a VIPService by its name. It returns an error if the VIPService
// does not exist or if the deletion fails.
func (c *tsClientImpl) deleteVIPService(ctx context.Context, name tailcfg.ServiceName) error {
path := fmt.Sprintf("%s/api/v2/tailnet/%s/vip-services/%s", c.baseURL, c.tailnet, url.PathEscape(name.String()))
req, err := http.NewRequestWithContext(ctx, httpm.DELETE, path, nil)
if err != nil {
return fmt.Errorf("error creating new HTTP request: %w", err)
}
b, resp, err := c.sendRequest(req)
if err != nil {
return fmt.Errorf("error making Tailscale API request: %w", err)
}
// If status code was not successful, return the error.
if resp.StatusCode != http.StatusOK {
return handleErrorResponse(b, resp)
}
return nil
}
// sendRequest add the authentication key to the request and sends it. It
// receives the response and reads up to 10MB of it.
func (c *tsClientImpl) sendRequest(req *http.Request) ([]byte, *http.Response, error) {
resp, err := c.Do(req)
if err != nil {
return nil, resp, fmt.Errorf("error actually doing request: %w", err)
}
defer resp.Body.Close()
// Read response
b, err := io.ReadAll(resp.Body)
if err != nil {
err = fmt.Errorf("error reading response body: %v", err)
}
return b, resp, err
}
// handleErrorResponse decodes the error message from the server and returns
// an ErrResponse from it.
func handleErrorResponse(b []byte, resp *http.Response) error {
var errResp tailscale.ErrResponse
if err := json.Unmarshal(b, &errResp); err != nil {
return err
}
errResp.Status = resp.StatusCode
return errResp
}

View File

@@ -88,11 +88,13 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
golang.org/x/crypto/curve25519 from golang.org/x/crypto/nacl/box+
golang.org/x/crypto/hkdf from crypto/tls+
golang.org/x/crypto/internal/alias from golang.org/x/crypto/chacha20+
golang.org/x/crypto/internal/poly1305 from golang.org/x/crypto/chacha20poly1305+
golang.org/x/crypto/nacl/box from tailscale.com/types/key
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
golang.org/x/crypto/sha3 from crypto/internal/mlkem768+
golang.org/x/net/dns/dnsmessage from net+
golang.org/x/net/http/httpguts from net/http
golang.org/x/net/http/httpproxy from net/http
@@ -114,7 +116,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
container/list from crypto/tls+
context from crypto/tls+
crypto from crypto/ecdh+
crypto/aes from crypto/internal/hpke+
crypto/aes from crypto/ecdsa+
crypto/cipher from crypto/aes+
crypto/des from crypto/tls+
crypto/dsa from crypto/x509
@@ -122,59 +124,32 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
crypto/ecdsa from crypto/tls+
crypto/ed25519 from crypto/tls+
crypto/elliptic from crypto/ecdsa+
crypto/hmac from crypto/tls
crypto/hmac from crypto/tls+
crypto/internal/alias from crypto/aes+
crypto/internal/bigmod from crypto/ecdsa+
crypto/internal/boring from crypto/aes+
crypto/internal/boring/bbig from crypto/ecdsa+
crypto/internal/boring/sig from crypto/internal/boring
crypto/internal/entropy from crypto/internal/fips140/drbg
crypto/internal/fips140 from crypto/internal/fips140/aes+
crypto/internal/fips140/aes from crypto/aes+
crypto/internal/fips140/aes/gcm from crypto/cipher+
crypto/internal/fips140/alias from crypto/cipher+
crypto/internal/fips140/bigmod from crypto/internal/fips140/ecdsa+
crypto/internal/fips140/check from crypto/internal/fips140/aes+
crypto/internal/fips140/drbg from crypto/internal/fips140/aes/gcm+
crypto/internal/fips140/ecdh from crypto/ecdh
crypto/internal/fips140/ecdsa from crypto/ecdsa
crypto/internal/fips140/ed25519 from crypto/ed25519
crypto/internal/fips140/edwards25519 from crypto/internal/fips140/ed25519
crypto/internal/fips140/edwards25519/field from crypto/ecdh+
crypto/internal/fips140/hkdf from crypto/internal/fips140/tls13+
crypto/internal/fips140/hmac from crypto/hmac+
crypto/internal/fips140/mlkem from crypto/tls
crypto/internal/fips140/nistec from crypto/elliptic+
crypto/internal/fips140/nistec/fiat from crypto/internal/fips140/nistec
crypto/internal/fips140/rsa from crypto/rsa
crypto/internal/fips140/sha256 from crypto/internal/fips140/check+
crypto/internal/fips140/sha3 from crypto/internal/fips140/hmac+
crypto/internal/fips140/sha512 from crypto/internal/fips140/ecdsa+
crypto/internal/fips140/subtle from crypto/internal/fips140/aes+
crypto/internal/fips140/tls12 from crypto/tls
crypto/internal/fips140/tls13 from crypto/tls
crypto/internal/fips140deps/byteorder from crypto/internal/fips140/aes+
crypto/internal/fips140deps/cpu from crypto/internal/fips140/aes+
crypto/internal/fips140deps/godebug from crypto/internal/fips140+
crypto/internal/fips140hash from crypto/ecdsa+
crypto/internal/fips140only from crypto/cipher+
crypto/internal/edwards25519 from crypto/ed25519
crypto/internal/edwards25519/field from crypto/ecdh+
crypto/internal/hpke from crypto/tls
crypto/internal/impl from crypto/internal/fips140/aes+
crypto/internal/mlkem768 from crypto/tls
crypto/internal/nistec from crypto/ecdh+
crypto/internal/nistec/fiat from crypto/internal/nistec
crypto/internal/randutil from crypto/dsa+
crypto/internal/sysrand from crypto/internal/entropy+
crypto/md5 from crypto/tls+
crypto/rand from crypto/ed25519+
crypto/rc4 from crypto/tls
crypto/rsa from crypto/tls+
crypto/sha1 from crypto/tls+
crypto/sha256 from crypto/tls+
crypto/sha3 from crypto/internal/fips140hash
crypto/sha512 from crypto/ecdsa+
crypto/subtle from crypto/cipher+
crypto/subtle from crypto/aes+
crypto/tls from net/http+
crypto/tls/internal/fips140tls from crypto/tls
crypto/x509 from crypto/tls
D crypto/x509/internal/macos from crypto/x509
crypto/x509/pkix from crypto/x509
embed from google.golang.org/protobuf/internal/editiondefaults+
embed from crypto/internal/nistec+
encoding from encoding/json+
encoding/asn1 from crypto/x509+
encoding/base32 from github.com/go-json-experiment/json
@@ -194,22 +169,23 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
hash/maphash from go4.org/mem
html from net/http/pprof+
internal/abi from crypto/x509/internal/macos+
internal/asan from syscall+
internal/asan from syscall
internal/bisect from internal/godebug
internal/bytealg from bytes+
internal/byteorder from crypto/cipher+
internal/byteorder from crypto/aes+
internal/chacha8rand from math/rand/v2+
internal/concurrent from unique
internal/coverage/rtcov from runtime
internal/cpu from crypto/internal/fips140deps/cpu+
internal/cpu from crypto/aes+
internal/filepathlite from os+
internal/fmtsort from fmt
internal/goarch from crypto/internal/fips140deps/cpu+
internal/goarch from crypto/aes+
internal/godebug from crypto/tls+
internal/godebugs from internal/godebug+
internal/goexperiment from runtime+
internal/goexperiment from runtime
internal/goos from crypto/x509+
internal/itoa from internal/poll+
internal/msan from syscall+
internal/msan from syscall
internal/nettrace from net+
internal/oserror from io/fs+
internal/poll from net+
@@ -219,20 +195,17 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
internal/reflectlite from context+
internal/runtime/atomic from internal/runtime/exithook+
internal/runtime/exithook from runtime
internal/runtime/maps from reflect+
internal/runtime/math from internal/runtime/maps+
internal/runtime/sys from crypto/subtle+
L internal/runtime/syscall from runtime+
internal/singleflight from net
internal/stringslite from embed+
internal/sync from sync+
internal/syscall/execenv from os
LD internal/syscall/unix from crypto/internal/sysrand+
W internal/syscall/windows from crypto/internal/sysrand+
LD internal/syscall/unix from crypto/rand+
W internal/syscall/windows from crypto/rand+
W internal/syscall/windows/registry from mime+
W internal/syscall/windows/sysdll from internal/syscall/windows+
internal/testlog from os
internal/unsafeheader from internal/reflectlite+
internal/weak from unique
io from bufio+
io/fs from crypto/x509+
iter from maps+
@@ -243,7 +216,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
math/big from crypto/dsa+
math/bits from compress/flate+
math/rand from math/big+
math/rand/v2 from crypto/ecdsa+
math/rand/v2 from internal/concurrent+
mime from github.com/prometheus/common/expfmt+
mime/multipart from net/http
mime/quotedprintable from mime/multipart
@@ -256,15 +229,17 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
net/netip from go4.org/netipx+
net/textproto from golang.org/x/net/http/httpguts+
net/url from crypto/x509+
os from crypto/internal/sysrand+
os from crypto/rand+
os/signal from tailscale.com/cmd/stund
path from github.com/prometheus/client_golang/prometheus/internal+
path/filepath from crypto/x509+
reflect from crypto/x509+
regexp from github.com/prometheus/client_golang/prometheus/internal+
regexp/syntax from regexp
runtime from crypto/internal/fips140+
runtime from crypto/internal/nistec+
runtime/debug from github.com/prometheus/client_golang/prometheus+
runtime/internal/math from runtime
runtime/internal/sys from runtime
runtime/metrics from github.com/prometheus/client_golang/prometheus+
runtime/pprof from net/http/pprof
runtime/trace from net/http/pprof
@@ -274,7 +249,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
strings from bufio+
sync from compress/flate+
sync/atomic from context+
syscall from crypto/internal/sysrand+
syscall from crypto/rand+
text/tabwriter from runtime/pprof
time from compress/gzip+
unicode from bytes+
@@ -282,4 +257,3 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
unicode/utf8 from bufio+
unique from net/netip
unsafe from bytes+
weak from unique

View File

@@ -195,13 +195,14 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
golang.org/x/crypto/curve25519 from golang.org/x/crypto/nacl/box+
golang.org/x/crypto/hkdf from tailscale.com/control/controlbase
golang.org/x/crypto/hkdf from crypto/tls+
golang.org/x/crypto/internal/alias from golang.org/x/crypto/chacha20+
golang.org/x/crypto/internal/poly1305 from golang.org/x/crypto/chacha20poly1305+
golang.org/x/crypto/nacl/box from tailscale.com/types/key
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
golang.org/x/crypto/pbkdf2 from software.sslmate.com/src/go-pkcs12
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
golang.org/x/crypto/sha3 from crypto/internal/mlkem768+
W golang.org/x/exp/constraints from github.com/dblohm7/wingoes/pe+
golang.org/x/exp/maps from tailscale.com/util/syspolicy/internal/metrics+
golang.org/x/net/bpf from github.com/mdlayher/netlink+
@@ -245,7 +246,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
container/list from crypto/tls+
context from crypto/tls+
crypto from crypto/ecdh+
crypto/aes from crypto/internal/hpke+
crypto/aes from crypto/ecdsa+
crypto/cipher from crypto/aes+
crypto/des from crypto/tls+
crypto/dsa from crypto/x509
@@ -254,61 +255,34 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
crypto/ed25519 from crypto/tls+
crypto/elliptic from crypto/ecdsa+
crypto/hmac from crypto/tls+
crypto/internal/alias from crypto/aes+
crypto/internal/bigmod from crypto/ecdsa+
crypto/internal/boring from crypto/aes+
crypto/internal/boring/bbig from crypto/ecdsa+
crypto/internal/boring/sig from crypto/internal/boring
crypto/internal/entropy from crypto/internal/fips140/drbg
crypto/internal/fips140 from crypto/internal/fips140/aes+
crypto/internal/fips140/aes from crypto/aes+
crypto/internal/fips140/aes/gcm from crypto/cipher+
crypto/internal/fips140/alias from crypto/cipher+
crypto/internal/fips140/bigmod from crypto/internal/fips140/ecdsa+
crypto/internal/fips140/check from crypto/internal/fips140/aes+
crypto/internal/fips140/drbg from crypto/internal/fips140/aes/gcm+
crypto/internal/fips140/ecdh from crypto/ecdh
crypto/internal/fips140/ecdsa from crypto/ecdsa
crypto/internal/fips140/ed25519 from crypto/ed25519
crypto/internal/fips140/edwards25519 from crypto/internal/fips140/ed25519
crypto/internal/fips140/edwards25519/field from crypto/ecdh+
crypto/internal/fips140/hkdf from crypto/internal/fips140/tls13+
crypto/internal/fips140/hmac from crypto/hmac+
crypto/internal/fips140/mlkem from crypto/tls
crypto/internal/fips140/nistec from crypto/elliptic+
crypto/internal/fips140/nistec/fiat from crypto/internal/fips140/nistec
crypto/internal/fips140/rsa from crypto/rsa
crypto/internal/fips140/sha256 from crypto/internal/fips140/check+
crypto/internal/fips140/sha3 from crypto/internal/fips140/hmac+
crypto/internal/fips140/sha512 from crypto/internal/fips140/ecdsa+
crypto/internal/fips140/subtle from crypto/internal/fips140/aes+
crypto/internal/fips140/tls12 from crypto/tls
crypto/internal/fips140/tls13 from crypto/tls
crypto/internal/fips140deps/byteorder from crypto/internal/fips140/aes+
crypto/internal/fips140deps/cpu from crypto/internal/fips140/aes+
crypto/internal/fips140deps/godebug from crypto/internal/fips140+
crypto/internal/fips140hash from crypto/ecdsa+
crypto/internal/fips140only from crypto/cipher+
crypto/internal/edwards25519 from crypto/ed25519
crypto/internal/edwards25519/field from crypto/ecdh+
crypto/internal/hpke from crypto/tls
crypto/internal/impl from crypto/internal/fips140/aes+
crypto/internal/mlkem768 from crypto/tls
crypto/internal/nistec from crypto/ecdh+
crypto/internal/nistec/fiat from crypto/internal/nistec
crypto/internal/randutil from crypto/dsa+
crypto/internal/sysrand from crypto/internal/entropy+
crypto/md5 from crypto/tls+
crypto/rand from crypto/ed25519+
crypto/rc4 from crypto/tls
crypto/rsa from crypto/tls+
crypto/sha1 from crypto/tls+
crypto/sha256 from crypto/tls+
crypto/sha3 from crypto/internal/fips140hash
crypto/sha512 from crypto/ecdsa+
crypto/subtle from crypto/cipher+
crypto/subtle from crypto/aes+
crypto/tls from github.com/miekg/dns+
crypto/tls/internal/fips140tls from crypto/tls
crypto/x509 from crypto/tls+
D crypto/x509/internal/macos from crypto/x509
crypto/x509/pkix from crypto/x509+
DW database/sql/driver from github.com/google/uuid
W debug/dwarf from debug/pe
W debug/pe from github.com/dblohm7/wingoes/pe
embed from github.com/peterbourgon/ff/v3+
embed from crypto/internal/nistec+
encoding from encoding/gob+
encoding/asn1 from crypto/x509+
encoding/base32 from github.com/fxamacker/cbor/v2+
@@ -333,22 +307,23 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
image/color from github.com/skip2/go-qrcode+
image/png from github.com/skip2/go-qrcode
internal/abi from crypto/x509/internal/macos+
internal/asan from syscall+
internal/asan from syscall
internal/bisect from internal/godebug
internal/bytealg from bytes+
internal/byteorder from crypto/cipher+
internal/byteorder from crypto/aes+
internal/chacha8rand from math/rand/v2+
internal/concurrent from unique
internal/coverage/rtcov from runtime
internal/cpu from crypto/internal/fips140deps/cpu+
internal/cpu from crypto/aes+
internal/filepathlite from os+
internal/fmtsort from fmt+
internal/goarch from crypto/internal/fips140deps/cpu+
internal/goarch from crypto/aes+
internal/godebug from archive/tar+
internal/godebugs from internal/godebug+
internal/goexperiment from runtime+
internal/goexperiment from runtime
internal/goos from crypto/x509+
internal/itoa from internal/poll+
internal/msan from syscall+
internal/msan from syscall
internal/nettrace from net+
internal/oserror from io/fs+
internal/poll from net+
@@ -357,21 +332,18 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
internal/reflectlite from context+
internal/runtime/atomic from internal/runtime/exithook+
internal/runtime/exithook from runtime
internal/runtime/maps from reflect+
internal/runtime/math from internal/runtime/maps+
internal/runtime/sys from crypto/subtle+
L internal/runtime/syscall from runtime+
internal/saferio from debug/pe+
internal/singleflight from net
internal/stringslite from embed+
internal/sync from sync+
internal/syscall/execenv from os+
LD internal/syscall/unix from crypto/internal/sysrand+
W internal/syscall/windows from crypto/internal/sysrand+
LD internal/syscall/unix from crypto/rand+
W internal/syscall/windows from crypto/rand+
W internal/syscall/windows/registry from mime+
W internal/syscall/windows/sysdll from internal/syscall/windows+
internal/testlog from os
internal/unsafeheader from internal/reflectlite+
internal/weak from unique
io from archive/tar+
io/fs from archive/tar+
io/ioutil from github.com/mitchellh/go-ps+
@@ -397,7 +369,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
net/netip from go4.org/netipx+
net/textproto from golang.org/x/net/http/httpguts+
net/url from crypto/x509+
os from crypto/internal/sysrand+
os from crypto/rand+
os/exec from github.com/coreos/go-iptables/iptables+
os/signal from tailscale.com/cmd/tailscale/cli
os/user from archive/tar+
@@ -408,6 +380,8 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
regexp/syntax from regexp
runtime from archive/tar+
runtime/debug from tailscale.com+
runtime/internal/math from runtime
runtime/internal/sys from runtime
slices from tailscale.com/client/web+
sort from compress/flate+
strconv from archive/tar+
@@ -424,4 +398,3 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
unicode/utf8 from bufio+
unique from net/netip
unsafe from bytes+
weak from unique

View File

@@ -449,13 +449,14 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
golang.org/x/crypto/curve25519 from golang.org/x/crypto/ssh+
golang.org/x/crypto/hkdf from tailscale.com/control/controlbase
golang.org/x/crypto/hkdf from crypto/tls+
golang.org/x/crypto/internal/alias from golang.org/x/crypto/chacha20+
golang.org/x/crypto/internal/poly1305 from golang.org/x/crypto/chacha20poly1305+
golang.org/x/crypto/nacl/box from tailscale.com/types/key
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
golang.org/x/crypto/sha3 from crypto/internal/mlkem768+
LD golang.org/x/crypto/ssh from github.com/pkg/sftp+
LD golang.org/x/crypto/ssh/internal/bcrypt_pbkdf from golang.org/x/crypto/ssh
golang.org/x/exp/constraints from github.com/dblohm7/wingoes/pe+
@@ -503,7 +504,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
container/list from crypto/tls+
context from crypto/tls+
crypto from crypto/ecdh+
crypto/aes from crypto/internal/hpke+
crypto/aes from crypto/ecdsa+
crypto/cipher from crypto/aes+
crypto/des from crypto/tls+
crypto/dsa from crypto/x509+
@@ -512,61 +513,34 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
crypto/ed25519 from crypto/tls+
crypto/elliptic from crypto/ecdsa+
crypto/hmac from crypto/tls+
crypto/internal/alias from crypto/aes+
crypto/internal/bigmod from crypto/ecdsa+
crypto/internal/boring from crypto/aes+
crypto/internal/boring/bbig from crypto/ecdsa+
crypto/internal/boring/sig from crypto/internal/boring
crypto/internal/entropy from crypto/internal/fips140/drbg
crypto/internal/fips140 from crypto/internal/fips140/aes+
crypto/internal/fips140/aes from crypto/aes+
crypto/internal/fips140/aes/gcm from crypto/cipher+
crypto/internal/fips140/alias from crypto/cipher+
crypto/internal/fips140/bigmod from crypto/internal/fips140/ecdsa+
crypto/internal/fips140/check from crypto/internal/fips140/aes+
crypto/internal/fips140/drbg from crypto/internal/fips140/aes/gcm+
crypto/internal/fips140/ecdh from crypto/ecdh
crypto/internal/fips140/ecdsa from crypto/ecdsa
crypto/internal/fips140/ed25519 from crypto/ed25519
crypto/internal/fips140/edwards25519 from crypto/internal/fips140/ed25519
crypto/internal/fips140/edwards25519/field from crypto/ecdh+
crypto/internal/fips140/hkdf from crypto/internal/fips140/tls13+
crypto/internal/fips140/hmac from crypto/hmac+
crypto/internal/fips140/mlkem from crypto/tls
crypto/internal/fips140/nistec from crypto/elliptic+
crypto/internal/fips140/nistec/fiat from crypto/internal/fips140/nistec
crypto/internal/fips140/rsa from crypto/rsa
crypto/internal/fips140/sha256 from crypto/internal/fips140/check+
crypto/internal/fips140/sha3 from crypto/internal/fips140/hmac+
crypto/internal/fips140/sha512 from crypto/internal/fips140/ecdsa+
crypto/internal/fips140/subtle from crypto/internal/fips140/aes+
crypto/internal/fips140/tls12 from crypto/tls
crypto/internal/fips140/tls13 from crypto/tls
crypto/internal/fips140deps/byteorder from crypto/internal/fips140/aes+
crypto/internal/fips140deps/cpu from crypto/internal/fips140/aes+
crypto/internal/fips140deps/godebug from crypto/internal/fips140+
crypto/internal/fips140hash from crypto/ecdsa+
crypto/internal/fips140only from crypto/cipher+
crypto/internal/edwards25519 from crypto/ed25519
crypto/internal/edwards25519/field from crypto/ecdh+
crypto/internal/hpke from crypto/tls
crypto/internal/impl from crypto/internal/fips140/aes+
crypto/internal/mlkem768 from crypto/tls
crypto/internal/nistec from crypto/ecdh+
crypto/internal/nistec/fiat from crypto/internal/nistec
crypto/internal/randutil from crypto/dsa+
crypto/internal/sysrand from crypto/internal/entropy+
crypto/md5 from crypto/tls+
crypto/rand from crypto/ed25519+
crypto/rc4 from crypto/tls+
crypto/rsa from crypto/tls+
crypto/sha1 from crypto/tls+
crypto/sha256 from crypto/tls+
crypto/sha3 from crypto/internal/fips140hash
crypto/sha512 from crypto/ecdsa+
crypto/subtle from crypto/cipher+
crypto/subtle from crypto/aes+
crypto/tls from github.com/aws/aws-sdk-go-v2/aws/transport/http+
crypto/tls/internal/fips140tls from crypto/tls
crypto/x509 from crypto/tls+
D crypto/x509/internal/macos from crypto/x509
crypto/x509/pkix from crypto/x509+
DW database/sql/driver from github.com/google/uuid
W debug/dwarf from debug/pe
W debug/pe from github.com/dblohm7/wingoes/pe
embed from github.com/tailscale/web-client-prebuilt+
embed from crypto/internal/nistec+
encoding from encoding/gob+
encoding/asn1 from crypto/x509+
encoding/base32 from github.com/fxamacker/cbor/v2+
@@ -588,22 +562,23 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
html from html/template+
html/template from github.com/gorilla/csrf
internal/abi from crypto/x509/internal/macos+
internal/asan from syscall+
internal/asan from syscall
internal/bisect from internal/godebug
internal/bytealg from bytes+
internal/byteorder from crypto/cipher+
internal/byteorder from crypto/aes+
internal/chacha8rand from math/rand/v2+
internal/concurrent from unique
internal/coverage/rtcov from runtime
internal/cpu from crypto/internal/fips140deps/cpu+
internal/cpu from crypto/aes+
internal/filepathlite from os+
internal/fmtsort from fmt+
internal/goarch from crypto/internal/fips140deps/cpu+
internal/goarch from crypto/aes+
internal/godebug from archive/tar+
internal/godebugs from internal/godebug+
internal/goexperiment from runtime+
internal/goexperiment from runtime
internal/goos from crypto/x509+
internal/itoa from internal/poll+
internal/msan from syscall+
internal/msan from syscall
internal/nettrace from net+
internal/oserror from io/fs+
internal/poll from net+
@@ -613,21 +588,18 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
internal/reflectlite from context+
internal/runtime/atomic from internal/runtime/exithook+
internal/runtime/exithook from runtime
internal/runtime/maps from reflect+
internal/runtime/math from internal/runtime/maps+
internal/runtime/sys from crypto/subtle+
L internal/runtime/syscall from runtime+
internal/saferio from debug/pe+
internal/singleflight from net
internal/stringslite from embed+
internal/sync from sync+
internal/syscall/execenv from os+
LD internal/syscall/unix from crypto/internal/sysrand+
W internal/syscall/windows from crypto/internal/sysrand+
LD internal/syscall/unix from crypto/rand+
W internal/syscall/windows from crypto/rand+
W internal/syscall/windows/registry from mime+
W internal/syscall/windows/sysdll from internal/syscall/windows+
internal/testlog from os
internal/unsafeheader from internal/reflectlite+
internal/weak from unique
io from archive/tar+
io/fs from archive/tar+
io/ioutil from github.com/aws/aws-sdk-go-v2/aws/protocol/query+
@@ -654,7 +626,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
net/netip from github.com/tailscale/wireguard-go/conn+
net/textproto from github.com/aws/aws-sdk-go-v2/aws/signer/v4+
net/url from crypto/x509+
os from crypto/internal/sysrand+
os from crypto/rand+
os/exec from github.com/aws/aws-sdk-go-v2/credentials/processcreds+
os/signal from tailscale.com/cmd/tailscaled
os/user from archive/tar+
@@ -665,6 +637,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
regexp/syntax from regexp
runtime from archive/tar+
runtime/debug from github.com/aws/aws-sdk-go-v2/internal/sync/singleflight+
runtime/internal/math from runtime
runtime/internal/sys from runtime
runtime/pprof from net/http/pprof+
runtime/trace from net/http/pprof
slices from tailscale.com/appc+
@@ -683,4 +657,3 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
unicode/utf8 from bufio+
unique from net/netip
unsafe from bytes+
weak from unique

View File

@@ -7,14 +7,10 @@
package flakytest
import (
"fmt"
"os"
"path"
"regexp"
"sync"
"strings"
"testing"
"tailscale.com/util/mak"
)
// FlakyTestLogMessage is a sentinel value that is printed to stderr when a
@@ -29,11 +25,6 @@ const FlakeAttemptEnv = "TS_TESTWRAPPER_ATTEMPT"
var issueRegexp = regexp.MustCompile(`\Ahttps://github\.com/tailscale/[a-zA-Z0-9_.-]+/issues/\d+\z`)
var (
rootFlakesMu sync.Mutex
rootFlakes map[string]bool
)
// Mark sets the current test as a flaky test, such that if it fails, it will
// be retried a few times on failure. issue must be a GitHub issue that tracks
// the status of the flaky test being marked, of the format:
@@ -47,27 +38,14 @@ func Mark(t testing.TB, issue string) {
// We're being run under cmd/testwrapper so send our sentinel message
// to stderr. (We avoid doing this when the env is absent to avoid
// spamming people running tests without the wrapper)
fmt.Fprintf(os.Stderr, "%s: %s\n", FlakyTestLogMessage, issue)
t.Cleanup(func() {
if t.Failed() {
// FIXME: this won't catch panics because t.Failed() won't yet
// be correctly set. https://github.com/golang/go/issues/49929
root, _, _ := strings.Cut(t.Name(), "/")
t.Logf("flakytest: retry: %s %s", root, strings.Join(os.Args, " "))
}
})
}
t.Logf("flakytest: issue tracking this flaky test: %s", issue)
// Record the root test name as flakey.
rootFlakesMu.Lock()
defer rootFlakesMu.Unlock()
mak.Set(&rootFlakes, t.Name(), true)
}
// Marked reports whether the current test or one of its parents was marked flaky.
func Marked(t testing.TB) bool {
n := t.Name()
for {
if rootFlakes[n] {
return true
}
n = path.Dir(n)
if n == "." || n == "/" {
break
}
}
return false
}

View File

@@ -42,48 +42,31 @@ func TestFlakeRun(t *testing.T) {
}
}
func TestMarked_Root(t *testing.T) {
Mark(t, "https://github.com/tailscale/tailscale/issues/0")
t.Run("child", func(t *testing.T) {
t.Run("grandchild", func(t *testing.T) {
if got, want := Marked(t), true; got != want {
t.Fatalf("Marked(t) = %t, want %t", got, want)
}
})
if got, want := Marked(t), true; got != want {
t.Fatalf("Marked(t) = %t, want %t", got, want)
}
})
if got, want := Marked(t), true; got != want {
t.Fatalf("Marked(t) = %t, want %t", got, want)
// TestFlakePanic is a test that panics when run in the testwrapper
// for the first time, but succeeds on the second run.
// It's used to test whether the testwrapper retries flaky tests.
func TestFlakeExit(t *testing.T) {
Mark(t, "https://github.com/tailscale/tailscale/issues/0") // random issue
e := os.Getenv(FlakeAttemptEnv)
if e == "" {
t.Skip("not running in testwrapper")
}
if e == "1" {
t.Log("First run in testwrapper, failing so exiting so test is retried. This is expected.")
os.Exit(0)
}
}
func TestMarked_Subtest(t *testing.T) {
t.Run("flaky", func(t *testing.T) {
Mark(t, "https://github.com/tailscale/tailscale/issues/0")
t.Run("child", func(t *testing.T) {
t.Run("grandchild", func(t *testing.T) {
if got, want := Marked(t), true; got != want {
t.Fatalf("Marked(t) = %t, want %t", got, want)
}
})
if got, want := Marked(t), true; got != want {
t.Fatalf("Marked(t) = %t, want %t", got, want)
}
})
if got, want := Marked(t), true; got != want {
t.Fatalf("Marked(t) = %t, want %t", got, want)
}
})
if got, want := Marked(t), false; got != want {
t.Fatalf("Marked(t) = %t, want %t", got, want)
// TestFlakePanic is a test that panics when run in the testwrapper
// for the first time, but succeeds on the second run.
// It's used to test whether the testwrapper retries flaky tests.
func TestFlakePanic(t *testing.T) {
Mark(t, "https://github.com/tailscale/tailscale/issues/0") // random issue
e := os.Getenv(FlakeAttemptEnv)
if e == "" {
t.Skip("not running in testwrapper")
}
if e == "1" {
panic("First run in testwrapper, failing so that test is retried. This is expected.")
}
}

View File

@@ -10,7 +10,6 @@ package main
import (
"bufio"
"bytes"
"cmp"
"context"
"encoding/json"
"errors"
@@ -60,12 +59,11 @@ type packageTests struct {
}
type goTestOutput struct {
Time time.Time
Action string
ImportPath string
Package string
Test string
Output string
Time time.Time
Action string
Package string
Test string
Output string
}
var debug = os.Getenv("TS_TESTWRAPPER_DEBUG") != ""
@@ -113,43 +111,42 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, goTestArgs, te
for s.Scan() {
var goOutput goTestOutput
if err := json.Unmarshal(s.Bytes(), &goOutput); err != nil {
return fmt.Errorf("failed to parse go test output %q: %w", s.Bytes(), err)
if errors.Is(err, io.EOF) || errors.Is(err, os.ErrClosed) {
break
}
// `go test -json` outputs invalid JSON when a build fails.
// In that case, discard the the output and start reading again.
// The build error will be printed to stderr.
// See: https://github.com/golang/go/issues/35169
if _, ok := err.(*json.SyntaxError); ok {
fmt.Println(s.Text())
continue
}
panic(err)
}
pkg := cmp.Or(
goOutput.Package,
"build:"+goOutput.ImportPath, // can be "./cmd" while Package is "tailscale.com/cmd" so use separate namespace
)
pkg := goOutput.Package
pkgTests := resultMap[pkg]
if pkgTests == nil {
pkgTests = map[string]*testAttempt{
"": {}, // Used for start time and build logs.
}
pkgTests = make(map[string]*testAttempt)
resultMap[pkg] = pkgTests
}
if goOutput.Test == "" {
switch goOutput.Action {
case "start":
pkgTests[""].start = goOutput.Time
case "build-output":
pkgTests[""].logs.WriteString(goOutput.Output)
case "build-fail", "fail", "pass", "skip":
pkgTests[""] = &testAttempt{start: goOutput.Time}
case "fail", "pass", "skip":
for _, test := range pkgTests {
if test.testName != "" && test.outcome == "" {
test.outcome = "fail"
ch <- test
}
}
outcome := goOutput.Action
if outcome == "build-fail" {
outcome = "FAIL"
}
pkgTests[""].logs.WriteString(goOutput.Output)
ch <- &testAttempt{
pkg: goOutput.Package,
outcome: outcome,
outcome: goOutput.Action,
start: pkgTests[""].start,
end: goOutput.Time,
logs: pkgTests[""].logs,
pkgFinished: true,
}
}
@@ -194,7 +191,7 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, goTestArgs, te
return nil
}
func main() {
func _main() {
goTestArgs, packages, testArgs, err := splitArgs(os.Args[1:])
if err != nil {
log.Fatal(err)
@@ -218,9 +215,6 @@ func main() {
}
toRun := []*nextRun{firstRun}
printPkgOutcome := func(pkg, outcome string, attempt int, runtime time.Duration) {
if pkg == "" {
return // We reach this path on a build error.
}
if outcome == "skip" {
fmt.Printf("?\t%s [skipped/no tests] \n", pkg)
return
@@ -276,7 +270,6 @@ func main() {
// when a package times out.
failed = true
}
os.Stdout.ReadFrom(&tr.logs)
printPkgOutcome(tr.pkg, tr.outcome, thisRun.attempt, tr.end.Sub(tr.start))
continue
}

View File

@@ -0,0 +1,239 @@
package main
import (
"bufio"
"bytes"
"errors"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"slices"
"strconv"
"strings"
"sync"
"tailscale.com/cmd/testwrapper/flakytest"
"tailscale.com/util/mak"
)
func main() {
log.SetFlags(log.Lshortfile)
log.SetPrefix("testwrapper: ")
// Build go args: test [-work] ...
var workdir string
var args = []string{"test"}
if !slices.Contains(args, "-work") && !slices.Contains(args, "--work") {
args = append(args, "-work")
defer func() {
if workdir != "" {
// Clean up the WORK directory as the user didn't want it.
if err := os.RemoveAll(workdir); err != nil {
log.Printf("error removing workdir: %s", err)
}
}
}()
}
args = append(args, os.Args[1:]...)
// Run go test.
attempt := 1
r, xerr := run("go", args, []string{attemptenv(attempt)}, os.Stdout, os.Stderr)
if nonexecerr(xerr) {
log.Fatal("go test: ", xerr)
}
// Check whether anything needs retried.
log.Printf("failures: builds=%d tests=%d retryable=%d", r.buildFailures, r.testFailures, r.testFailuresRetryable)
if r.buildFailures > 0 || r.testFailuresRetryable == 0 || r.testFailures > r.testFailuresRetryable {
exit(xerr)
}
// Retry tests we found.
const maxAttempts = 3
for cmd := range r.retryCmds {
pkg := strings.TrimSuffix(cmdPkg(cmd), ".test")
for {
attempt++
p := r.retryCmds[cmd]
log.Printf("attempt %d: %s %s", attempt, pkg, strings.Join(p.tests, " "))
// Retry the test by invoking the built pkg.test binary directly.
pr, xerr := run(
cmd,
append(p.args, "-test.run=^"+strings.Join(p.tests, "$|^")+"$"),
[]string{attemptenv(attempt)},
os.Stdout, os.Stdout, // go test copies all underlying pkg.test output to stdout
)
if nonexecerr(xerr) {
log.Fatalf("%s: %s", cmd, xerr)
}
if code, _ := exitcode(xerr); code == 0 {
break // all tests passed.
}
if attempt == maxAttempts {
log.Fatalf("failed %d times: %s %s", attempt, pkg, strings.Join(p.tests, " "))
}
// Try again with the new failure instructions. Hopefully with fewer
// failed tests...
r.retryCmds[cmd] = pr.retryCmds[cmd]
}
}
}
// attemptenv returns the environment variable value K=V used to signal
// [flakytest] that it's in a test environment.
func attemptenv(attempt int) string {
return flakytest.FlakeAttemptEnv + "=" + strconv.Itoa(attempt)
}
type testRun struct {
workDir string
buildFailures int
testFailures int
testFailuresRetryable int
retryCmds map[string]pkgRetry // cmd path => retry instructions
}
type pkgRetry struct {
cmd string
args []string
tests []string
}
// run executes prog with args and environ, writing output to stdout and stderr
// and returns the error from [exec.Cmd.Wait], along with information parsed
// from the output about how many builds or tests failed and how to retry them.
func run(prog string, args []string, environ []string, stdout, stderr io.Writer) (r testRun, _ error) {
cmd := exec.Command(prog, args...)
cmd.Env = append(os.Environ(), environ...)
cmdout, err := cmd.StdoutPipe()
if err != nil {
log.Fatalf("StdoutPipe: %s", err)
}
cmderr, err := cmd.StderrPipe()
if err != nil {
log.Fatalf("StderrPipe: %s", err)
}
if err := cmd.Start(); err != nil {
log.Fatalf("Start: %s", err)
}
var wg sync.WaitGroup
// Read WORK= from first line of stderr. We retain this so we can clean it
// when testwrapper ends.
wg.Add(1)
go func() {
defer wg.Done()
err := readthrulines(cmderr, stderr, func(line string) {
if r.workDir == "" {
if w, ok := strings.CutPrefix(line, "WORK="); ok {
r.workDir = w
}
}
})
if err != nil {
log.Fatalf("reading stderr: %s", err)
}
}()
wg.Add(1)
go func() {
defer wg.Done()
err := readthrulines(cmdout, stdout, func(line string) {
if strings.HasPrefix(line, "--- FAIL: Test") {
r.testFailures++
return
}
if strings.HasPrefix(line, "FAIL\t") && strings.HasSuffix(line, "[build failed]") {
r.buildFailures++
return
}
if _, args, ok := strings.Cut(line, "flakytest: retry:"); ok {
wargs := strings.Split(strings.TrimSpace(args), " ")
if len(wargs) < 2 {
log.Printf("failed to retry log line %q", line)
return
}
test, cmd, args := wargs[0], wargs[1], wargs[2:]
p := r.retryCmds[cmd]
p.cmd = cmd
p.args = args
p.tests = append(p.tests, test)
mak.Set(&r.retryCmds, cmd, p)
r.testFailuresRetryable++
return
}
})
if err != nil {
log.Fatalf("reading stdout: %s", err)
}
}()
wg.Wait()
xerr := cmd.Wait()
return r, xerr
}
// exit calls os.Exit with the exit code for err.
func exit(err error) {
code, _ := exitcode(err)
os.Exit(code)
}
// nonexecerr reports whether err is an error which prevented a program executing.
func nonexecerr(err error) bool {
if err == nil {
return false
}
xe := &exec.ExitError{}
return !errors.As(err, &xe) || xe.ExitCode() < 0
}
// exitcode returns a representative error code for err. If err has an
// ExitCode() int method, its exit code is returned.
func exitcode(err error) (code int, ok bool) {
if xe := (interface{ ExitCode() int })(nil); errors.As(err, &xe) {
return xe.ExitCode(), true
}
if err != nil {
return 1, false
}
return 0, false
}
// readthrulines copies r to w, calling f with each line of text.
func readthrulines(r io.Reader, w io.Writer, f func(line string)) error {
s := bufio.NewScanner(r)
for s.Scan() {
line := s.Text()
f(line)
io.WriteString(w, line)
io.WriteString(w, "\n")
}
return s.Err()
}
// cmdPkg will return the package of the binary that was built. From Go 1.24 on,
// this will return the full package path followed by the ".test" from the
// autogenerated main test pkg. For earlier Go versions return base(exe).
func cmdPkg(exe string) string {
v, _ := exec.Command("go", "version", "-m", exe).Output()
_, vp, ok := bytes.Cut(v, []byte("\n\tpath\t"))
if ok {
p, _, _ := bytes.Cut(vp, []byte("\n"))
p = bytes.TrimSpace(p)
if len(p) > 0 {
return string(p)
}
}
return filepath.Base(exe)
}

View File

@@ -11,7 +11,6 @@ import (
"os/exec"
"path/filepath"
"regexp"
"strings"
"sync"
"testing"
)
@@ -155,24 +154,24 @@ func TestBuildError(t *testing.T) {
t.Fatalf("writing package: %s", err)
}
wantErr := "builderror_test.go:3:1: expected declaration, found derp\nFAIL"
buildErr := []byte("builderror_test.go:3:1: expected declaration, found derp\nFAIL command-line-arguments [setup failed]")
// Confirm `go test` exits with code 1.
goOut, err := exec.Command("go", "test", testfile).CombinedOutput()
if code, ok := errExitCode(err); !ok || code != 1 {
t.Fatalf("go test %s: got exit code %d, want 1 (err: %v)", testfile, code, err)
t.Fatalf("go test %s: expected error with exit code 0 but got: %v", testfile, err)
}
if !strings.Contains(string(goOut), wantErr) {
t.Fatalf("go test %s: got output %q, want output containing %q", testfile, goOut, wantErr)
if !bytes.Contains(goOut, buildErr) {
t.Fatalf("go test %s: expected build error containing %q but got:\n%s", testfile, buildErr, goOut)
}
// Confirm `testwrapper` exits with code 1.
twOut, err := cmdTestwrapper(t, testfile).CombinedOutput()
if code, ok := errExitCode(err); !ok || code != 1 {
t.Fatalf("testwrapper %s: got exit code %d, want 1 (err: %v)", testfile, code, err)
t.Fatalf("testwrapper %s: expected error with exit code 0 but got: %v", testfile, err)
}
if !strings.Contains(string(twOut), wantErr) {
t.Fatalf("testwrapper %s: got output %q, want output containing %q", testfile, twOut, wantErr)
if !bytes.Contains(twOut, buildErr) {
t.Fatalf("testwrapper %s: expected build error containing %q but got:\n%s", testfile, buildErr, twOut)
}
if testing.Verbose() {

View File

@@ -176,10 +176,6 @@ func runEsbuild(buildOptions esbuild.BuildOptions) esbuild.BuildResult {
// wasm_exec.js runtime helper library from the Go toolchain.
func setupEsbuildWasmExecJS(build esbuild.PluginBuild) {
wasmExecSrcPath := filepath.Join(runtime.GOROOT(), "misc", "wasm", "wasm_exec.js")
if _, err := os.Stat(wasmExecSrcPath); os.IsNotExist(err) {
// Go 1.24+ location:
wasmExecSrcPath = filepath.Join(runtime.GOROOT(), "lib", "wasm", "wasm_exec.js")
}
build.OnResolve(esbuild.OnResolveOptions{
Filter: "./wasm_exec$",
}, func(args esbuild.OnResolveArgs) (esbuild.OnResolveResult, error) {

View File

@@ -137,7 +137,6 @@ type Server struct {
metaCert []byte // the encoded x509 cert to send after LetsEncrypt cert+intermediate
dupPolicy dupPolicy
debug bool
localClient local.Client
// Counters:
packetsSent, bytesSent expvar.Int
@@ -486,16 +485,6 @@ func (s *Server) SetVerifyClientURLFailOpen(v bool) {
s.verifyClientsURLFailOpen = v
}
// SetTailscaledSocketPath sets the unix socket path to use to talk to
// tailscaled if client verification is enabled.
//
// If unset or set to the empty string, the default path for the operating
// system is used.
func (s *Server) SetTailscaledSocketPath(path string) {
s.localClient.Socket = path
s.localClient.UseSocketOnly = path != ""
}
// SetTCPWriteTimeout sets the timeout for writing to connected clients.
// This timeout does not apply to mesh connections.
// Defaults to 2 seconds.
@@ -1331,6 +1320,8 @@ func (c *sclient) requestMeshUpdate() {
}
}
var localClient local.Client
// isMeshPeer reports whether the client is a trusted mesh peer
// node in the DERP region.
func (s *Server) isMeshPeer(info *clientInfo) bool {
@@ -1349,7 +1340,7 @@ func (s *Server) verifyClient(ctx context.Context, clientKey key.NodePublic, inf
// tailscaled-based verification:
if s.verifyClientsLocalTailscaled {
_, err := s.localClient.WhoIsNodeKey(ctx, clientKey)
_, err := localClient.WhoIsNodeKey(ctx, clientKey)
if err == tailscale.ErrPeerNotFound {
return fmt.Errorf("peer %v not authorized (not found in local tailscaled)", clientKey)
}
@@ -2249,7 +2240,7 @@ func (s *Server) ConsistencyCheck() error {
func (s *Server) checkVerifyClientsLocalTailscaled() error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
status, err := s.localClient.StatusWithoutPeers(ctx)
status, err := localClient.StatusWithoutPeers(ctx)
if err != nil {
return fmt.Errorf("localClient.Status: %w", err)
}

View File

@@ -109,14 +109,6 @@ If you enable this policy setting, users will not be allowed to disconnect Tails
If necessary, it can be used along with Unattended Mode to keep Tailscale connected regardless of whether a user is logged in. This can be used to facilitate remote access to a device or ensure connectivity to a Domain Controller before a user logs in.
If you disable or don't configure this policy setting, users will be allowed to disconnect Tailscale at their will.]]></string>
<string id="ReconnectAfter">Configure automatic reconnect delay</string>
<string id="ReconnectAfter_Help"><![CDATA[This policy setting controls when Tailscale will attempt to reconnect automatically after a user disconnects it. It helps users remain connected most of the time and retain access to corporate resources without preventing them from temporarily disconnecting Tailscale. To configure whether and when Tailscale can be disconnected, see the "Restrict users from disconnecting Tailscale (always-on mode)" policy setting.
If you enable this policy setting, you can specify how long Tailscale will wait before attempting to reconnect after a user disconnects. The value should be specified as a Go duration: for example, 30s, 5m, or 1h30m. If the value is left blank, or if the specified duration is zero, Tailscale will not attempt to reconnect automatically.
If you disable or don't configure this policy setting, Tailscale will only reconnect if a user chooses to or if required by a different policy setting.
Refer to https://pkg.go.dev/time#ParseDuration for information about the supported duration strings.]]></string>
<string id="ExitNodeAllowLANAccess">Allow Local Network Access when an Exit Node is in use</string>
<string id="ExitNodeAllowLANAccess_Help"><![CDATA[This policy can be used to require that the Allow Local Network Access setting is configured a certain way.
@@ -288,12 +280,6 @@ See https://tailscale.com/kb/1315/mdm-keys#set-your-organization-name for more d
<text>The options below allow configuring exceptions where disconnecting Tailscale is permitted.</text>
<dropdownList refId="AlwaysOn_OverrideWithReason" noSort="true" defaultItem="0">Disconnects with reason:</dropdownList>
</presentation>
<presentation id="ReconnectAfter">
<text>The delay must be a valid Go duration string, such as 30s, 5m, or 1h30m, all without spaces or any other symbols.</text>
<textBox refId="ReconnectAfterDelay">
<label>Reconnect after:</label>
</textBox>
</presentation>
<presentation id="ExitNodeID">
<textBox refId="ExitNodeIDPrompt">
<label>Exit Node:</label>

View File

@@ -156,13 +156,6 @@
</enum>
</elements>
</policy>
<policy name="ReconnectAfter" class="Machine" displayName="$(string.ReconnectAfter)" explainText="$(string.ReconnectAfter_Help)" presentation="$(presentation.ReconnectAfter)" key="Software\Policies\Tailscale">
<parentCategory ref="Settings_Category" />
<supportedOn ref="SINCE_V1_82" />
<elements>
<text id="ReconnectAfterDelay" valueName="ReconnectAfter" required="true" />
</elements>
</policy>
<policy name="ExitNodeAllowLANAccess" class="Machine" displayName="$(string.ExitNodeAllowLANAccess)" explainText="$(string.ExitNodeAllowLANAccess_Help)" key="Software\Policies\Tailscale" valueName="ExitNodeAllowLANAccess">
<parentCategory ref="Settings_Category" />
<supportedOn ref="PARTIAL_FULL_SINCE_V1_56" />

8
go.mod
View File

@@ -1,6 +1,6 @@
module tailscale.com
go 1.24.0
go 1.23.6
require (
filippo.io/mkcert v1.4.4
@@ -32,7 +32,7 @@ require (
github.com/frankban/quicktest v1.14.6
github.com/fxamacker/cbor/v2 v2.7.0
github.com/gaissmai/bart v0.18.0
github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874
github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288
github.com/go-logr/zapr v1.3.0
github.com/go-ole/go-ole v1.3.0
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466
@@ -74,7 +74,7 @@ require (
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e
github.com/tailscale/depaware v0.0.0-20250112153213-b748de04d81b
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41
github.com/tailscale/golang-x-crypto v0.0.0-20250218230618-9a281fd8faca
github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a
github.com/tailscale/mkctr v0.0.0-20250110151924-54977352e4a6
@@ -93,7 +93,7 @@ require (
go.uber.org/zap v1.27.0
go4.org/mem v0.0.0-20240501181205-ae6ca9944745
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.35.0
golang.org/x/crypto v0.33.0
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac
golang.org/x/mod v0.23.0
golang.org/x/net v0.35.0

12
go.sum
View File

@@ -327,8 +327,8 @@ github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0q
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 h1:F8d1AJ6M9UQCavhwmO6ZsrYLfG8zVFWfEfMS2MXPkSY=
github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288 h1:KbX3Z3CgiYlbaavUq3Cj9/MjpO+88S7/AGXzynVDv84=
github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
@@ -900,8 +900,8 @@ github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41 h1:/V2rCMMWcsjYaYO2MeovLw+ClP63OtXgCF2Y1eb8+Ns=
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41/go.mod h1:/roCdA6gg6lQyw/Oz6gIIGu3ggJKYhF+WC/AQReE5XQ=
github.com/tailscale/golang-x-crypto v0.0.0-20250218230618-9a281fd8faca h1:ecjHwH73Yvqf/oIdQ2vxAX+zc6caQsYdPzsxNW1J3G8=
github.com/tailscale/golang-x-crypto v0.0.0-20250218230618-9a281fd8faca/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ=
github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 h1:rXZGgEa+k2vJM8xT0PoSKfVXwFGPQ3z3CJfmnHJkZZw=
github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw=
@@ -1041,8 +1041,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=

View File

@@ -1 +1 @@
tailscale.go1.24
tailscale.go1.23

View File

@@ -1 +1 @@
2b494987ff3c1a6a26e10570c490394ff0a77aa4
65c3f5f3fc9d96f56a37a79cad4ebbd7ff985801

View File

@@ -8,16 +8,9 @@
package tailscale
import (
"errors"
"io"
"net/http"
tsclient "tailscale.com/client/tailscale"
)
// maxSize is the maximum read size (10MB) of responses from the server.
const maxReadSize = 10 << 20
func init() {
tsclient.I_Acknowledge_This_API_Is_Unstable = true
}
@@ -57,27 +50,3 @@ func NewClient(tailnet string, auth AuthMethod) *Client {
type Client struct {
*tsclient.Client
}
// HandleErrorResponse is an alias to tailscale.com/client/tailscale.
func HandleErrorResponse(b []byte, resp *http.Response) error {
return tsclient.HandleErrorResponse(b, resp)
}
// SendRequest add the authentication key to the request and sends it. It
// receives the response and reads up to 10MB of it.
func SendRequest(c *Client, req *http.Request) ([]byte, *http.Response, error) {
resp, err := c.Do(req)
if err != nil {
return nil, resp, err
}
defer resp.Body.Close()
// Read response. Limit the response to 10MB.
// This limit is carried over from client/tailscale/tailscale.go.
body := io.LimitReader(resp.Body, maxReadSize+1)
b, err := io.ReadAll(body)
if len(b) > maxReadSize {
err = errors.New("API response too large")
}
return b, resp, err
}

View File

@@ -1,103 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package tailscale
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"tailscale.com/tailcfg"
"tailscale.com/util/httpm"
)
// VIPService is a Tailscale VIPService with Tailscale API JSON representation.
type VIPService struct {
// Name is a VIPService name in form svc:<leftmost-label-of-service-DNS-name>.
Name tailcfg.ServiceName `json:"name,omitempty"`
// Addrs are the IP addresses of the VIP Service. There are two addresses:
// the first is IPv4 and the second is IPv6.
// When creating a new VIP Service, the IP addresses are optional: if no
// addresses are specified then they will be selected. If an IPv4 address is
// specified at index 0, then that address will attempt to be used. An IPv6
// address can not be specified upon creation.
Addrs []string `json:"addrs,omitempty"`
// Comment is an optional text string for display in the admin panel.
Comment string `json:"comment,omitempty"`
// Ports are the ports of a VIPService that will be configured via Tailscale serve config.
// If set, any node wishing to advertise this VIPService must have this port configured via Tailscale serve.
Ports []string `json:"ports,omitempty"`
// Tags are optional ACL tags that will be applied to the VIPService.
Tags []string `json:"tags,omitempty"`
}
// GetVIPService retrieves a VIPService by its name. It returns 404 if the VIPService is not found.
func (client *Client) GetVIPService(ctx context.Context, name tailcfg.ServiceName) (*VIPService, error) {
path := client.BuildTailnetURL("vip-services", name.String())
req, err := http.NewRequestWithContext(ctx, httpm.GET, path, nil)
if err != nil {
return nil, fmt.Errorf("error creating new HTTP request: %w", err)
}
b, resp, err := SendRequest(client, req)
if err != nil {
return nil, fmt.Errorf("error making Tailsale API request: %w", err)
}
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, HandleErrorResponse(b, resp)
}
svc := &VIPService{}
if err := json.Unmarshal(b, svc); err != nil {
return nil, err
}
return svc, nil
}
// CreateOrUpdateVIPService creates or updates a VIPService by its name. Caller must ensure that, if the
// VIPService already exists, the VIPService is fetched first to ensure that any auto-allocated IP addresses are not
// lost during the update. If the VIPService was created without any IP addresses explicitly set (so that they were
// auto-allocated by Tailscale) any subsequent request to this function that does not set any IP addresses will error.
func (client *Client) CreateOrUpdateVIPService(ctx context.Context, svc *VIPService) error {
data, err := json.Marshal(svc)
if err != nil {
return err
}
path := client.BuildTailnetURL("vip-services", svc.Name.String())
req, err := http.NewRequestWithContext(ctx, httpm.PUT, path, bytes.NewBuffer(data))
if err != nil {
return fmt.Errorf("error creating new HTTP request: %w", err)
}
b, resp, err := SendRequest(client, req)
if err != nil {
return fmt.Errorf("error making Tailscale API request: %w", err)
}
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return HandleErrorResponse(b, resp)
}
return nil
}
// DeleteVIPService deletes a VIPService by its name. It returns an error if the VIPService
// does not exist or if the deletion fails.
func (client *Client) DeleteVIPService(ctx context.Context, name tailcfg.ServiceName) error {
path := client.BuildTailnetURL("vip-services", name.String())
req, err := http.NewRequestWithContext(ctx, httpm.DELETE, path, nil)
if err != nil {
return fmt.Errorf("error creating new HTTP request: %w", err)
}
b, resp, err := SendRequest(client, req)
if err != nil {
return fmt.Errorf("error making Tailscale API request: %w", err)
}
// If status code was not successful, return the error.
if resp.StatusCode != http.StatusOK {
return HandleErrorResponse(b, resp)
}
return nil
}

View File

@@ -442,10 +442,6 @@ type LocalBackend struct {
// See tailscale/corp#26146.
overrideAlwaysOn bool
// reconnectTimer is used to schedule a reconnect by setting [ipn.Prefs.WantRunning]
// to true after a delay, or nil if no reconnect is scheduled.
reconnectTimer tstime.TimerController
// shutdownCbs are the callbacks to be called when the backend is shutting down.
// Each callback is called exactly once in unspecified order and without b.mu held.
// Returned errors are logged but otherwise ignored and do not affect the shutdown process.
@@ -1074,8 +1070,6 @@ func (b *LocalBackend) Shutdown() {
b.captiveCancel()
}
b.stopReconnectTimerLocked()
if b.loginFlags&controlclient.LoginEphemeral != 0 {
b.mu.Unlock()
ctx, cancel := context.WithTimeout(b.ctx, 5*time.Second)
@@ -2347,20 +2341,12 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
}); err != nil {
b.logf("failed to save UpdatePrefs state: %v", err)
}
b.setAtomicValuesFromPrefsLocked(pv)
} else {
b.setAtomicValuesFromPrefsLocked(b.pm.CurrentPrefs())
}
// Reset the always-on override whenever Start is called.
b.resetAlwaysOnOverrideLocked()
// And also apply syspolicy settings to the current profile.
// This is important in two cases: when opts.UpdatePrefs is not nil,
// and when Always Mode is enabled and we need to set WantRunning to true.
if newp := b.pm.CurrentPrefs().AsStruct(); applySysPolicy(newp, b.lastSuggestedExitNode, b.overrideAlwaysOn) {
setExitNodeID(newp, b.netMap)
b.pm.setPrefsNoPermCheck(newp.View())
}
prefs := b.pm.CurrentPrefs()
b.setAtomicValuesFromPrefsLocked(prefs)
wantRunning := prefs.WantRunning()
if wantRunning {
if err := b.initMachineKeyLocked(); err != nil {
@@ -4303,75 +4289,15 @@ func (b *LocalBackend) EditPrefsAs(mp *ipn.MaskedPrefs, actor ipnauth.Actor) (ip
// mode on them until the policy changes, they switch to a different profile, etc.
b.overrideAlwaysOn = true
if reconnectAfter, _ := syspolicy.GetDuration(syspolicy.ReconnectAfter, 0); reconnectAfter > 0 {
b.startReconnectTimerLocked(reconnectAfter)
}
// TODO(nickkhyl): check the ReconnectAfter policy here. If configured,
// start a timer to automatically reconnect after the specified duration.
}
return b.editPrefsLockedOnEntry(mp, unlock)
}
// startReconnectTimerLocked sets a timer to automatically set WantRunning to true
// after the specified duration.
func (b *LocalBackend) startReconnectTimerLocked(d time.Duration) {
if b.reconnectTimer != nil {
// Stop may return false if the timer has already fired,
// and the function has been called in its own goroutine,
// but lost the race to acquire b.mu. In this case, it'll
// end up as a no-op due to a reconnectTimer mismatch
// once it manages to acquire the lock. This is fine, and we
// don't need to check the return value.
b.reconnectTimer.Stop()
}
profileID := b.pm.CurrentProfile().ID()
var reconnectTimer tstime.TimerController
reconnectTimer = b.clock.AfterFunc(d, func() {
unlock := b.lockAndGetUnlock()
defer unlock()
if b.reconnectTimer != reconnectTimer {
// We're either not the most recent timer, or we lost the race when
// the timer was stopped. No need to reconnect.
return
}
b.reconnectTimer = nil
cp := b.pm.CurrentProfile()
if cp.ID() != profileID {
// The timer fired before the profile changed but we lost the race
// and acquired the lock shortly after.
// No need to reconnect.
return
}
mp := &ipn.MaskedPrefs{WantRunningSet: true, Prefs: ipn.Prefs{WantRunning: true}}
if _, err := b.editPrefsLockedOnEntry(mp, unlock); err != nil {
b.logf("failed to automatically reconnect as %q after %v: %v", cp.Name(), d, err)
} else {
b.logf("automatically reconnected as %q after %v", cp.Name(), d)
}
})
b.reconnectTimer = reconnectTimer
b.logf("reconnect for %q has been scheduled and will be performed in %v", b.pm.CurrentProfile().Name(), d)
}
func (b *LocalBackend) resetAlwaysOnOverrideLocked() {
b.overrideAlwaysOn = false
b.stopReconnectTimerLocked()
}
func (b *LocalBackend) stopReconnectTimerLocked() {
if b.reconnectTimer != nil {
// Stop may return false if the timer has already fired,
// and the function has been called in its own goroutine,
// but lost the race to acquire b.mu.
// In this case, it'll end up as a no-op due to a reconnectTimer
// mismatch (see [LocalBackend.startReconnectTimerLocked])
// once it manages to acquire the lock. This is fine, and we
// don't need to check the return value.
b.reconnectTimer.Stop()
b.reconnectTimer = nil
}
}
// Warning: b.mu must be held on entry, but it unlocks it on the way out.
@@ -4465,7 +4391,7 @@ func (b *LocalBackend) setPrefsLockedOnEntry(newp *ipn.Prefs, unlock unlockOnce)
if oldp.Valid() {
newp.Persist = oldp.Persist().AsStruct() // caller isn't allowed to override this
}
// applySysPolicy returns whether it updated newp,
// applySysPolicyToPrefsLocked returns whether it updated newp,
// but everything in this function treats b.prefs as completely new
// anyway, so its return value can be ignored here.
applySysPolicy(newp, b.lastSuggestedExitNode, b.overrideAlwaysOn)

View File

@@ -1,72 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package maths contains additional mathematical functions or structures not
// found in the standard library.
package maths
import (
"math"
"time"
)
// EWMA is an exponentially weighted moving average supporting updates at
// irregular intervals with at most nanosecond resolution.
// The zero value will compute a half-life of 1 second.
// It is not safe for concurrent use.
// TODO(raggi): de-duplicate with tstime/rate.Value, which has a more complex
// and synchronized interface and does not provide direct access to the stable
// value.
type EWMA struct {
value float64 // current value of the average
lastTime int64 // time of last update in unix nanos
halfLife float64 // half-life in seconds
}
// NewEWMA creates a new EWMA with the specified half-life. If halfLifeSeconds
// is 0, it defaults to 1.
func NewEWMA(halfLifeSeconds float64) *EWMA {
return &EWMA{
halfLife: halfLifeSeconds,
}
}
// Update adds a new sample to the average. If t is zero or precedes the last
// update, the update is ignored.
func (e *EWMA) Update(value float64, t time.Time) {
if t.IsZero() {
return
}
hl := e.halfLife
if hl == 0 {
hl = 1
}
tn := t.UnixNano()
if e.lastTime == 0 {
e.value = value
e.lastTime = tn
return
}
dt := (time.Duration(tn-e.lastTime) * time.Nanosecond).Seconds()
if dt < 0 {
// drop out of order updates
return
}
// decay = 2^(-dt/halfLife)
decay := math.Exp2(-dt / hl)
e.value = e.value*decay + value*(1-decay)
e.lastTime = tn
}
// Get returns the current value of the average
func (e *EWMA) Get() float64 {
return e.value
}
// Reset clears the EWMA to its initial state
func (e *EWMA) Reset() {
e.value = 0
e.lastTime = 0
}

View File

@@ -1,178 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package maths
import (
"slices"
"testing"
"time"
)
// some real world latency samples.
var (
latencyHistory1 = []int{
14, 12, 15, 6, 19, 12, 13, 13, 13, 16, 17, 11, 17, 11, 14, 15, 14, 15,
16, 16, 17, 14, 12, 16, 18, 14, 14, 11, 15, 15, 25, 11, 15, 14, 12, 15,
13, 12, 13, 15, 11, 13, 15, 14, 14, 15, 12, 15, 18, 12, 15, 22, 12, 13,
10, 14, 16, 15, 16, 11, 14, 17, 18, 20, 16, 11, 16, 14, 5, 15, 17, 12,
15, 11, 15, 20, 12, 17, 12, 17, 15, 12, 12, 11, 14, 15, 11, 20, 14, 13,
11, 12, 13, 13, 11, 13, 11, 15, 13, 13, 14, 12, 11, 12, 12, 14, 11, 13,
12, 12, 12, 19, 14, 13, 13, 14, 11, 12, 10, 11, 15, 12, 14, 11, 11, 14,
14, 12, 12, 11, 14, 12, 11, 12, 14, 11, 12, 15, 12, 14, 12, 12, 21, 16,
21, 12, 16, 9, 11, 16, 14, 13, 14, 12, 13, 16,
}
latencyHistory2 = []int{
18, 20, 21, 21, 20, 23, 18, 18, 20, 21, 20, 19, 22, 18, 20, 20, 19, 21,
21, 22, 22, 19, 18, 22, 22, 19, 20, 17, 16, 11, 25, 16, 18, 21, 17, 22,
19, 18, 22, 21, 20, 18, 22, 17, 17, 20, 19, 10, 19, 16, 19, 25, 17, 18,
15, 20, 21, 20, 23, 22, 22, 22, 19, 22, 22, 17, 22, 20, 20, 19, 21, 22,
20, 19, 17, 22, 16, 16, 20, 22, 17, 19, 21, 16, 20, 22, 19, 21, 20, 19,
13, 14, 23, 19, 16, 10, 19, 15, 15, 17, 16, 18, 14, 16, 18, 22, 20, 18,
18, 21, 15, 19, 18, 19, 18, 20, 17, 19, 21, 19, 20, 19, 20, 20, 17, 14,
17, 17, 18, 21, 20, 18, 18, 17, 16, 17, 17, 20, 22, 19, 20, 21, 21, 20,
21, 24, 20, 18, 12, 17, 18, 17, 19, 19, 19,
}
)
func TestEWMALatencyHistory(t *testing.T) {
type result struct {
t time.Time
v float64
s int
}
for _, latencyHistory := range [][]int{latencyHistory1, latencyHistory2} {
startTime := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC)
halfLife := 30.0
ewma := NewEWMA(halfLife)
var results []result
sum := 0.0
for i, latency := range latencyHistory {
t := startTime.Add(time.Duration(i) * time.Second)
ewma.Update(float64(latency), t)
sum += float64(latency)
results = append(results, result{t, ewma.Get(), latency})
}
mean := sum / float64(len(latencyHistory))
min := float64(slices.Min(latencyHistory))
max := float64(slices.Max(latencyHistory))
t.Logf("EWMA Latency History (half-life: %.1f seconds):", halfLife)
t.Logf("Mean latency: %.2f ms", mean)
t.Logf("Range: [%.1f, %.1f]", min, max)
t.Log("Samples: ")
sparkline := []rune("▁▂▃▄▅▆▇█")
var sampleLine []rune
for _, r := range results {
idx := int(((float64(r.s) - min) / (max - min)) * float64(len(sparkline)-1))
if idx >= len(sparkline) {
idx = len(sparkline) - 1
}
sampleLine = append(sampleLine, sparkline[idx])
}
t.Log(string(sampleLine))
t.Log("EWMA: ")
var ewmaLine []rune
for _, r := range results {
idx := int(((r.v - min) / (max - min)) * float64(len(sparkline)-1))
if idx >= len(sparkline) {
idx = len(sparkline) - 1
}
ewmaLine = append(ewmaLine, sparkline[idx])
}
t.Log(string(ewmaLine))
t.Log("")
t.Logf("Time | Sample | Value | Value - Sample")
t.Logf("")
for _, result := range results {
t.Logf("%10s | % 6d | % 5.2f | % 5.2f", result.t.Format("15:04:05"), result.s, result.v, result.v-float64(result.s))
}
// check that all results are greater than the min, and less than the max of the input,
// and they're all close to the mean.
for _, result := range results {
if result.v < float64(min) || result.v > float64(max) {
t.Errorf("result %f out of range [%f, %f]", result.v, min, max)
}
if result.v < mean*0.9 || result.v > mean*1.1 {
t.Errorf("result %f not close to mean %f", result.v, mean)
}
}
}
}
func TestHalfLife(t *testing.T) {
start := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC)
ewma := NewEWMA(30.0)
ewma.Update(10, start)
ewma.Update(0, start.Add(30*time.Second))
if ewma.Get() != 5 {
t.Errorf("expected 5, got %f", ewma.Get())
}
ewma.Update(10, start.Add(60*time.Second))
if ewma.Get() != 7.5 {
t.Errorf("expected 7.5, got %f", ewma.Get())
}
ewma.Update(10, start.Add(90*time.Second))
if ewma.Get() != 8.75 {
t.Errorf("expected 8.75, got %f", ewma.Get())
}
}
func TestZeroValue(t *testing.T) {
start := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC)
var ewma EWMA
ewma.Update(10, start)
ewma.Update(0, start.Add(time.Second))
if ewma.Get() != 5 {
t.Errorf("expected 5, got %f", ewma.Get())
}
ewma.Update(10, start.Add(2*time.Second))
if ewma.Get() != 7.5 {
t.Errorf("expected 7.5, got %f", ewma.Get())
}
ewma.Update(10, start.Add(3*time.Second))
if ewma.Get() != 8.75 {
t.Errorf("expected 8.75, got %f", ewma.Get())
}
}
func TestReset(t *testing.T) {
start := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC)
ewma := NewEWMA(30.0)
ewma.Update(10, start)
ewma.Update(0, start.Add(30*time.Second))
if ewma.Get() != 5 {
t.Errorf("expected 5, got %f", ewma.Get())
}
ewma.Reset()
if ewma.Get() != 0 {
t.Errorf("expected 0, got %f", ewma.Get())
}
ewma.Update(10, start.Add(90*time.Second))
if ewma.Get() != 10 {
t.Errorf("expected 10, got %f", ewma.Get())
}
}

View File

@@ -4,22 +4,17 @@
package ktimeout
import (
"context"
"net"
"testing"
"time"
"golang.org/x/net/nettest"
"golang.org/x/sys/unix"
"tailscale.com/util/must"
)
func TestSetUserTimeout(t *testing.T) {
lc := net.ListenConfig{}
// As of 2025-02-19, MPTCP does not support TCP_USER_TIMEOUT socket option
// set in ktimeout.UserTimeout above.
lc.SetMultipathTCP(false)
l := must.Get(lc.Listen(context.Background(), "tcp", "localhost:0"))
l := must.Get(nettest.NewLocalListener("tcp"))
defer l.Close()
var err error

View File

@@ -7,14 +7,6 @@
set -eu
# Ensure that this script runs with the default umask for Linux. In practice,
# this means that files created by this script (such as keyring files) will be
# created with 644 permissions. This ensures that keyrings and other files
# created by this script are readable by installers on systems where the
# umask is set to a more restrictive value.
# See https://github.com/tailscale/tailscale/issues/15133
umask 022
# All the code is wrapped in a main function that gets called at the
# bottom of the file, so that a truncated partial download doesn't end
# up executing half a script.
@@ -194,12 +186,6 @@ main() {
VERSION="$DEBIAN_CODENAME"
fi
;;
sparky)
OS="debian"
PACKAGETYPE="apt"
VERSION="$DEBIAN_CODENAME"
APT_KEY_TYPE="keyring"
;;
centos)
OS="$ID"
VERSION="$VERSION_ID"

View File

@@ -557,11 +557,7 @@ func (c *Client) Accept(ctx context.Context, chal *Challenge) (*Challenge, error
return nil, err
}
payload := json.RawMessage("{}")
if len(chal.Payload) != 0 {
payload = chal.Payload
}
res, err := c.post(ctx, nil, chal.URI, payload, wantStatus(
res, err := c.post(ctx, nil, chal.URI, json.RawMessage("{}"), wantStatus(
http.StatusOK, // according to the spec
http.StatusAccepted, // Let's Encrypt: see https://goo.gl/WsJ7VT (acme-divergences.md)
))

View File

@@ -875,7 +875,7 @@ func TestTLSALPN01ChallengeCert(t *testing.T) {
}
func TestTLSChallengeCertOpt(t *testing.T) {
key, err := rsa.GenerateKey(rand.Reader, 1024)
key, err := rsa.GenerateKey(rand.Reader, 512)
if err != nil {
t.Fatal(err)
}

View File

@@ -15,7 +15,6 @@ import (
"io"
"math/big"
"net/http"
"runtime/debug"
"strconv"
"strings"
"time"
@@ -272,27 +271,9 @@ func (c *Client) httpClient() *http.Client {
}
// packageVersion is the version of the module that contains this package, for
// sending as part of the User-Agent header.
// sending as part of the User-Agent header. It's set in version_go112.go.
var packageVersion string
func init() {
// Set packageVersion if the binary was built in modules mode and x/crypto
// was not replaced with a different module.
info, ok := debug.ReadBuildInfo()
if !ok {
return
}
for _, m := range info.Deps {
if m.Path != "golang.org/x/crypto" {
continue
}
if m.Replace == nil {
packageVersion = m.Version
}
break
}
}
// userAgent returns the User-Agent header value. It includes the package name,
// the module version (if available), and the c.UserAgent value (if set).
func (c *Client) userAgent() string {

View File

@@ -7,7 +7,6 @@ package acme
import (
"crypto"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"net/http"
@@ -293,7 +292,7 @@ type Directory struct {
// Renewal Information (ARI) Extension.
RenewalInfoURL string
// Terms is a URI identifying the current terms of service.
// Term is a URI identifying the current terms of service.
Terms string
// Website is an HTTP or HTTPS URL locating a website
@@ -532,16 +531,6 @@ type Challenge struct {
// when this challenge was used.
// The type of a non-nil value is *Error.
Error error
// Payload is the JSON-formatted payload that the client sends
// to the server to indicate it is ready to respond to the challenge.
// When unset, it defaults to an empty JSON object: {}.
// For most challenges, the client must not set Payload,
// see https://tools.ietf.org/html/rfc8555#section-7.5.1.
// Payload is used only for newer challenges (such as "device-attest-01")
// where the client must send additional data for the server to validate
// the challenge.
Payload json.RawMessage
}
// wireChallenge is ACME JSON challenge representation.

View File

@@ -27,7 +27,6 @@ type DepChecker struct {
BadDeps map[string]string // package => why
WantDeps set.Set[string] // packages expected
Tags string // comma-separated
ExtraEnv []string // extra environment for "go list" (e.g. CGO_ENABLED=1)
}
func (c DepChecker) Check(t *testing.T) {
@@ -44,7 +43,6 @@ func (c DepChecker) Check(t *testing.T) {
if c.GOARCH != "" {
extraEnv = append(extraEnv, "GOARCH="+c.GOARCH)
}
extraEnv = append(extraEnv, c.ExtraEnv...)
cmd.Env = append(os.Environ(), extraEnv...)
out, err := cmd.Output()
if err != nil {

View File

@@ -100,31 +100,31 @@ func (o Value[T]) Equal(v Value[T]) bool {
return false
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (o Value[T]) MarshalJSONTo(enc *jsontext.Encoder) error {
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
func (o Value[T]) MarshalJSONV2(enc *jsontext.Encoder, opts jsonv2.Options) error {
if !o.set {
return enc.WriteToken(jsontext.Null)
}
return jsonv2.MarshalEncode(enc, &o.value)
return jsonv2.MarshalEncode(enc, &o.value, opts)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (o *Value[T]) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
func (o *Value[T]) UnmarshalJSONV2(dec *jsontext.Decoder, opts jsonv2.Options) error {
if dec.PeekKind() == 'n' {
*o = Value[T]{}
_, err := dec.ReadToken() // read null
return err
}
o.set = true
return jsonv2.UnmarshalDecode(dec, &o.value)
return jsonv2.UnmarshalDecode(dec, &o.value, opts)
}
// MarshalJSON implements [json.Marshaler].
func (o Value[T]) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(o) // uses MarshalJSONTo
return jsonv2.Marshal(o) // uses MarshalJSONV2
}
// UnmarshalJSON implements [json.Unmarshaler].
func (o *Value[T]) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, o) // uses UnmarshalJSONFrom
return jsonv2.Unmarshal(b, o) // uses UnmarshalJSONV2
}

View File

@@ -152,15 +152,15 @@ func (iv ItemView[T, V]) Equal(iv2 ItemView[T, V]) bool {
return iv.ж.Equal(*iv2.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (iv ItemView[T, V]) MarshalJSONTo(out *jsontext.Encoder) error {
return iv.ж.MarshalJSONTo(out)
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
func (iv ItemView[T, V]) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
return iv.ж.MarshalJSONV2(out, opts)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (iv *ItemView[T, V]) UnmarshalJSONFrom(in *jsontext.Decoder) error {
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
func (iv *ItemView[T, V]) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
var x Item[T]
if err := x.UnmarshalJSONFrom(in); err != nil {
if err := x.UnmarshalJSONV2(in, opts); err != nil {
return err
}
iv.ж = &x
@@ -169,10 +169,10 @@ func (iv *ItemView[T, V]) UnmarshalJSONFrom(in *jsontext.Decoder) error {
// MarshalJSON implements [json.Marshaler].
func (iv ItemView[T, V]) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(iv) // uses MarshalJSONTo
return jsonv2.Marshal(iv) // uses MarshalJSONV2
}
// UnmarshalJSON implements [json.Unmarshaler].
func (iv *ItemView[T, V]) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, iv) // uses UnmarshalJSONFrom
return jsonv2.Unmarshal(b, iv) // uses UnmarshalJSONV2
}

View File

@@ -157,15 +157,15 @@ func (lv ListView[T]) Equal(lv2 ListView[T]) bool {
return lv.ж.Equal(*lv2.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (lv ListView[T]) MarshalJSONTo(out *jsontext.Encoder) error {
return lv.ж.MarshalJSONTo(out)
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
func (lv ListView[T]) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
return lv.ж.MarshalJSONV2(out, opts)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (lv *ListView[T]) UnmarshalJSONFrom(in *jsontext.Decoder) error {
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
func (lv *ListView[T]) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
var x List[T]
if err := x.UnmarshalJSONFrom(in); err != nil {
if err := x.UnmarshalJSONV2(in, opts); err != nil {
return err
}
lv.ж = &x
@@ -174,10 +174,10 @@ func (lv *ListView[T]) UnmarshalJSONFrom(in *jsontext.Decoder) error {
// MarshalJSON implements [json.Marshaler].
func (lv ListView[T]) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(lv) // uses MarshalJSONTo
return jsonv2.Marshal(lv) // uses MarshalJSONV2
}
// UnmarshalJSON implements [json.Unmarshaler].
func (lv *ListView[T]) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, lv) // uses UnmarshalJSONFrom
return jsonv2.Unmarshal(b, lv) // uses UnmarshalJSONV2
}

View File

@@ -133,15 +133,15 @@ func (mv MapView[K, V]) Equal(mv2 MapView[K, V]) bool {
return mv.ж.Equal(*mv2.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (mv MapView[K, V]) MarshalJSONTo(out *jsontext.Encoder) error {
return mv.ж.MarshalJSONTo(out)
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
func (mv MapView[K, V]) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
return mv.ж.MarshalJSONV2(out, opts)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (mv *MapView[K, V]) UnmarshalJSONFrom(in *jsontext.Decoder) error {
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
func (mv *MapView[K, V]) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
var x Map[K, V]
if err := x.UnmarshalJSONFrom(in); err != nil {
if err := x.UnmarshalJSONV2(in, opts); err != nil {
return err
}
mv.ж = &x
@@ -150,10 +150,10 @@ func (mv *MapView[K, V]) UnmarshalJSONFrom(in *jsontext.Decoder) error {
// MarshalJSON implements [json.Marshaler].
func (mv MapView[K, V]) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(mv) // uses MarshalJSONTo
return jsonv2.Marshal(mv) // uses MarshalJSONV2
}
// UnmarshalJSON implements [json.Unmarshaler].
func (mv *MapView[K, V]) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, mv) // uses UnmarshalJSONFrom
return jsonv2.Unmarshal(b, mv) // uses UnmarshalJSONV2
}

View File

@@ -158,22 +158,22 @@ func (p *preference[T]) SetReadOnly(readonly bool) {
p.s.Metadata.ReadOnly = readonly
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (p preference[T]) MarshalJSONTo(out *jsontext.Encoder) error {
return jsonv2.MarshalEncode(out, &p.s)
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
func (p preference[T]) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
return jsonv2.MarshalEncode(out, &p.s, opts)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (p *preference[T]) UnmarshalJSONFrom(in *jsontext.Decoder) error {
return jsonv2.UnmarshalDecode(in, &p.s)
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
func (p *preference[T]) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
return jsonv2.UnmarshalDecode(in, &p.s, opts)
}
// MarshalJSON implements [json.Marshaler].
func (p preference[T]) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(p) // uses MarshalJSONTo
return jsonv2.Marshal(p) // uses MarshalJSONV2
}
// UnmarshalJSON implements [json.Unmarshaler].
func (p *preference[T]) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, p) // uses UnmarshalJSONFrom
return jsonv2.Unmarshal(b, p) // uses UnmarshalJSONV2
}

View File

@@ -48,10 +48,10 @@ import (
// the `omitzero` JSON tag option. This option is not supported by the
// [encoding/json] package as of 2024-08-21; see golang/go#45669.
// It is recommended that a prefs type implements both
// [jsonv2.MarshalerTo]/[jsonv2.UnmarshalerFrom] and [json.Marshaler]/[json.Unmarshaler]
// [jsonv2.MarshalerV2]/[jsonv2.UnmarshalerV2] and [json.Marshaler]/[json.Unmarshaler]
// to ensure consistent and more performant marshaling, regardless of the JSON package
// used at the call sites; the standard marshalers can be implemented via [jsonv2].
// See [Prefs.MarshalJSONTo], [Prefs.UnmarshalJSONFrom], [Prefs.MarshalJSON],
// See [Prefs.MarshalJSONV2], [Prefs.UnmarshalJSONV2], [Prefs.MarshalJSON],
// and [Prefs.UnmarshalJSON] for an example implementation.
type Prefs struct {
ControlURL prefs.Item[string] `json:",omitzero"`
@@ -128,34 +128,34 @@ type AppConnectorPrefs struct {
Advertise prefs.Item[bool] `json:",omitzero"`
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
// It is implemented as a performance improvement and to enable omission of
// unconfigured preferences from the JSON output. See the [Prefs] doc for details.
func (p Prefs) MarshalJSONTo(out *jsontext.Encoder) error {
func (p Prefs) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
// The prefs type shadows the Prefs's method set,
// causing [jsonv2] to use the default marshaler and avoiding
// infinite recursion.
type prefs Prefs
return jsonv2.MarshalEncode(out, (*prefs)(&p))
return jsonv2.MarshalEncode(out, (*prefs)(&p), opts)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (p *Prefs) UnmarshalJSONFrom(in *jsontext.Decoder) error {
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
func (p *Prefs) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
// The prefs type shadows the Prefs's method set,
// causing [jsonv2] to use the default unmarshaler and avoiding
// infinite recursion.
type prefs Prefs
return jsonv2.UnmarshalDecode(in, (*prefs)(p))
return jsonv2.UnmarshalDecode(in, (*prefs)(p), opts)
}
// MarshalJSON implements [json.Marshaler].
func (p Prefs) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(p) // uses MarshalJSONTo
return jsonv2.Marshal(p) // uses MarshalJSONV2
}
// UnmarshalJSON implements [json.Unmarshaler].
func (p *Prefs) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, p) // uses UnmarshalJSONFrom
return jsonv2.Unmarshal(b, p) // uses UnmarshalJSONV2
}
type marshalAsTrueInJSON struct{}

View File

@@ -53,32 +53,32 @@ type TestPrefs struct {
Group TestPrefsGroup `json:",omitzero"`
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (p TestPrefs) MarshalJSONTo(out *jsontext.Encoder) error {
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
func (p TestPrefs) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
// The testPrefs type shadows the TestPrefs's method set,
// causing jsonv2 to use the default marshaler and avoiding
// infinite recursion.
type testPrefs TestPrefs
return jsonv2.MarshalEncode(out, (*testPrefs)(&p))
return jsonv2.MarshalEncode(out, (*testPrefs)(&p), opts)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (p *TestPrefs) UnmarshalJSONFrom(in *jsontext.Decoder) error {
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
func (p *TestPrefs) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
// The testPrefs type shadows the TestPrefs's method set,
// causing jsonv2 to use the default unmarshaler and avoiding
// infinite recursion.
type testPrefs TestPrefs
return jsonv2.UnmarshalDecode(in, (*testPrefs)(p))
return jsonv2.UnmarshalDecode(in, (*testPrefs)(p), opts)
}
// MarshalJSON implements [json.Marshaler].
func (p TestPrefs) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(p) // uses MarshalJSONTo
return jsonv2.Marshal(p) // uses MarshalJSONV2
}
// UnmarshalJSON implements [json.Unmarshaler].
func (p *TestPrefs) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, p) // uses UnmarshalJSONFrom
return jsonv2.Unmarshal(b, p) // uses UnmarshalJSONV2
}
// TestBundle is an example structure type that,

View File

@@ -169,15 +169,15 @@ func (lv StructListView[T, V]) Equal(lv2 StructListView[T, V]) bool {
return lv.ж.Equal(*lv2.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (lv StructListView[T, V]) MarshalJSONTo(out *jsontext.Encoder) error {
return lv.ж.MarshalJSONTo(out)
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
func (lv StructListView[T, V]) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
return lv.ж.MarshalJSONV2(out, opts)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (lv *StructListView[T, V]) UnmarshalJSONFrom(in *jsontext.Decoder) error {
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
func (lv *StructListView[T, V]) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
var x StructList[T]
if err := x.UnmarshalJSONFrom(in); err != nil {
if err := x.UnmarshalJSONV2(in, opts); err != nil {
return err
}
lv.ж = &x
@@ -186,10 +186,10 @@ func (lv *StructListView[T, V]) UnmarshalJSONFrom(in *jsontext.Decoder) error {
// MarshalJSON implements [json.Marshaler].
func (lv StructListView[T, V]) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(lv) // uses MarshalJSONTo
return jsonv2.Marshal(lv) // uses MarshalJSONV2
}
// UnmarshalJSON implements [json.Unmarshaler].
func (lv *StructListView[T, V]) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, lv) // uses UnmarshalJSONFrom
return jsonv2.Unmarshal(b, lv) // uses UnmarshalJSONV2
}

View File

@@ -149,15 +149,15 @@ func (mv StructMapView[K, T, V]) Equal(mv2 StructMapView[K, T, V]) bool {
return mv.ж.Equal(*mv2.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (mv StructMapView[K, T, V]) MarshalJSONTo(out *jsontext.Encoder) error {
return mv.ж.MarshalJSONTo(out)
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
func (mv StructMapView[K, T, V]) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
return mv.ж.MarshalJSONV2(out, opts)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (mv *StructMapView[K, T, V]) UnmarshalJSONFrom(in *jsontext.Decoder) error {
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
func (mv *StructMapView[K, T, V]) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
var x StructMap[K, T]
if err := x.UnmarshalJSONFrom(in); err != nil {
if err := x.UnmarshalJSONV2(in, opts); err != nil {
return err
}
mv.ж = &x
@@ -166,10 +166,10 @@ func (mv *StructMapView[K, T, V]) UnmarshalJSONFrom(in *jsontext.Decoder) error
// MarshalJSON implements [json.Marshaler].
func (mv StructMapView[K, T, V]) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(mv) // uses MarshalJSONTo
return jsonv2.Marshal(mv) // uses MarshalJSONV2
}
// UnmarshalJSON implements [json.Unmarshaler].
func (mv *StructMapView[K, T, V]) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, mv) // uses UnmarshalJSONFrom
return jsonv2.Unmarshal(b, mv) // uses UnmarshalJSONV2
}

View File

@@ -56,10 +56,10 @@ func EqualJSONForTest(tb TB, j1, j2 jsontext.Value) (s1, s2 string, equal bool)
return "", "", true
}
// Otherwise, format the values for display and return false.
if err := j1.Indent(); err != nil {
if err := j1.Indent("", "\t"); err != nil {
tb.Fatal(err)
}
if err := j2.Indent(); err != nil {
if err := j2.Indent("", "\t"); err != nil {
tb.Fatal(err)
}
return j1.String(), j2.String(), false

View File

@@ -42,12 +42,6 @@ const (
// for auditing purposes. It has no effect when [AlwaysOn] is false.
AlwaysOnOverrideWithReason Key = "AlwaysOn.OverrideWithReason"
// ReconnectAfter is a string value formatted for use with time.ParseDuration()
// that defines the duration after which the client should automatically reconnect
// to the Tailscale network following a user-initiated disconnect.
// An empty string or a zero duration disables automatic reconnection.
ReconnectAfter Key = "ReconnectAfter"
// ExitNodeID is the exit node's node id. default ""; if blank, no exit node is forced.
// Exit node ID takes precedence over exit node IP.
// To find the node ID, go to /api.md#device.
@@ -182,7 +176,6 @@ var implicitDefinitions = []*setting.Definition{
setting.NewDefinition(LogTarget, setting.DeviceSetting, setting.StringValue),
setting.NewDefinition(MachineCertificateSubject, setting.DeviceSetting, setting.StringValue),
setting.NewDefinition(PostureChecking, setting.DeviceSetting, setting.PreferenceOptionValue),
setting.NewDefinition(ReconnectAfter, setting.DeviceSetting, setting.DurationValue),
setting.NewDefinition(Tailnet, setting.DeviceSetting, setting.StringValue),
// User policy settings (can be configured on a user- or device-basis):

View File

@@ -50,22 +50,22 @@ func (s Origin) String() string {
return s.Scope().String()
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (s Origin) MarshalJSONTo(out *jsontext.Encoder) error {
return jsonv2.MarshalEncode(out, &s.data)
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
func (s Origin) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
return jsonv2.MarshalEncode(out, &s.data, opts)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (s *Origin) UnmarshalJSONFrom(in *jsontext.Decoder) error {
return jsonv2.UnmarshalDecode(in, &s.data)
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
func (s *Origin) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
return jsonv2.UnmarshalDecode(in, &s.data, opts)
}
// MarshalJSON implements [json.Marshaler].
func (s Origin) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(s) // uses MarshalJSONTo
return jsonv2.Marshal(s) // uses MarshalJSONV2
}
// UnmarshalJSON implements [json.Unmarshaler].
func (s *Origin) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, s) // uses UnmarshalJSONFrom
return jsonv2.Unmarshal(b, s) // uses UnmarshalJSONV2
}

View File

@@ -75,31 +75,31 @@ func (i RawItem) String() string {
return fmt.Sprintf("%v%s", i.data.Value.Value, suffix)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (i RawItem) MarshalJSONTo(out *jsontext.Encoder) error {
return jsonv2.MarshalEncode(out, &i.data)
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
func (i RawItem) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
return jsonv2.MarshalEncode(out, &i.data, opts)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (i *RawItem) UnmarshalJSONFrom(in *jsontext.Decoder) error {
return jsonv2.UnmarshalDecode(in, &i.data)
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
func (i *RawItem) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
return jsonv2.UnmarshalDecode(in, &i.data, opts)
}
// MarshalJSON implements [json.Marshaler].
func (i RawItem) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(i) // uses MarshalJSONTo
return jsonv2.Marshal(i) // uses MarshalJSONV2
}
// UnmarshalJSON implements [json.Unmarshaler].
func (i *RawItem) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, i) // uses UnmarshalJSONFrom
return jsonv2.Unmarshal(b, i) // uses UnmarshalJSONV2
}
// RawValue represents a raw policy setting value read from a policy store.
// It is JSON-marshallable and facilitates unmarshalling of JSON values
// into corresponding policy setting types, with special handling for JSON numbers
// (unmarshalled as float64) and JSON string arrays (unmarshalled as []string).
// See also [RawValue.UnmarshalJSONFrom].
// See also [RawValue.UnmarshalJSONV2].
type RawValue struct {
opt.Value[any]
}
@@ -114,16 +114,16 @@ func RawValueOf[T RawValueType](v T) RawValue {
return RawValue{opt.ValueOf[any](v)}
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v RawValue) MarshalJSONTo(out *jsontext.Encoder) error {
return jsonv2.MarshalEncode(out, v.Value)
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
func (v RawValue) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
return jsonv2.MarshalEncode(out, v.Value, opts)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom] by attempting to unmarshal
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2] by attempting to unmarshal
// a JSON value as one of the supported policy setting value types (bool, string, uint64, or []string),
// based on the JSON value type. It fails if the JSON value is an object, if it's a JSON number that
// cannot be represented as a uint64, or if a JSON array contains anything other than strings.
func (v *RawValue) UnmarshalJSONFrom(in *jsontext.Decoder) error {
func (v *RawValue) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
var valPtr any
switch k := in.PeekKind(); k {
case 't', 'f':
@@ -139,7 +139,7 @@ func (v *RawValue) UnmarshalJSONFrom(in *jsontext.Decoder) error {
default:
panic("unreachable")
}
if err := jsonv2.UnmarshalDecode(in, valPtr); err != nil {
if err := jsonv2.UnmarshalDecode(in, valPtr, opts); err != nil {
v.Value.Clear()
return err
}
@@ -150,12 +150,12 @@ func (v *RawValue) UnmarshalJSONFrom(in *jsontext.Decoder) error {
// MarshalJSON implements [json.Marshaler].
func (v RawValue) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(v) // uses MarshalJSONTo
return jsonv2.Marshal(v) // uses MarshalJSONV2
}
// UnmarshalJSON implements [json.Unmarshaler].
func (v *RawValue) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, v) // uses UnmarshalJSONFrom
return jsonv2.Unmarshal(b, v) // uses UnmarshalJSONV2
}
// RawValues is a map of keyed setting values that can be read from a JSON.

View File

@@ -147,23 +147,23 @@ type snapshotJSON struct {
Settings map[Key]RawItem `json:",omitempty"`
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (s *Snapshot) MarshalJSONTo(out *jsontext.Encoder) error {
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
func (s *Snapshot) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
data := &snapshotJSON{}
if s != nil {
data.Summary = s.summary
data.Settings = s.m
}
return jsonv2.MarshalEncode(out, data)
return jsonv2.MarshalEncode(out, data, opts)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (s *Snapshot) UnmarshalJSONFrom(in *jsontext.Decoder) error {
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
func (s *Snapshot) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
if s == nil {
return errors.New("s must not be nil")
}
data := &snapshotJSON{}
if err := jsonv2.UnmarshalDecode(in, data); err != nil {
if err := jsonv2.UnmarshalDecode(in, data, opts); err != nil {
return err
}
*s = Snapshot{m: data.Settings, sig: deephash.Hash(&data.Settings), summary: data.Summary}
@@ -172,12 +172,12 @@ func (s *Snapshot) UnmarshalJSONFrom(in *jsontext.Decoder) error {
// MarshalJSON implements [json.Marshaler].
func (s *Snapshot) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(s) // uses MarshalJSONTo
return jsonv2.Marshal(s) // uses MarshalJSONV2
}
// UnmarshalJSON implements [json.Unmarshaler].
func (s *Snapshot) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, s) // uses UnmarshalJSONFrom
return jsonv2.Unmarshal(b, s) // uses UnmarshalJSONV2
}
// MergeSnapshots returns a [Snapshot] that contains all [RawItem]s

View File

@@ -54,24 +54,24 @@ func (s Summary) String() string {
return s.data.Scope.String()
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (s Summary) MarshalJSONTo(out *jsontext.Encoder) error {
return jsonv2.MarshalEncode(out, &s.data)
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
func (s Summary) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
return jsonv2.MarshalEncode(out, &s.data, opts)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (s *Summary) UnmarshalJSONFrom(in *jsontext.Decoder) error {
return jsonv2.UnmarshalDecode(in, &s.data)
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
func (s *Summary) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
return jsonv2.UnmarshalDecode(in, &s.data, opts)
}
// MarshalJSON implements [json.Marshaler].
func (s Summary) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(s) // uses MarshalJSONTo
return jsonv2.Marshal(s) // uses MarshalJSONV2
}
// UnmarshalJSON implements [json.Unmarshaler].
func (s *Summary) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, s) // uses UnmarshalJSONFrom
return jsonv2.Unmarshal(b, s) // uses UnmarshalJSONV2
}
// SummaryOption is an option that configures [Summary]