Update dependencies
This commit is contained in:
83
vendor/tailscale.com/control/controlclient/auto.go
generated
vendored
83
vendor/tailscale.com/control/controlclient/auto.go
generated
vendored
@@ -21,6 +21,7 @@ import (
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/execqueue"
|
||||
)
|
||||
|
||||
@@ -118,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()
|
||||
|
||||
@@ -131,6 +133,8 @@ type Auto struct {
|
||||
// the server.
|
||||
lastUpdateGen updateGen
|
||||
|
||||
lastStatus atomic.Pointer[Status]
|
||||
|
||||
paused bool // whether we should stop making HTTP requests
|
||||
unpauseWaiters []chan bool // chans that gets sent true (once) on wake, or false on Shutdown
|
||||
loggedIn bool // true if currently logged in
|
||||
@@ -186,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)
|
||||
@@ -596,21 +601,90 @@ func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkM
|
||||
// not logged in.
|
||||
nm = nil
|
||||
}
|
||||
new := Status{
|
||||
newSt := &Status{
|
||||
URL: url,
|
||||
Persist: p,
|
||||
NetMap: nm,
|
||||
Err: err,
|
||||
state: state,
|
||||
}
|
||||
c.lastStatus.Store(newSt)
|
||||
|
||||
// Launch a new goroutine to avoid blocking the caller while the observer
|
||||
// does its thing, which may result in a call back into the client.
|
||||
metricQueued.Add(1)
|
||||
c.observerQueue.Add(func() {
|
||||
c.observer.SetControlClientStatus(c, new)
|
||||
if canSkipStatus(newSt, c.lastStatus.Load()) {
|
||||
metricSkippable.Add(1)
|
||||
if !c.direct.controlKnobs.DisableSkipStatusQueue.Load() {
|
||||
metricSkipped.Add(1)
|
||||
return
|
||||
}
|
||||
}
|
||||
c.observer.SetControlClientStatus(c, *newSt)
|
||||
|
||||
// Best effort stop retaining the memory now that we've sent it to the
|
||||
// observer (LocalBackend). We CAS here because the caller goroutine is
|
||||
// doing a Store which we want to win a race. This is only a memory
|
||||
// optimization and is not for correctness.
|
||||
//
|
||||
// If the CAS fails, that means somebody else's Store replaced our
|
||||
// pointer (so mission accomplished: our netmap is no longer retained in
|
||||
// any case) and that Store caller will be responsible for removing
|
||||
// their own netmap (or losing their race too, down the chain).
|
||||
// Eventually the last caller will win this CAS and zero lastStatus.
|
||||
c.lastStatus.CompareAndSwap(newSt, nil)
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
metricQueued = clientmetric.NewCounter("controlclient_auto_status_queued")
|
||||
metricSkippable = clientmetric.NewCounter("controlclient_auto_status_queue_skippable")
|
||||
metricSkipped = clientmetric.NewCounter("controlclient_auto_status_queue_skipped")
|
||||
)
|
||||
|
||||
// canSkipStatus reports whether we can skip sending s1, knowing
|
||||
// that s2 is enqueued sometime in the future after s1.
|
||||
//
|
||||
// s1 must be non-nil. s2 may be nil.
|
||||
func canSkipStatus(s1, s2 *Status) bool {
|
||||
if s2 == nil {
|
||||
// Nothing in the future.
|
||||
return false
|
||||
}
|
||||
if s1 == s2 {
|
||||
// If the last item in the queue is the same as s1,
|
||||
// we can't skip it.
|
||||
return false
|
||||
}
|
||||
if s1.Err != nil || s1.URL != "" {
|
||||
// If s1 has an error or a URL, we shouldn't skip it, lest the error go
|
||||
// away in s2 or in-between. We want to make sure all the subsystems see
|
||||
// it. Plus there aren't many of these, so not worth skipping.
|
||||
return false
|
||||
}
|
||||
if !s1.Persist.Equals(s2.Persist) || s1.state != s2.state {
|
||||
// If s1 has a different Persist or state than s2,
|
||||
// don't skip it. We only care about skipping the typical
|
||||
// entries where the only difference is the NetMap.
|
||||
return false
|
||||
}
|
||||
// If nothing above precludes it, and both s1 and s2 have NetMaps, then
|
||||
// we can skip it, because s2's NetMap is a newer version and we can
|
||||
// jump straight from whatever state we had before to s2's state,
|
||||
// without passing through s1's state first. A NetMap is regrettably a
|
||||
// full snapshot of the state, not an incremental delta. We're slowly
|
||||
// moving towards passing around only deltas around internally at all
|
||||
// layers, but this is explicitly the case where we didn't have a delta
|
||||
// path for the message we received over the wire and had to resort
|
||||
// to the legacy full NetMap path. And then we can get behind processing
|
||||
// these full NetMap snapshots in LocalBackend/wgengine/magicsock/netstack
|
||||
// and this path (when it returns true) lets us skip over useless work
|
||||
// and not get behind in the queue. This matters in particular for tailnets
|
||||
// that are both very large + very churny.
|
||||
return s1.NetMap != nil && s2.NetMap != nil
|
||||
}
|
||||
|
||||
func (c *Auto) Login(flags LoginFlags) {
|
||||
c.logf("client.Login(%v)", flags)
|
||||
|
||||
@@ -683,6 +757,7 @@ func (c *Auto) Shutdown() {
|
||||
return
|
||||
}
|
||||
c.logf("client.Shutdown ...")
|
||||
shutdownFn := c.shutdownFn
|
||||
|
||||
direct := c.direct
|
||||
c.closed = true
|
||||
@@ -695,6 +770,10 @@ func (c *Auto) Shutdown() {
|
||||
c.unpauseWaiters = nil
|
||||
c.mu.Unlock()
|
||||
|
||||
if shutdownFn != nil {
|
||||
shutdownFn()
|
||||
}
|
||||
|
||||
c.unregisterHealthWatch()
|
||||
<-c.authDone
|
||||
<-c.mapDone
|
||||
|
||||
142
vendor/tailscale.com/control/controlclient/direct.go
generated
vendored
142
vendor/tailscale.com/control/controlclient/direct.go
generated
vendored
@@ -15,7 +15,6 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -42,6 +41,7 @@ import (
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tempfork/httprec"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/key"
|
||||
@@ -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
|
||||
@@ -650,7 +655,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
c.logf("RegisterReq sign error: %v", err)
|
||||
}
|
||||
}
|
||||
if debugRegister() {
|
||||
if DevKnob.DumpRegister() {
|
||||
j, _ := json.MarshalIndent(request, "", "\t")
|
||||
c.logf("RegisterRequest: %s", j)
|
||||
}
|
||||
@@ -691,7 +696,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
c.logf("error decoding RegisterResponse with server key %s and machine key %s: %v", serverKey, machinePrivKey.Public(), err)
|
||||
return regen, opt.URL, nil, fmt.Errorf("register request: %v", err)
|
||||
}
|
||||
if debugRegister() {
|
||||
if DevKnob.DumpRegister() {
|
||||
j, _ := json.MarshalIndent(resp, "", "\t")
|
||||
c.logf("RegisterResponse: %s", j)
|
||||
}
|
||||
@@ -877,7 +882,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
c.logf("[v1] PollNetMap: stream=%v ep=%v", isStreaming, epStrs)
|
||||
|
||||
vlogf := logger.Discard
|
||||
if DevKnob.DumpNetMaps() {
|
||||
if DevKnob.DumpNetMapsVerbose() {
|
||||
// TODO(bradfitz): update this to use "[v2]" prefix perhaps? but we don't
|
||||
// want to upload it always.
|
||||
vlogf = c.logf
|
||||
@@ -1003,7 +1008,9 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
if persist == c.persist {
|
||||
newPersist := persist.AsStruct()
|
||||
newPersist.NodeID = nm.SelfNode.StableID()
|
||||
newPersist.UserProfile = nm.UserProfiles[nm.User()]
|
||||
if up, ok := nm.UserProfiles[nm.User()]; ok {
|
||||
newPersist.UserProfile = *up.AsStruct()
|
||||
}
|
||||
|
||||
c.persist = newPersist.View()
|
||||
persist = c.persist
|
||||
@@ -1170,11 +1177,6 @@ func decode(res *http.Response, v any) error {
|
||||
return json.Unmarshal(msg, v)
|
||||
}
|
||||
|
||||
var (
|
||||
debugMap = envknob.RegisterBool("TS_DEBUG_MAP")
|
||||
debugRegister = envknob.RegisterBool("TS_DEBUG_REGISTER")
|
||||
)
|
||||
|
||||
var jsonEscapedZero = []byte(`\u0000`)
|
||||
|
||||
// decodeMsg is responsible for uncompressing msg and unmarshaling into v.
|
||||
@@ -1183,7 +1185,7 @@ func (c *Direct) decodeMsg(compressedMsg []byte, v any) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if debugMap() {
|
||||
if DevKnob.DumpNetMaps() {
|
||||
var buf bytes.Buffer
|
||||
json.Indent(&buf, b, "", " ")
|
||||
log.Printf("MapResponse: %s", buf.Bytes())
|
||||
@@ -1205,7 +1207,7 @@ func encode(v any) ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if debugMap() {
|
||||
if DevKnob.DumpNetMaps() {
|
||||
if _, ok := v.(*tailcfg.MapRequest); ok {
|
||||
log.Printf("MapRequest: %s", b)
|
||||
}
|
||||
@@ -1253,18 +1255,25 @@ func loadServerPubKeys(ctx context.Context, httpc *http.Client, serverURL string
|
||||
var DevKnob = initDevKnob()
|
||||
|
||||
type devKnobs struct {
|
||||
DumpNetMaps func() bool
|
||||
ForceProxyDNS func() bool
|
||||
StripEndpoints func() bool // strip endpoints from control (only use disco messages)
|
||||
StripCaps func() bool // strip all local node's control-provided capabilities
|
||||
DumpRegister func() bool
|
||||
DumpNetMaps func() bool
|
||||
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
|
||||
}
|
||||
|
||||
func initDevKnob() devKnobs {
|
||||
nm := envknob.RegisterInt("TS_DEBUG_MAP")
|
||||
return devKnobs{
|
||||
DumpNetMaps: envknob.RegisterBool("TS_DEBUG_NETMAP"),
|
||||
ForceProxyDNS: envknob.RegisterBool("TS_DEBUG_PROXY_DNS"),
|
||||
StripEndpoints: envknob.RegisterBool("TS_DEBUG_STRIP_ENDPOINTS"),
|
||||
StripCaps: envknob.RegisterBool("TS_DEBUG_STRIP_CAPS"),
|
||||
DumpNetMaps: func() bool { return nm() > 0 },
|
||||
DumpNetMapsVerbose: func() bool { return nm() > 1 },
|
||||
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"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1384,7 +1393,7 @@ func answerC2NPing(logf logger.Logf, c2nHandler http.Handler, c *http.Client, pr
|
||||
handlerCtx, cancel := context.WithTimeout(context.Background(), handlerTimeout)
|
||||
defer cancel()
|
||||
hreq = hreq.WithContext(handlerCtx)
|
||||
rec := httptest.NewRecorder()
|
||||
rec := httprec.NewRecorder()
|
||||
c2nHandler.ServeHTTP(rec, hreq)
|
||||
cancel()
|
||||
|
||||
@@ -1643,6 +1652,97 @@ func (c *Direct) ReportHealthChange(w *health.Warnable, us *health.UnhealthyStat
|
||||
res.Body.Close()
|
||||
}
|
||||
|
||||
// SetDeviceAttrs does a synchronous call to the control plane to update
|
||||
// the node's attributes.
|
||||
//
|
||||
// See docs on [tailcfg.SetDeviceAttributesRequest] for background.
|
||||
func (c *Auto) SetDeviceAttrs(ctx context.Context, attrs tailcfg.AttrUpdate) error {
|
||||
return c.direct.SetDeviceAttrs(ctx, attrs)
|
||||
}
|
||||
|
||||
// SetDeviceAttrs does a synchronous call to the control plane to update
|
||||
// the node's attributes.
|
||||
//
|
||||
// See docs on [tailcfg.SetDeviceAttributesRequest] for background.
|
||||
func (c *Direct) SetDeviceAttrs(ctx context.Context, attrs tailcfg.AttrUpdate) error {
|
||||
nc, err := c.getNoiseClient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", errNoNoiseClient, err)
|
||||
}
|
||||
nodeKey, ok := c.GetPersist().PublicNodeKeyOK()
|
||||
if !ok {
|
||||
return errNoNodeKey
|
||||
}
|
||||
if c.panicOnUse {
|
||||
panic("tainted client")
|
||||
}
|
||||
req := &tailcfg.SetDeviceAttributesRequest{
|
||||
NodeKey: nodeKey,
|
||||
Version: tailcfg.CurrentCapabilityVersion,
|
||||
Update: attrs,
|
||||
}
|
||||
|
||||
// TODO(bradfitz): unify the callers using doWithBody vs those using
|
||||
// DoNoiseRequest. There seems to be a ~50/50 split and they're very close,
|
||||
// but doWithBody sets the load balancing header and auto-JSON-encodes the
|
||||
// body, but DoNoiseRequest is exported. Clean it up so they're consistent
|
||||
// one way or another.
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
res, err := nc.doWithBody(ctx, "PATCH", "/machine/set-device-attr", nodeKey, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
all, _ := io.ReadAll(res.Body)
|
||||
if res.StatusCode != 200 {
|
||||
return fmt.Errorf("HTTP error from control plane: %v: %s", res.Status, all)
|
||||
}
|
||||
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())
|
||||
|
||||
51
vendor/tailscale.com/control/controlclient/errors.go
generated
vendored
Normal file
51
vendor/tailscale.com/control/controlclient/errors.go
generated
vendored
Normal file
@@ -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}
|
||||
}
|
||||
180
vendor/tailscale.com/control/controlclient/map.go
generated
vendored
180
vendor/tailscale.com/control/controlclient/map.go
generated
vendored
@@ -7,14 +7,12 @@ import (
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -31,6 +29,7 @@ import (
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/slicesx"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
@@ -75,11 +74,10 @@ type mapSession struct {
|
||||
lastPrintMap time.Time
|
||||
lastNode tailcfg.NodeView
|
||||
lastCapSet set.Set[tailcfg.NodeCapability]
|
||||
peers map[tailcfg.NodeID]*tailcfg.NodeView // pointer to view (oddly). same pointers as sortedPeers.
|
||||
sortedPeers []*tailcfg.NodeView // same pointers as peers, but sorted by Node.ID
|
||||
peers map[tailcfg.NodeID]tailcfg.NodeView
|
||||
lastDNSConfig *tailcfg.DNSConfig
|
||||
lastDERPMap *tailcfg.DERPMap
|
||||
lastUserProfile map[tailcfg.UserID]tailcfg.UserProfile
|
||||
lastUserProfile map[tailcfg.UserID]tailcfg.UserProfileView
|
||||
lastPacketFilterRules views.Slice[tailcfg.FilterRule] // concatenation of all namedPacketFilters
|
||||
namedPacketFilters map[string]views.Slice[tailcfg.FilterRule]
|
||||
lastParsedPacketFilter []filter.Match
|
||||
@@ -91,7 +89,6 @@ type mapSession struct {
|
||||
lastPopBrowserURL string
|
||||
lastTKAInfo *tailcfg.TKAInfo
|
||||
lastNetmapSummary string // from NetworkMap.VeryConcise
|
||||
lastMaxExpiry time.Duration
|
||||
}
|
||||
|
||||
// newMapSession returns a mostly unconfigured new mapSession.
|
||||
@@ -106,7 +103,7 @@ func newMapSession(privateNodeKey key.NodePrivate, nu NetmapUpdater, controlKnob
|
||||
privateNodeKey: privateNodeKey,
|
||||
publicNodeKey: privateNodeKey.Public(),
|
||||
lastDNSConfig: new(tailcfg.DNSConfig),
|
||||
lastUserProfile: map[tailcfg.UserID]tailcfg.UserProfile{},
|
||||
lastUserProfile: map[tailcfg.UserID]tailcfg.UserProfileView{},
|
||||
|
||||
// Non-nil no-op defaults, to be optionally overridden by the caller.
|
||||
logf: logger.Discard,
|
||||
@@ -167,6 +164,7 @@ func (ms *mapSession) HandleNonKeepAliveMapResponse(ctx context.Context, resp *t
|
||||
|
||||
// For responses that mutate the self node, check for updated nodeAttrs.
|
||||
if resp.Node != nil {
|
||||
upgradeNode(resp.Node)
|
||||
if DevKnob.StripCaps() {
|
||||
resp.Node.Capabilities = nil
|
||||
resp.Node.CapMap = nil
|
||||
@@ -182,6 +180,13 @@ func (ms *mapSession) HandleNonKeepAliveMapResponse(ctx context.Context, resp *t
|
||||
ms.controlKnobs.UpdateFromNodeAttributes(resp.Node.CapMap)
|
||||
}
|
||||
|
||||
for _, p := range resp.Peers {
|
||||
upgradeNode(p)
|
||||
}
|
||||
for _, p := range resp.PeersChanged {
|
||||
upgradeNode(p)
|
||||
}
|
||||
|
||||
// Call Node.InitDisplayNames on any changed nodes.
|
||||
initDisplayNames(cmp.Or(resp.Node.View(), ms.lastNode), resp)
|
||||
|
||||
@@ -217,6 +222,33 @@ func (ms *mapSession) HandleNonKeepAliveMapResponse(ctx context.Context, resp *t
|
||||
return nil
|
||||
}
|
||||
|
||||
// upgradeNode upgrades Node fields from the server into the modern forms
|
||||
// not using deprecated fields.
|
||||
func upgradeNode(n *tailcfg.Node) {
|
||||
if n == nil {
|
||||
return
|
||||
}
|
||||
if n.LegacyDERPString != "" {
|
||||
if n.HomeDERP == 0 {
|
||||
ip, portStr, err := net.SplitHostPort(n.LegacyDERPString)
|
||||
if ip == tailcfg.DerpMagicIP && err == nil {
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err == nil {
|
||||
n.HomeDERP = port
|
||||
}
|
||||
}
|
||||
}
|
||||
n.LegacyDERPString = ""
|
||||
}
|
||||
if DevKnob.StripHomeDERP() {
|
||||
n.HomeDERP = 0
|
||||
}
|
||||
|
||||
if n.AllowedIPs == nil {
|
||||
n.AllowedIPs = slices.Clone(n.Addresses)
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *mapSession) tryHandleIncrementally(res *tailcfg.MapResponse) bool {
|
||||
if ms.controlKnobs != nil && ms.controlKnobs.DisableDeltaUpdates.Load() {
|
||||
return false
|
||||
@@ -260,13 +292,22 @@ func (ms *mapSession) updateStateFromResponse(resp *tailcfg.MapResponse) {
|
||||
}
|
||||
|
||||
for _, up := range resp.UserProfiles {
|
||||
ms.lastUserProfile[up.ID] = up
|
||||
ms.lastUserProfile[up.ID] = up.View()
|
||||
}
|
||||
// TODO(bradfitz): clean up old user profiles? maybe not worth it.
|
||||
|
||||
if dm := resp.DERPMap; dm != nil {
|
||||
ms.vlogf("netmap: new map contains DERP map")
|
||||
|
||||
// Guard against the control server accidentally sending
|
||||
// a nil region definition, which at least Headscale was
|
||||
// observed to send.
|
||||
for rid, r := range dm.Regions {
|
||||
if r == nil {
|
||||
delete(dm.Regions, rid)
|
||||
}
|
||||
}
|
||||
|
||||
// Zero-valued fields in a DERPMap mean that we're not changing
|
||||
// anything and are using the previous value(s).
|
||||
if ldm := ms.lastDERPMap; ldm != nil {
|
||||
@@ -345,9 +386,6 @@ func (ms *mapSession) updateStateFromResponse(resp *tailcfg.MapResponse) {
|
||||
if resp.TKAInfo != nil {
|
||||
ms.lastTKAInfo = resp.TKAInfo
|
||||
}
|
||||
if resp.MaxKeyDuration > 0 {
|
||||
ms.lastMaxExpiry = resp.MaxKeyDuration
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -366,16 +404,11 @@ var (
|
||||
patchifiedPeerEqual = clientmetric.NewCounter("controlclient_patchified_peer_equal")
|
||||
)
|
||||
|
||||
// updatePeersStateFromResponseres updates ms.peers and ms.sortedPeers from res. It takes ownership of res.
|
||||
// updatePeersStateFromResponseres updates ms.peers from resp.
|
||||
// It takes ownership of resp.
|
||||
func (ms *mapSession) updatePeersStateFromResponse(resp *tailcfg.MapResponse) (stats updateStats) {
|
||||
defer func() {
|
||||
if stats.removed > 0 || stats.added > 0 {
|
||||
ms.rebuildSorted()
|
||||
}
|
||||
}()
|
||||
|
||||
if ms.peers == nil {
|
||||
ms.peers = make(map[tailcfg.NodeID]*tailcfg.NodeView)
|
||||
ms.peers = make(map[tailcfg.NodeID]tailcfg.NodeView)
|
||||
}
|
||||
|
||||
if len(resp.Peers) > 0 {
|
||||
@@ -384,12 +417,12 @@ func (ms *mapSession) updatePeersStateFromResponse(resp *tailcfg.MapResponse) (s
|
||||
keep := make(map[tailcfg.NodeID]bool, len(resp.Peers))
|
||||
for _, n := range resp.Peers {
|
||||
keep[n.ID] = true
|
||||
if vp, ok := ms.peers[n.ID]; ok {
|
||||
lenBefore := len(ms.peers)
|
||||
ms.peers[n.ID] = n.View()
|
||||
if len(ms.peers) == lenBefore {
|
||||
stats.changed++
|
||||
*vp = n.View()
|
||||
} else {
|
||||
stats.added++
|
||||
ms.peers[n.ID] = ptr.To(n.View())
|
||||
}
|
||||
}
|
||||
for id := range ms.peers {
|
||||
@@ -410,12 +443,12 @@ func (ms *mapSession) updatePeersStateFromResponse(resp *tailcfg.MapResponse) (s
|
||||
}
|
||||
|
||||
for _, n := range resp.PeersChanged {
|
||||
if vp, ok := ms.peers[n.ID]; ok {
|
||||
lenBefore := len(ms.peers)
|
||||
ms.peers[n.ID] = n.View()
|
||||
if len(ms.peers) == lenBefore {
|
||||
stats.changed++
|
||||
*vp = n.View()
|
||||
} else {
|
||||
stats.added++
|
||||
ms.peers[n.ID] = ptr.To(n.View())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,7 +460,7 @@ func (ms *mapSession) updatePeersStateFromResponse(resp *tailcfg.MapResponse) (s
|
||||
} else {
|
||||
mut.LastSeen = nil
|
||||
}
|
||||
*vp = mut.View()
|
||||
ms.peers[nodeID] = mut.View()
|
||||
stats.changed++
|
||||
}
|
||||
}
|
||||
@@ -436,7 +469,7 @@ func (ms *mapSession) updatePeersStateFromResponse(resp *tailcfg.MapResponse) (s
|
||||
if vp, ok := ms.peers[nodeID]; ok {
|
||||
mut := vp.AsStruct()
|
||||
mut.Online = ptr.To(online)
|
||||
*vp = mut.View()
|
||||
ms.peers[nodeID] = mut.View()
|
||||
stats.changed++
|
||||
}
|
||||
}
|
||||
@@ -449,7 +482,7 @@ func (ms *mapSession) updatePeersStateFromResponse(resp *tailcfg.MapResponse) (s
|
||||
stats.changed++
|
||||
mut := vp.AsStruct()
|
||||
if pc.DERPRegion != 0 {
|
||||
mut.DERP = fmt.Sprintf("%s:%v", tailcfg.DerpMagicIP, pc.DERPRegion)
|
||||
mut.HomeDERP = pc.DERPRegion
|
||||
patchDERPRegion.Add(1)
|
||||
}
|
||||
if pc.Cap != 0 {
|
||||
@@ -488,31 +521,12 @@ func (ms *mapSession) updatePeersStateFromResponse(resp *tailcfg.MapResponse) (s
|
||||
mut.CapMap = v
|
||||
patchCapMap.Add(1)
|
||||
}
|
||||
*vp = mut.View()
|
||||
ms.peers[pc.NodeID] = mut.View()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// rebuildSorted rebuilds ms.sortedPeers from ms.peers. It should be called
|
||||
// after any additions or removals from peers.
|
||||
func (ms *mapSession) rebuildSorted() {
|
||||
if ms.sortedPeers == nil {
|
||||
ms.sortedPeers = make([]*tailcfg.NodeView, 0, len(ms.peers))
|
||||
} else {
|
||||
if len(ms.sortedPeers) > len(ms.peers) {
|
||||
clear(ms.sortedPeers[len(ms.peers):])
|
||||
}
|
||||
ms.sortedPeers = ms.sortedPeers[:0]
|
||||
}
|
||||
for _, p := range ms.peers {
|
||||
ms.sortedPeers = append(ms.sortedPeers, p)
|
||||
}
|
||||
sort.Slice(ms.sortedPeers, func(i, j int) bool {
|
||||
return ms.sortedPeers[i].ID() < ms.sortedPeers[j].ID()
|
||||
})
|
||||
}
|
||||
|
||||
func (ms *mapSession) addUserProfile(nm *netmap.NetworkMap, userID tailcfg.UserID) {
|
||||
if userID == 0 {
|
||||
return
|
||||
@@ -576,7 +590,7 @@ func (ms *mapSession) patchifyPeer(n *tailcfg.Node) (_ *tailcfg.PeerChange, ok b
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return peerChangeDiff(*was, n)
|
||||
return peerChangeDiff(was, n)
|
||||
}
|
||||
|
||||
// peerChangeDiff returns the difference from 'was' to 'n', if possible.
|
||||
@@ -656,17 +670,13 @@ func peerChangeDiff(was tailcfg.NodeView, n *tailcfg.Node) (_ *tailcfg.PeerChang
|
||||
if !views.SliceEqual(was.Endpoints(), views.SliceOf(n.Endpoints)) {
|
||||
pc().Endpoints = slices.Clone(n.Endpoints)
|
||||
}
|
||||
case "DERP":
|
||||
if was.DERP() != n.DERP {
|
||||
ip, portStr, err := net.SplitHostPort(n.DERP)
|
||||
if err != nil || ip != "127.3.3.40" {
|
||||
return nil, false
|
||||
}
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil || port < 1 || port > 65535 {
|
||||
return nil, false
|
||||
}
|
||||
pc().DERPRegion = port
|
||||
case "LegacyDERPString":
|
||||
if was.LegacyDERPString() != "" || n.LegacyDERPString != "" {
|
||||
panic("unexpected; caller should've already called upgradeNode")
|
||||
}
|
||||
case "HomeDERP":
|
||||
if was.HomeDERP() != n.HomeDERP {
|
||||
pc().DERPRegion = n.HomeDERP
|
||||
}
|
||||
case "Hostinfo":
|
||||
if !was.Hostinfo().Valid() && !n.Hostinfo.Valid() {
|
||||
@@ -688,21 +698,23 @@ func peerChangeDiff(was tailcfg.NodeView, n *tailcfg.Node) (_ *tailcfg.PeerChang
|
||||
}
|
||||
case "CapMap":
|
||||
if len(n.CapMap) != was.CapMap().Len() {
|
||||
// If they have different lengths, they're different.
|
||||
if n.CapMap == nil {
|
||||
pc().CapMap = make(tailcfg.NodeCapMap)
|
||||
} else {
|
||||
pc().CapMap = maps.Clone(n.CapMap)
|
||||
}
|
||||
break
|
||||
}
|
||||
was.CapMap().Range(func(k tailcfg.NodeCapability, v views.Slice[tailcfg.RawMessage]) bool {
|
||||
nv, ok := n.CapMap[k]
|
||||
if !ok || !views.SliceEqual(v, views.SliceOf(nv)) {
|
||||
pc().CapMap = maps.Clone(n.CapMap)
|
||||
return false
|
||||
} else {
|
||||
// If they have the same length, check that all their keys
|
||||
// have the same values.
|
||||
for k, v := range was.CapMap().All() {
|
||||
nv, ok := n.CapMap[k]
|
||||
if !ok || !views.SliceEqual(v, views.SliceOf(nv)) {
|
||||
pc().CapMap = maps.Clone(n.CapMap)
|
||||
break
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
case "Tags":
|
||||
if !views.SliceEqual(was.Tags(), views.SliceOf(n.Tags)) {
|
||||
return nil, false
|
||||
@@ -712,13 +724,11 @@ func peerChangeDiff(was tailcfg.NodeView, n *tailcfg.Node) (_ *tailcfg.PeerChang
|
||||
return nil, false
|
||||
}
|
||||
case "Online":
|
||||
wasOnline := was.Online()
|
||||
if n.Online != nil && wasOnline != nil && *n.Online != *wasOnline {
|
||||
if wasOnline, ok := was.Online().GetOk(); ok && n.Online != nil && *n.Online != wasOnline {
|
||||
pc().Online = ptr.To(*n.Online)
|
||||
}
|
||||
case "LastSeen":
|
||||
wasSeen := was.LastSeen()
|
||||
if n.LastSeen != nil && wasSeen != nil && !wasSeen.Equal(*n.LastSeen) {
|
||||
if wasSeen, ok := was.LastSeen().GetOk(); ok && n.LastSeen != nil && !wasSeen.Equal(*n.LastSeen) {
|
||||
pc().LastSeen = ptr.To(*n.LastSeen)
|
||||
}
|
||||
case "MachineAuthorized":
|
||||
@@ -743,18 +753,18 @@ func peerChangeDiff(was tailcfg.NodeView, n *tailcfg.Node) (_ *tailcfg.PeerChang
|
||||
}
|
||||
case "SelfNodeV4MasqAddrForThisPeer":
|
||||
va, vb := was.SelfNodeV4MasqAddrForThisPeer(), n.SelfNodeV4MasqAddrForThisPeer
|
||||
if va == nil && vb == nil {
|
||||
if !va.Valid() && vb == nil {
|
||||
continue
|
||||
}
|
||||
if va == nil || vb == nil || *va != *vb {
|
||||
if va, ok := va.GetOk(); !ok || vb == nil || va != *vb {
|
||||
return nil, false
|
||||
}
|
||||
case "SelfNodeV6MasqAddrForThisPeer":
|
||||
va, vb := was.SelfNodeV6MasqAddrForThisPeer(), n.SelfNodeV6MasqAddrForThisPeer
|
||||
if va == nil && vb == nil {
|
||||
if !va.Valid() && vb == nil {
|
||||
continue
|
||||
}
|
||||
if va == nil || vb == nil || *va != *vb {
|
||||
if va, ok := va.GetOk(); !ok || vb == nil || va != *vb {
|
||||
return nil, false
|
||||
}
|
||||
case "ExitNodeDNSResolvers":
|
||||
@@ -778,21 +788,26 @@ func peerChangeDiff(was tailcfg.NodeView, n *tailcfg.Node) (_ *tailcfg.PeerChang
|
||||
return ret, true
|
||||
}
|
||||
|
||||
func (ms *mapSession) sortedPeers() []tailcfg.NodeView {
|
||||
ret := slicesx.MapValues(ms.peers)
|
||||
slices.SortFunc(ret, func(a, b tailcfg.NodeView) int {
|
||||
return cmp.Compare(a.ID(), b.ID())
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
// netmap returns a fully populated NetworkMap from the last state seen from
|
||||
// a call to updateStateFromResponse, filling in omitted
|
||||
// information from prior MapResponse values.
|
||||
func (ms *mapSession) netmap() *netmap.NetworkMap {
|
||||
peerViews := make([]tailcfg.NodeView, len(ms.sortedPeers))
|
||||
for i, vp := range ms.sortedPeers {
|
||||
peerViews[i] = *vp
|
||||
}
|
||||
peerViews := ms.sortedPeers()
|
||||
|
||||
nm := &netmap.NetworkMap{
|
||||
NodeKey: ms.publicNodeKey,
|
||||
PrivateKey: ms.privateNodeKey,
|
||||
MachineKey: ms.machinePubKey,
|
||||
Peers: peerViews,
|
||||
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
|
||||
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfileView),
|
||||
Domain: ms.lastDomain,
|
||||
DomainAuditLogID: ms.lastDomainAuditLogID,
|
||||
DNS: *ms.lastDNSConfig,
|
||||
@@ -803,7 +818,6 @@ func (ms *mapSession) netmap() *netmap.NetworkMap {
|
||||
DERPMap: ms.lastDERPMap,
|
||||
ControlHealth: ms.lastHealth,
|
||||
TKAEnabled: ms.lastTKAInfo != nil && !ms.lastTKAInfo.Disabled,
|
||||
MaxKeyDuration: ms.lastMaxExpiry,
|
||||
}
|
||||
|
||||
if ms.lastTKAInfo != nil && ms.lastTKAInfo.Head != "" {
|
||||
|
||||
42
vendor/tailscale.com/control/controlclient/noise.go
generated
vendored
42
vendor/tailscale.com/control/controlclient/noise.go
generated
vendored
@@ -11,13 +11,13 @@ import (
|
||||
"errors"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"tailscale.com/control/controlhttp"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/internal/noiseconn"
|
||||
"tailscale.com/net/dnscache"
|
||||
@@ -30,7 +30,6 @@ import (
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/multierr"
|
||||
"tailscale.com/util/singleflight"
|
||||
"tailscale.com/util/testenv"
|
||||
)
|
||||
|
||||
// NoiseClient provides a http.Client to connect to tailcontrol over
|
||||
@@ -107,35 +106,45 @@ type NoiseOpts struct {
|
||||
DialPlan func() *tailcfg.ControlDialPlan
|
||||
}
|
||||
|
||||
// controlIsPlaintext is whether we should assume that the controlplane is only accessible
|
||||
// over plaintext HTTP (as the first hop, before the ts2021 encryption begins).
|
||||
// This is used by some tests which don't have a real TLS certificate.
|
||||
var controlIsPlaintext = envknob.RegisterBool("TS_CONTROL_IS_PLAINTEXT_HTTP")
|
||||
|
||||
// NewNoiseClient returns a new noiseClient for the provided server and machine key.
|
||||
// serverURL is of the form https://<host>:<port> (no trailing slash).
|
||||
//
|
||||
// netMon may be nil, if non-nil it's used to do faster interface lookups.
|
||||
// dialPlan may be nil
|
||||
func NewNoiseClient(opts NoiseOpts) (*NoiseClient, error) {
|
||||
logf := opts.Logf
|
||||
u, err := url.Parse(opts.ServerURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if u.Scheme != "http" && u.Scheme != "https" {
|
||||
return nil, errors.New("invalid ServerURL scheme, must be http or https")
|
||||
}
|
||||
|
||||
var httpPort string
|
||||
var httpsPort string
|
||||
addr, _ := netip.ParseAddr(u.Hostname())
|
||||
isPrivateHost := addr.IsPrivate() || addr.IsLoopback() || u.Hostname() == "localhost"
|
||||
if port := u.Port(); port != "" {
|
||||
// If there is an explicit port specified, trust the scheme and hope for the best
|
||||
if u.Scheme == "http" {
|
||||
// If there is an explicit port specified, entirely rely on the scheme,
|
||||
// unless it's http with a private host in which case we never try using HTTPS.
|
||||
if u.Scheme == "https" {
|
||||
httpPort = ""
|
||||
httpsPort = port
|
||||
} else if u.Scheme == "http" {
|
||||
httpPort = port
|
||||
httpsPort = "443"
|
||||
if (testenv.InTest() || controlIsPlaintext()) && (u.Hostname() == "127.0.0.1" || u.Hostname() == "localhost") {
|
||||
if isPrivateHost {
|
||||
logf("setting empty HTTPS port with http scheme and private host %s", u.Hostname())
|
||||
httpsPort = ""
|
||||
}
|
||||
} else {
|
||||
httpPort = "80"
|
||||
httpsPort = port
|
||||
}
|
||||
} else if u.Scheme == "http" && isPrivateHost {
|
||||
// Whenever the scheme is http and the hostname is an IP address, do not set the HTTPS port,
|
||||
// as there cannot be a TLS certificate issued for an IP, unless it's a public IP.
|
||||
httpPort = "80"
|
||||
httpsPort = ""
|
||||
} else {
|
||||
// Otherwise, use the standard ports
|
||||
httpPort = "80"
|
||||
@@ -387,17 +396,20 @@ func (nc *NoiseClient) dial(ctx context.Context) (*noiseconn.Conn, error) {
|
||||
// post does a POST to the control server at the given path, JSON-encoding body.
|
||||
// The provided nodeKey is an optional load balancing hint.
|
||||
func (nc *NoiseClient) post(ctx context.Context, path string, nodeKey key.NodePublic, body any) (*http.Response, error) {
|
||||
return nc.doWithBody(ctx, "POST", path, nodeKey, body)
|
||||
}
|
||||
|
||||
func (nc *NoiseClient) doWithBody(ctx context.Context, method, path string, nodeKey key.NodePublic, body any) (*http.Response, error) {
|
||||
jbody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", "https://"+nc.host+path, bytes.NewReader(jbody))
|
||||
req, err := http.NewRequestWithContext(ctx, method, "https://"+nc.host+path, bytes.NewReader(jbody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addLBHeader(req, nodeKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
conn, err := nc.getConn(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
13
vendor/tailscale.com/control/controlclient/sign_supported.go
generated
vendored
13
vendor/tailscale.com/control/controlclient/sign_supported.go
generated
vendored
@@ -13,7 +13,6 @@ import (
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/certstore"
|
||||
@@ -22,11 +21,6 @@ import (
|
||||
"tailscale.com/util/syspolicy"
|
||||
)
|
||||
|
||||
var getMachineCertificateSubjectOnce struct {
|
||||
sync.Once
|
||||
v string // Subject of machine certificate to search for
|
||||
}
|
||||
|
||||
// getMachineCertificateSubject returns the exact name of a Subject that needs
|
||||
// to be present in an identity's certificate chain to sign a RegisterRequest,
|
||||
// formatted as per pkix.Name.String(). The Subject may be that of the identity
|
||||
@@ -37,11 +31,8 @@ var getMachineCertificateSubjectOnce struct {
|
||||
//
|
||||
// Example: "CN=Tailscale Inc Test Root CA,OU=Tailscale Inc Test Certificate Authority,O=Tailscale Inc,ST=ON,C=CA"
|
||||
func getMachineCertificateSubject() string {
|
||||
getMachineCertificateSubjectOnce.Do(func() {
|
||||
getMachineCertificateSubjectOnce.v, _ = syspolicy.GetString(syspolicy.MachineCertificateSubject, "")
|
||||
})
|
||||
|
||||
return getMachineCertificateSubjectOnce.v
|
||||
machineCertSubject, _ := syspolicy.GetString(syspolicy.MachineCertificateSubject, "")
|
||||
return machineCertSubject
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
16
vendor/tailscale.com/control/controlhttp/client.go
generated
vendored
16
vendor/tailscale.com/control/controlhttp/client.go
generated
vendored
@@ -38,6 +38,7 @@ import (
|
||||
"time"
|
||||
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/control/controlhttp/controlhttpcommon"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/dnscache"
|
||||
@@ -95,6 +96,9 @@ func (a *Dialer) httpsFallbackDelay() time.Duration {
|
||||
var _ = envknob.RegisterBool("TS_USE_CONTROL_DIAL_PLAN") // to record at init time whether it's in use
|
||||
|
||||
func (a *Dialer) dial(ctx context.Context) (*ClientConn, error) {
|
||||
|
||||
a.logPort80Failure.Store(true)
|
||||
|
||||
// If we don't have a dial plan, just fall back to dialing the single
|
||||
// host we know about.
|
||||
useDialPlan := envknob.BoolDefaultTrue("TS_USE_CONTROL_DIAL_PLAN")
|
||||
@@ -277,7 +281,9 @@ func (d *Dialer) forceNoise443() bool {
|
||||
// This heuristic works around networks where port 80 is MITMed and
|
||||
// appears to work for a bit post-Upgrade but then gets closed,
|
||||
// such as seen in https://github.com/tailscale/tailscale/issues/13597.
|
||||
d.logf("controlhttp: forcing port 443 dial due to recent noise dial")
|
||||
if d.logPort80Failure.CompareAndSwap(true, false) {
|
||||
d.logf("controlhttp: forcing port 443 dial due to recent noise dial")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -571,9 +577,9 @@ func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, optAddr netip.Ad
|
||||
Method: "POST",
|
||||
URL: u,
|
||||
Header: http.Header{
|
||||
"Upgrade": []string{upgradeHeaderValue},
|
||||
"Connection": []string{"upgrade"},
|
||||
handshakeHeaderName: []string{base64.StdEncoding.EncodeToString(init)},
|
||||
"Upgrade": []string{controlhttpcommon.UpgradeHeaderValue},
|
||||
"Connection": []string{"upgrade"},
|
||||
controlhttpcommon.HandshakeHeaderName: []string{base64.StdEncoding.EncodeToString(init)},
|
||||
},
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
@@ -597,7 +603,7 @@ func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, optAddr netip.Ad
|
||||
return nil, fmt.Errorf("httptrace didn't provide a connection")
|
||||
}
|
||||
|
||||
if next := resp.Header.Get("Upgrade"); next != upgradeHeaderValue {
|
||||
if next := resp.Header.Get("Upgrade"); next != controlhttpcommon.UpgradeHeaderValue {
|
||||
resp.Body.Close()
|
||||
return nil, fmt.Errorf("server switched to unexpected protocol %q", next)
|
||||
}
|
||||
|
||||
5
vendor/tailscale.com/control/controlhttp/client_js.go
generated
vendored
5
vendor/tailscale.com/control/controlhttp/client_js.go
generated
vendored
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/coder/websocket"
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/control/controlhttp/controlhttpcommon"
|
||||
"tailscale.com/net/wsconn"
|
||||
)
|
||||
|
||||
@@ -42,11 +43,11 @@ func (d *Dialer) Dial(ctx context.Context) (*ClientConn, error) {
|
||||
// Can't set HTTP headers on the websocket request, so we have to to send
|
||||
// the handshake via an HTTP header.
|
||||
RawQuery: url.Values{
|
||||
handshakeHeaderName: []string{base64.StdEncoding.EncodeToString(init)},
|
||||
controlhttpcommon.HandshakeHeaderName: []string{base64.StdEncoding.EncodeToString(init)},
|
||||
}.Encode(),
|
||||
}
|
||||
wsConn, _, err := websocket.Dial(ctx, wsURL.String(), &websocket.DialOptions{
|
||||
Subprotocols: []string{upgradeHeaderValue},
|
||||
Subprotocols: []string{controlhttpcommon.UpgradeHeaderValue},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
17
vendor/tailscale.com/control/controlhttp/constants.go
generated
vendored
17
vendor/tailscale.com/control/controlhttp/constants.go
generated
vendored
@@ -6,6 +6,7 @@ package controlhttp
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/health"
|
||||
@@ -18,15 +19,6 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// upgradeHeader is the value of the Upgrade HTTP header used to
|
||||
// indicate the Tailscale control protocol.
|
||||
upgradeHeaderValue = "tailscale-control-protocol"
|
||||
|
||||
// handshakeHeaderName is the HTTP request header that can
|
||||
// optionally contain base64-encoded initial handshake
|
||||
// payload, to save an RTT.
|
||||
handshakeHeaderName = "X-Tailscale-Handshake"
|
||||
|
||||
// serverUpgradePath is where the server-side HTTP handler to
|
||||
// to do the protocol switch is located.
|
||||
serverUpgradePath = "/ts2021"
|
||||
@@ -85,6 +77,8 @@ type Dialer struct {
|
||||
// dropped.
|
||||
Logf logger.Logf
|
||||
|
||||
// NetMon is the [netmon.Monitor] to use for this Dialer. It must be
|
||||
// non-nil.
|
||||
NetMon *netmon.Monitor
|
||||
|
||||
// HealthTracker, if non-nil, is the health tracker to use.
|
||||
@@ -97,6 +91,11 @@ type Dialer struct {
|
||||
|
||||
proxyFunc func(*http.Request) (*url.URL, error) // or nil
|
||||
|
||||
// logPort80Failure is whether we should log about port 80 interceptions
|
||||
// and forcing a port 443 dial. We do this only once per "dial" method
|
||||
// which can result in many concurrent racing dialHost calls.
|
||||
logPort80Failure atomic.Bool
|
||||
|
||||
// For tests only
|
||||
drainFinished chan struct{}
|
||||
omitCertErrorLogging bool
|
||||
|
||||
15
vendor/tailscale.com/control/controlhttp/controlhttpcommon/controlhttpcommon.go
generated
vendored
Normal file
15
vendor/tailscale.com/control/controlhttp/controlhttpcommon/controlhttpcommon.go
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package controlhttpcommon contains common constants for used
|
||||
// by the controlhttp client and controlhttpserver packages.
|
||||
package controlhttpcommon
|
||||
|
||||
// UpgradeHeader is the value of the Upgrade HTTP header used to
|
||||
// indicate the Tailscale control protocol.
|
||||
const UpgradeHeaderValue = "tailscale-control-protocol"
|
||||
|
||||
// handshakeHeaderName is the HTTP request header that can
|
||||
// optionally contain base64-encoded initial handshake
|
||||
// payload, to save an RTT.
|
||||
const HandshakeHeaderName = "X-Tailscale-Handshake"
|
||||
217
vendor/tailscale.com/control/controlhttp/server.go
generated
vendored
217
vendor/tailscale.com/control/controlhttp/server.go
generated
vendored
@@ -1,217 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ios
|
||||
|
||||
package controlhttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/net/wsconn"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
// AcceptHTTP upgrades the HTTP request given by w and r into a Tailscale
|
||||
// control protocol base transport connection.
|
||||
//
|
||||
// AcceptHTTP always writes an HTTP response to w. The caller must not attempt
|
||||
// their own response after calling AcceptHTTP.
|
||||
//
|
||||
// earlyWrite optionally specifies a func to write to the noise connection
|
||||
// (encrypted). It receives the negotiated version and a writer to write to, if
|
||||
// desired.
|
||||
func AcceptHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request, private key.MachinePrivate, earlyWrite func(protocolVersion int, w io.Writer) error) (*controlbase.Conn, error) {
|
||||
return acceptHTTP(ctx, w, r, private, earlyWrite)
|
||||
}
|
||||
|
||||
func acceptHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request, private key.MachinePrivate, earlyWrite func(protocolVersion int, w io.Writer) error) (_ *controlbase.Conn, retErr error) {
|
||||
next := strings.ToLower(r.Header.Get("Upgrade"))
|
||||
if next == "" {
|
||||
http.Error(w, "missing next protocol", http.StatusBadRequest)
|
||||
return nil, errors.New("no next protocol in HTTP request")
|
||||
}
|
||||
if next == "websocket" {
|
||||
return acceptWebsocket(ctx, w, r, private)
|
||||
}
|
||||
if next != upgradeHeaderValue {
|
||||
http.Error(w, "unknown next protocol", http.StatusBadRequest)
|
||||
return nil, fmt.Errorf("client requested unhandled next protocol %q", next)
|
||||
}
|
||||
|
||||
initB64 := r.Header.Get(handshakeHeaderName)
|
||||
if initB64 == "" {
|
||||
http.Error(w, "missing Tailscale handshake header", http.StatusBadRequest)
|
||||
return nil, errors.New("no tailscale handshake header in HTTP request")
|
||||
}
|
||||
init, err := base64.StdEncoding.DecodeString(initB64)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid tailscale handshake header", http.StatusBadRequest)
|
||||
return nil, fmt.Errorf("decoding base64 handshake header: %v", err)
|
||||
}
|
||||
|
||||
hijacker, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
http.Error(w, "make request over HTTP/1", http.StatusBadRequest)
|
||||
return nil, errors.New("can't hijack client connection")
|
||||
}
|
||||
|
||||
w.Header().Set("Upgrade", upgradeHeaderValue)
|
||||
w.Header().Set("Connection", "upgrade")
|
||||
w.WriteHeader(http.StatusSwitchingProtocols)
|
||||
|
||||
conn, brw, err := hijacker.Hijack()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("hijacking client connection: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if retErr != nil {
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
if err := brw.Flush(); err != nil {
|
||||
return nil, fmt.Errorf("flushing hijacked HTTP buffer: %w", err)
|
||||
}
|
||||
conn = netutil.NewDrainBufConn(conn, brw.Reader)
|
||||
|
||||
cwc := newWriteCorkingConn(conn)
|
||||
|
||||
nc, err := controlbase.Server(ctx, cwc, private, init)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("noise handshake failed: %w", err)
|
||||
}
|
||||
|
||||
if earlyWrite != nil {
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
if err := conn.SetDeadline(deadline); err != nil {
|
||||
return nil, fmt.Errorf("setting conn deadline: %w", err)
|
||||
}
|
||||
defer conn.SetDeadline(time.Time{})
|
||||
}
|
||||
if err := earlyWrite(nc.ProtocolVersion(), nc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := cwc.uncork(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nc, nil
|
||||
}
|
||||
|
||||
// acceptWebsocket upgrades a WebSocket connection (from a client that cannot
|
||||
// speak HTTP) to a Tailscale control protocol base transport connection.
|
||||
func acceptWebsocket(ctx context.Context, w http.ResponseWriter, r *http.Request, private key.MachinePrivate) (*controlbase.Conn, error) {
|
||||
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
|
||||
Subprotocols: []string{upgradeHeaderValue},
|
||||
OriginPatterns: []string{"*"},
|
||||
// Disable compression because we transmit Noise messages that are not
|
||||
// compressible.
|
||||
// Additionally, Safari has a broken implementation of compression
|
||||
// (see https://github.com/nhooyr/websocket/issues/218) that makes
|
||||
// enabling it actively harmful.
|
||||
CompressionMode: websocket.CompressionDisabled,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not accept WebSocket connection %v", err)
|
||||
}
|
||||
if c.Subprotocol() != upgradeHeaderValue {
|
||||
c.Close(websocket.StatusPolicyViolation, "client must speak the control subprotocol")
|
||||
return nil, fmt.Errorf("Unexpected subprotocol %q", c.Subprotocol())
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
c.Close(websocket.StatusPolicyViolation, "Could not parse parameters")
|
||||
return nil, fmt.Errorf("parse query parameters: %v", err)
|
||||
}
|
||||
initB64 := r.Form.Get(handshakeHeaderName)
|
||||
if initB64 == "" {
|
||||
c.Close(websocket.StatusPolicyViolation, "missing Tailscale handshake parameter")
|
||||
return nil, errors.New("no tailscale handshake parameter in HTTP request")
|
||||
}
|
||||
init, err := base64.StdEncoding.DecodeString(initB64)
|
||||
if err != nil {
|
||||
c.Close(websocket.StatusPolicyViolation, "invalid tailscale handshake parameter")
|
||||
return nil, fmt.Errorf("decoding base64 handshake parameter: %v", err)
|
||||
}
|
||||
|
||||
conn := wsconn.NetConn(ctx, c, websocket.MessageBinary, r.RemoteAddr)
|
||||
nc, err := controlbase.Server(ctx, conn, private, init)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("noise handshake failed: %w", err)
|
||||
}
|
||||
|
||||
return nc, nil
|
||||
}
|
||||
|
||||
// corkConn is a net.Conn wrapper that initially buffers all writes until uncork
|
||||
// is called. If the conn is corked and a Read occurs, the Read will flush any
|
||||
// buffered (corked) write.
|
||||
//
|
||||
// Until uncorked, Read/Write/uncork may be not called concurrently.
|
||||
//
|
||||
// Deadlines still work, but a corked write ignores deadlines until a Read or
|
||||
// uncork goes to do that Write.
|
||||
//
|
||||
// Use newWriteCorkingConn to create one.
|
||||
type corkConn struct {
|
||||
net.Conn
|
||||
corked bool
|
||||
buf []byte // corked data
|
||||
}
|
||||
|
||||
func newWriteCorkingConn(c net.Conn) *corkConn {
|
||||
return &corkConn{Conn: c, corked: true}
|
||||
}
|
||||
|
||||
func (c *corkConn) Write(b []byte) (int, error) {
|
||||
if c.corked {
|
||||
c.buf = append(c.buf, b...)
|
||||
return len(b), nil
|
||||
}
|
||||
return c.Conn.Write(b)
|
||||
}
|
||||
|
||||
func (c *corkConn) Read(b []byte) (int, error) {
|
||||
if c.corked {
|
||||
if err := c.flush(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return c.Conn.Read(b)
|
||||
}
|
||||
|
||||
// uncork flushes any buffered data and uncorks the connection so future Writes
|
||||
// don't buffer. It may not be called concurrently with reads or writes and
|
||||
// may only be called once.
|
||||
func (c *corkConn) uncork() error {
|
||||
if !c.corked {
|
||||
panic("usage error; uncork called twice") // worth panicking to catch misuse
|
||||
}
|
||||
err := c.flush()
|
||||
c.corked = false
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *corkConn) flush() error {
|
||||
if len(c.buf) == 0 {
|
||||
return nil
|
||||
}
|
||||
_, err := c.Conn.Write(c.buf)
|
||||
c.buf = nil
|
||||
return err
|
||||
}
|
||||
43
vendor/tailscale.com/control/controlknobs/controlknobs.go
generated
vendored
43
vendor/tailscale.com/control/controlknobs/controlknobs.go
generated
vendored
@@ -6,6 +6,8 @@
|
||||
package controlknobs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync/atomic"
|
||||
|
||||
"tailscale.com/syncs"
|
||||
@@ -103,6 +105,11 @@ type Knobs struct {
|
||||
// DisableCaptivePortalDetection is whether the node should not perform captive portal detection
|
||||
// automatically when the network state changes.
|
||||
DisableCaptivePortalDetection atomic.Bool
|
||||
|
||||
// DisableSkipStatusQueue is whether the node should disable skipping
|
||||
// of queued netmap.NetworkMap between the controlclient and LocalBackend.
|
||||
// See tailscale/tailscale#14768.
|
||||
DisableSkipStatusQueue atomic.Bool
|
||||
}
|
||||
|
||||
// UpdateFromNodeAttributes updates k (if non-nil) based on the provided self
|
||||
@@ -132,6 +139,7 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) {
|
||||
disableLocalDNSOverrideViaNRPT = has(tailcfg.NodeAttrDisableLocalDNSOverrideViaNRPT)
|
||||
disableCryptorouting = has(tailcfg.NodeAttrDisableMagicSockCryptoRouting)
|
||||
disableCaptivePortalDetection = has(tailcfg.NodeAttrDisableCaptivePortalDetection)
|
||||
disableSkipStatusQueue = has(tailcfg.NodeAttrDisableSkipStatusQueue)
|
||||
)
|
||||
|
||||
if has(tailcfg.NodeAttrOneCGNATEnable) {
|
||||
@@ -159,6 +167,7 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) {
|
||||
k.DisableLocalDNSOverrideViaNRPT.Store(disableLocalDNSOverrideViaNRPT)
|
||||
k.DisableCryptorouting.Store(disableCryptorouting)
|
||||
k.DisableCaptivePortalDetection.Store(disableCaptivePortalDetection)
|
||||
k.DisableSkipStatusQueue.Store(disableSkipStatusQueue)
|
||||
}
|
||||
|
||||
// AsDebugJSON returns k as something that can be marshalled with json.Marshal
|
||||
@@ -167,25 +176,19 @@ func (k *Knobs) AsDebugJSON() map[string]any {
|
||||
if k == nil {
|
||||
return nil
|
||||
}
|
||||
return map[string]any{
|
||||
"DisableUPnP": k.DisableUPnP.Load(),
|
||||
"KeepFullWGConfig": k.KeepFullWGConfig.Load(),
|
||||
"RandomizeClientPort": k.RandomizeClientPort.Load(),
|
||||
"OneCGNAT": k.OneCGNAT.Load(),
|
||||
"ForceBackgroundSTUN": k.ForceBackgroundSTUN.Load(),
|
||||
"DisableDeltaUpdates": k.DisableDeltaUpdates.Load(),
|
||||
"PeerMTUEnable": k.PeerMTUEnable.Load(),
|
||||
"DisableDNSForwarderTCPRetries": k.DisableDNSForwarderTCPRetries.Load(),
|
||||
"SilentDisco": k.SilentDisco.Load(),
|
||||
"LinuxForceIPTables": k.LinuxForceIPTables.Load(),
|
||||
"LinuxForceNfTables": k.LinuxForceNfTables.Load(),
|
||||
"SeamlessKeyRenewal": k.SeamlessKeyRenewal.Load(),
|
||||
"ProbeUDPLifetime": k.ProbeUDPLifetime.Load(),
|
||||
"AppCStoreRoutes": k.AppCStoreRoutes.Load(),
|
||||
"UserDialUseRoutes": k.UserDialUseRoutes.Load(),
|
||||
"DisableSplitDNSWhenNoCustomResolvers": k.DisableSplitDNSWhenNoCustomResolvers.Load(),
|
||||
"DisableLocalDNSOverrideViaNRPT": k.DisableLocalDNSOverrideViaNRPT.Load(),
|
||||
"DisableCryptorouting": k.DisableCryptorouting.Load(),
|
||||
"DisableCaptivePortalDetection": k.DisableCaptivePortalDetection.Load(),
|
||||
ret := map[string]any{}
|
||||
rt := reflect.TypeFor[Knobs]()
|
||||
rv := reflect.ValueOf(k).Elem() // of *k
|
||||
for i := 0; i < rt.NumField(); i++ {
|
||||
name := rt.Field(i).Name
|
||||
switch v := rv.Field(i).Addr().Interface().(type) {
|
||||
case *atomic.Bool:
|
||||
ret[name] = v.Load()
|
||||
case *syncs.AtomicValue[opt.Bool]:
|
||||
ret[name] = v.Load()
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown field type %T for %v", v, name))
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user