diff options
| author | kari-ts <kari@tailscale.com> | 2025-03-19 11:28:04 -0700 |
|---|---|---|
| committer | kari-ts <kari@tailscale.com> | 2025-04-04 14:24:56 -0700 |
| commit | 6d5c7b11913e09b061e863411ad488dc44a13870 (patch) | |
| tree | 9e1789b5080ae4a92523611e49920dcb1102604b /control | |
| parent | ca50599c95e0a4cb7b4aab179e866e202f10c0c4 (diff) | |
| parent | 3a2c92f08eac8cd8f50356ff288e40a28636ee42 (diff) | |
| download | tailscale-kari/taildropsaf.tar.xz tailscale-kari/taildropsaf.zip | |
TO DO:kari/taildropsaf
-check if Context.getExternalFilesDirs works as is for private dir
Diffstat (limited to 'control')
| -rw-r--r-- | control/controlclient/auto.go | 7 | ||||
| -rw-r--r-- | control/controlclient/controlclient_test.go | 41 | ||||
| -rw-r--r-- | control/controlclient/direct.go | 52 | ||||
| -rw-r--r-- | control/controlclient/errors.go | 51 | ||||
| -rw-r--r-- | control/controlclient/map.go | 3 |
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) |
