summaryrefslogtreecommitdiffhomepage
path: root/control
diff options
context:
space:
mode:
authorkari-ts <kari@tailscale.com>2025-03-19 11:28:04 -0700
committerkari-ts <kari@tailscale.com>2025-04-04 14:24:56 -0700
commit6d5c7b11913e09b061e863411ad488dc44a13870 (patch)
tree9e1789b5080ae4a92523611e49920dcb1102604b /control
parentca50599c95e0a4cb7b4aab179e866e202f10c0c4 (diff)
parent3a2c92f08eac8cd8f50356ff288e40a28636ee42 (diff)
downloadtailscale-kari/taildropsaf.tar.xz
tailscale-kari/taildropsaf.zip
-check if Context.getExternalFilesDirs works as is for private dir
Diffstat (limited to 'control')
-rw-r--r--control/controlclient/auto.go7
-rw-r--r--control/controlclient/controlclient_test.go41
-rw-r--r--control/controlclient/direct.go52
-rw-r--r--control/controlclient/errors.go51
-rw-r--r--control/controlclient/map.go3
5 files changed, 152 insertions, 2 deletions
diff --git a/control/controlclient/auto.go b/control/controlclient/auto.go
index da123f8c4..e0168c19d 100644
--- a/control/controlclient/auto.go
+++ b/control/controlclient/auto.go
@@ -119,6 +119,7 @@ type Auto struct {
updateCh chan struct{} // readable when we should inform the server of a change
observer Observer // called to update Client status; always non-nil
observerQueue execqueue.ExecQueue
+ shutdownFn func() // to be called prior to shutdown or nil
unregisterHealthWatch func()
@@ -189,6 +190,7 @@ func NewNoStart(opts Options) (_ *Auto, err error) {
mapDone: make(chan struct{}),
updateDone: make(chan struct{}),
observer: opts.Observer,
+ shutdownFn: opts.Shutdown,
}
c.authCtx, c.authCancel = context.WithCancel(context.Background())
c.authCtx = sockstats.WithSockStats(c.authCtx, sockstats.LabelControlClientAuto, opts.Logf)
@@ -755,6 +757,7 @@ func (c *Auto) Shutdown() {
return
}
c.logf("client.Shutdown ...")
+ shutdownFn := c.shutdownFn
direct := c.direct
c.closed = true
@@ -767,6 +770,10 @@ func (c *Auto) Shutdown() {
c.unpauseWaiters = nil
c.mu.Unlock()
+ if shutdownFn != nil {
+ shutdownFn()
+ }
+
c.unregisterHealthWatch()
<-c.authDone
<-c.mapDone
diff --git a/control/controlclient/controlclient_test.go b/control/controlclient/controlclient_test.go
index 6885b5851..f8882a4e7 100644
--- a/control/controlclient/controlclient_test.go
+++ b/control/controlclient/controlclient_test.go
@@ -4,6 +4,8 @@
package controlclient
import (
+ "errors"
+ "fmt"
"io"
"reflect"
"slices"
@@ -147,3 +149,42 @@ func TestCanSkipStatus(t *testing.T) {
t.Errorf("Status fields = %q; this code was only written to handle fields %q", f, want)
}
}
+
+func TestRetryableErrors(t *testing.T) {
+ errorTests := []struct {
+ err error
+ want bool
+ }{
+ {errNoNoiseClient, true},
+ {errNoNodeKey, true},
+ {fmt.Errorf("%w: %w", errNoNoiseClient, errors.New("no noise")), true},
+ {fmt.Errorf("%w: %w", errHTTPPostFailure, errors.New("bad post")), true},
+ {fmt.Errorf("%w: %w", errNoNodeKey, errors.New("not node key")), true},
+ {errBadHTTPResponse(429, "too may requests"), true},
+ {errBadHTTPResponse(500, "internal server eror"), true},
+ {errBadHTTPResponse(502, "bad gateway"), true},
+ {errBadHTTPResponse(503, "service unavailable"), true},
+ {errBadHTTPResponse(504, "gateway timeout"), true},
+ {errBadHTTPResponse(1234, "random error"), false},
+ }
+
+ for _, tt := range errorTests {
+ t.Run(tt.err.Error(), func(t *testing.T) {
+ if isRetryableErrorForTest(tt.err) != tt.want {
+ t.Fatalf("retriable: got %v, want %v", tt.err, tt.want)
+ }
+ })
+ }
+}
+
+type retryableForTest interface {
+ Retryable() bool
+}
+
+func isRetryableErrorForTest(err error) bool {
+ var ae retryableForTest
+ if errors.As(err, &ae) {
+ return ae.Retryable()
+ }
+ return false
+}
diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go
index 883a1a587..68ab9ca17 100644
--- a/control/controlclient/direct.go
+++ b/control/controlclient/direct.go
@@ -156,6 +156,11 @@ type Options struct {
// If we receive a new DialPlan from the server, this value will be
// updated.
DialPlan ControlDialPlanner
+
+ // Shutdown is an optional function that will be called before client shutdown is
+ // attempted. It is used to allow the client to clean up any resources or complete any
+ // tasks that are dependent on a live client.
+ Shutdown func()
}
// ControlDialPlanner is the interface optionally supplied when creating a
@@ -1255,6 +1260,7 @@ type devKnobs struct {
DumpNetMapsVerbose func() bool
ForceProxyDNS func() bool
StripEndpoints func() bool // strip endpoints from control (only use disco messages)
+ StripHomeDERP func() bool // strip Home DERP from control
StripCaps func() bool // strip all local node's control-provided capabilities
}
@@ -1266,6 +1272,7 @@ func initDevKnob() devKnobs {
DumpRegister: envknob.RegisterBool("TS_DEBUG_REGISTER"),
ForceProxyDNS: envknob.RegisterBool("TS_DEBUG_PROXY_DNS"),
StripEndpoints: envknob.RegisterBool("TS_DEBUG_STRIP_ENDPOINTS"),
+ StripHomeDERP: envknob.RegisterBool("TS_DEBUG_STRIP_HOME_DERP"),
StripCaps: envknob.RegisterBool("TS_DEBUG_STRIP_CAPS"),
}
}
@@ -1660,11 +1667,11 @@ func (c *Auto) SetDeviceAttrs(ctx context.Context, attrs tailcfg.AttrUpdate) err
func (c *Direct) SetDeviceAttrs(ctx context.Context, attrs tailcfg.AttrUpdate) error {
nc, err := c.getNoiseClient()
if err != nil {
- return err
+ return fmt.Errorf("%w: %w", errNoNoiseClient, err)
}
nodeKey, ok := c.GetPersist().PublicNodeKeyOK()
if !ok {
- return errors.New("no node key")
+ return errNoNodeKey
}
if c.panicOnUse {
panic("tainted client")
@@ -1695,6 +1702,47 @@ func (c *Direct) SetDeviceAttrs(ctx context.Context, attrs tailcfg.AttrUpdate) e
return nil
}
+// SendAuditLog implements [auditlog.Transport] by sending an audit log synchronously to the control plane.
+//
+// See docs on [tailcfg.AuditLogRequest] and [auditlog.Logger] for background.
+func (c *Auto) SendAuditLog(ctx context.Context, auditLog tailcfg.AuditLogRequest) (err error) {
+ return c.direct.sendAuditLog(ctx, auditLog)
+}
+
+func (c *Direct) sendAuditLog(ctx context.Context, auditLog tailcfg.AuditLogRequest) (err error) {
+ nc, err := c.getNoiseClient()
+ if err != nil {
+ return fmt.Errorf("%w: %w", errNoNoiseClient, err)
+ }
+
+ nodeKey, ok := c.GetPersist().PublicNodeKeyOK()
+ if !ok {
+ return errNoNodeKey
+ }
+
+ req := &tailcfg.AuditLogRequest{
+ Version: tailcfg.CurrentCapabilityVersion,
+ NodeKey: nodeKey,
+ Action: auditLog.Action,
+ Details: auditLog.Details,
+ }
+
+ if c.panicOnUse {
+ panic("tainted client")
+ }
+
+ res, err := nc.post(ctx, "/machine/audit-log", nodeKey, req)
+ if err != nil {
+ return fmt.Errorf("%w: %w", errHTTPPostFailure, err)
+ }
+ defer res.Body.Close()
+ if res.StatusCode != 200 {
+ all, _ := io.ReadAll(res.Body)
+ return errBadHTTPResponse(res.StatusCode, string(all))
+ }
+ return nil
+}
+
func addLBHeader(req *http.Request, nodeKey key.NodePublic) {
if !nodeKey.IsZero() {
req.Header.Add(tailcfg.LBHeader, nodeKey.String())
diff --git a/control/controlclient/errors.go b/control/controlclient/errors.go
new file mode 100644
index 000000000..9b4dab844
--- /dev/null
+++ b/control/controlclient/errors.go
@@ -0,0 +1,51 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package controlclient
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+)
+
+// apiResponseError is an error type that can be returned by controlclient
+// api requests.
+//
+// It wraps an underlying error and a flag for clients to query if the
+// error is retryable via the Retryable() method.
+type apiResponseError struct {
+ err error
+ retryable bool
+}
+
+// Error implements [error].
+func (e *apiResponseError) Error() string {
+ return e.err.Error()
+}
+
+// Retryable reports whether the error is retryable.
+func (e *apiResponseError) Retryable() bool {
+ return e.retryable
+}
+
+func (e *apiResponseError) Unwrap() error { return e.err }
+
+var (
+ errNoNodeKey = &apiResponseError{errors.New("no node key"), true}
+ errNoNoiseClient = &apiResponseError{errors.New("no noise client"), true}
+ errHTTPPostFailure = &apiResponseError{errors.New("http failure"), true}
+)
+
+func errBadHTTPResponse(code int, msg string) error {
+ retryable := false
+ switch code {
+ case http.StatusTooManyRequests,
+ http.StatusInternalServerError,
+ http.StatusBadGateway,
+ http.StatusServiceUnavailable,
+ http.StatusGatewayTimeout:
+ retryable = true
+ }
+ return &apiResponseError{fmt.Errorf("http error %d: %s", code, msg), retryable}
+}
diff --git a/control/controlclient/map.go b/control/controlclient/map.go
index df2182c8b..769c8f1e3 100644
--- a/control/controlclient/map.go
+++ b/control/controlclient/map.go
@@ -240,6 +240,9 @@ func upgradeNode(n *tailcfg.Node) {
}
n.LegacyDERPString = ""
}
+ if DevKnob.StripHomeDERP() {
+ n.HomeDERP = 0
+ }
if n.AllowedIPs == nil {
n.AllowedIPs = slices.Clone(n.Addresses)