|
|
|
|
@@ -52,6 +52,7 @@ import (
|
|
|
|
|
"tailscale.com/types/persist"
|
|
|
|
|
"tailscale.com/types/ptr"
|
|
|
|
|
"tailscale.com/types/tkatype"
|
|
|
|
|
"tailscale.com/util/cache"
|
|
|
|
|
"tailscale.com/util/clientmetric"
|
|
|
|
|
"tailscale.com/util/multierr"
|
|
|
|
|
"tailscale.com/util/singleflight"
|
|
|
|
|
@@ -82,6 +83,9 @@ type Direct struct {
|
|
|
|
|
|
|
|
|
|
dialPlan ControlDialPlanner // can be nil
|
|
|
|
|
|
|
|
|
|
controlKeyMu sync.Mutex // guards controlKeyCache
|
|
|
|
|
controlKeyCache cache.Cache[string, *tailcfg.OverTLSPublicKeyResponse]
|
|
|
|
|
|
|
|
|
|
mu sync.Mutex // mutex guards the following fields
|
|
|
|
|
serverKey key.MachinePublic // original ("legacy") nacl crypto_box-based public key
|
|
|
|
|
serverNoiseKey key.MachinePublic
|
|
|
|
|
@@ -151,6 +155,10 @@ type Options struct {
|
|
|
|
|
// If we receive a new DialPlan from the server, this value will be
|
|
|
|
|
// updated.
|
|
|
|
|
DialPlan ControlDialPlanner
|
|
|
|
|
|
|
|
|
|
// ControlKeyCache caches Noise keys returned from control; if nil, no
|
|
|
|
|
// cache will be used.
|
|
|
|
|
ControlKeyCache cache.Cache[string, *tailcfg.OverTLSPublicKeyResponse]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ControlDialPlanner is the interface optionally supplied when creating a
|
|
|
|
|
@@ -264,6 +272,11 @@ func NewDirect(opts Options) (*Direct, error) {
|
|
|
|
|
httpc = &http.Client{Transport: tr}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ckcache := opts.ControlKeyCache
|
|
|
|
|
if ckcache == nil {
|
|
|
|
|
ckcache = cache.None[string, *tailcfg.OverTLSPublicKeyResponse]{}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
c := &Direct{
|
|
|
|
|
httpc: httpc,
|
|
|
|
|
controlKnobs: opts.ControlKnobs,
|
|
|
|
|
@@ -286,6 +299,7 @@ func NewDirect(opts Options) (*Direct, error) {
|
|
|
|
|
dialer: opts.Dialer,
|
|
|
|
|
dnsCache: dnsCache,
|
|
|
|
|
dialPlan: opts.DialPlan,
|
|
|
|
|
controlKeyCache: ckcache,
|
|
|
|
|
}
|
|
|
|
|
if opts.Hostinfo == nil {
|
|
|
|
|
c.SetHostinfo(hostinfo.New())
|
|
|
|
|
@@ -492,11 +506,12 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
|
|
|
|
|
|
|
|
|
c.logf("doLogin(regen=%v, hasUrl=%v)", regen, opt.URL != "")
|
|
|
|
|
if serverKey.IsZero() {
|
|
|
|
|
keys, err := loadServerPubKeys(ctx, c.httpc, c.serverURL)
|
|
|
|
|
keys, cached, err := c.loadServerPubKeys(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.logf("error fetching control server key (serverURL=%q): %v", c.serverURL, err)
|
|
|
|
|
return regen, opt.URL, nil, err
|
|
|
|
|
}
|
|
|
|
|
c.logf("control server key from %s: ts2021=%s, legacy=%v", c.serverURL, keys.PublicKey.ShortString(), keys.LegacyPublicKey.ShortString())
|
|
|
|
|
c.logf("control server key from %s: cached=%v ts2021=%s, legacy=%v", c.serverURL, cached, keys.PublicKey.ShortString(), keys.LegacyPublicKey.ShortString())
|
|
|
|
|
|
|
|
|
|
c.mu.Lock()
|
|
|
|
|
c.serverKey = keys.LegacyPublicKey
|
|
|
|
|
@@ -1260,39 +1275,54 @@ func encode(v any, serverKey, serverNoiseKey key.MachinePublic, mkey key.Machine
|
|
|
|
|
return mkey.SealTo(serverKey, b), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func loadServerPubKeys(ctx context.Context, httpc *http.Client, serverURL string) (*tailcfg.OverTLSPublicKeyResponse, error) {
|
|
|
|
|
keyURL := fmt.Sprintf("%v/key?v=%d", serverURL, tailcfg.CurrentCapabilityVersion)
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", keyURL, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("create control key request: %v", err)
|
|
|
|
|
}
|
|
|
|
|
res, err := httpc.Do(req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("fetch control key: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
b, err := io.ReadAll(io.LimitReader(res.Body, 64<<10))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("fetch control key response: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if res.StatusCode != 200 {
|
|
|
|
|
return nil, fmt.Errorf("fetch control key: %d", res.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
var out tailcfg.OverTLSPublicKeyResponse
|
|
|
|
|
jsonErr := json.Unmarshal(b, &out)
|
|
|
|
|
if jsonErr == nil {
|
|
|
|
|
return &out, nil
|
|
|
|
|
}
|
|
|
|
|
func (c *Direct) loadServerPubKeys(ctx context.Context) (ret *tailcfg.OverTLSPublicKeyResponse, cached bool, err error) {
|
|
|
|
|
c.controlKeyMu.Lock()
|
|
|
|
|
defer c.controlKeyMu.Unlock()
|
|
|
|
|
cached = true
|
|
|
|
|
|
|
|
|
|
// Some old control servers might not be updated to send the new format.
|
|
|
|
|
// Accept the old pre-JSON format too.
|
|
|
|
|
out = tailcfg.OverTLSPublicKeyResponse{}
|
|
|
|
|
k, err := key.ParseMachinePublicUntyped(mem.B(b))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, multierr.New(jsonErr, err)
|
|
|
|
|
}
|
|
|
|
|
out.LegacyPublicKey = k
|
|
|
|
|
return &out, nil
|
|
|
|
|
keyURL := fmt.Sprintf("%v/key?v=%d", c.serverURL, tailcfg.CurrentCapabilityVersion)
|
|
|
|
|
ret, err = c.controlKeyCache.Get(keyURL, func() (*tailcfg.OverTLSPublicKeyResponse, time.Time, error) {
|
|
|
|
|
cached = false
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", keyURL, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, time.Time{}, fmt.Errorf("create control key request: %v", err)
|
|
|
|
|
}
|
|
|
|
|
res, err := c.httpc.Do(req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, time.Time{}, fmt.Errorf("fetch control key: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
b, err := io.ReadAll(io.LimitReader(res.Body, 64<<10))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, time.Time{}, fmt.Errorf("fetch control key response: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if res.StatusCode != 200 {
|
|
|
|
|
return nil, time.Time{}, fmt.Errorf("fetch control key: %d", res.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cache keys for one minute at most, after which we'll
|
|
|
|
|
// re-fetch from the control server. However, if this cache has
|
|
|
|
|
// ServeExpired enabled, then we'll serve the expired key if
|
|
|
|
|
// the request to fetch a key fails.
|
|
|
|
|
expiry := c.clock.Now().Add(1 * time.Minute)
|
|
|
|
|
|
|
|
|
|
var out tailcfg.OverTLSPublicKeyResponse
|
|
|
|
|
jsonErr := json.Unmarshal(b, &out)
|
|
|
|
|
if jsonErr == nil {
|
|
|
|
|
return &out, expiry, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Some old control servers might not be updated to send the new format.
|
|
|
|
|
// Accept the old pre-JSON format too.
|
|
|
|
|
out = tailcfg.OverTLSPublicKeyResponse{}
|
|
|
|
|
k, err := key.ParseMachinePublicUntyped(mem.B(b))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, time.Time{}, multierr.New(jsonErr, err)
|
|
|
|
|
}
|
|
|
|
|
out.LegacyPublicKey = k
|
|
|
|
|
return &out, expiry, nil
|
|
|
|
|
})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DevKnob contains temporary internal-only debug knobs.
|
|
|
|
|
|