Update dependencies
This commit is contained in:
160
vendor/tailscale.com/ipn/ipnlocal/bus.go
generated
vendored
Normal file
160
vendor/tailscale.com/ipn/ipnlocal/bus.go
generated
vendored
Normal file
@@ -0,0 +1,160 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tstime"
|
||||
)
|
||||
|
||||
type rateLimitingBusSender struct {
|
||||
fn func(*ipn.Notify) (keepGoing bool)
|
||||
lastFlush time.Time // last call to fn, or zero value if none
|
||||
interval time.Duration // 0 to flush immediately; non-zero to rate limit sends
|
||||
clock tstime.DefaultClock // non-nil for testing
|
||||
didSendTestHook func() // non-nil for testing
|
||||
|
||||
// pending, if non-nil, is the pending notification that we
|
||||
// haven't sent yet. We own this memory to mutate.
|
||||
pending *ipn.Notify
|
||||
|
||||
// flushTimer is non-nil if the timer is armed.
|
||||
flushTimer tstime.TimerController // effectively a *time.Timer
|
||||
flushTimerC <-chan time.Time // ... said ~Timer's C chan
|
||||
}
|
||||
|
||||
func (s *rateLimitingBusSender) close() {
|
||||
if s.flushTimer != nil {
|
||||
s.flushTimer.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *rateLimitingBusSender) flushChan() <-chan time.Time {
|
||||
return s.flushTimerC
|
||||
}
|
||||
|
||||
func (s *rateLimitingBusSender) flush() (keepGoing bool) {
|
||||
if n := s.pending; n != nil {
|
||||
s.pending = nil
|
||||
return s.flushNotify(n)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *rateLimitingBusSender) flushNotify(n *ipn.Notify) (keepGoing bool) {
|
||||
s.lastFlush = s.clock.Now()
|
||||
return s.fn(n)
|
||||
}
|
||||
|
||||
// send conditionally sends n to the underlying fn, possibly rate
|
||||
// limiting it, depending on whether s.interval is set, and whether
|
||||
// n is a notable notification that the client (typically a GUI) would
|
||||
// want to act on (render) immediately.
|
||||
//
|
||||
// It returns whether the caller should keep looping.
|
||||
//
|
||||
// The passed-in memory 'n' is owned by the caller and should
|
||||
// not be mutated.
|
||||
func (s *rateLimitingBusSender) send(n *ipn.Notify) (keepGoing bool) {
|
||||
if s.interval <= 0 {
|
||||
// No rate limiting case.
|
||||
return s.fn(n)
|
||||
}
|
||||
if isNotableNotify(n) {
|
||||
// Notable notifications are always sent immediately.
|
||||
// But first send any boring one that was pending.
|
||||
// TODO(bradfitz): there might be a boring one pending
|
||||
// with a NetMap or Engine field that is redundant
|
||||
// with the new one (n) with NetMap or Engine populated.
|
||||
// We should clear the pending one's NetMap/Engine in
|
||||
// that case. Or really, merge the two, but mergeBoringNotifies
|
||||
// only handles the case of both sides being boring.
|
||||
// So for now, flush both.
|
||||
if !s.flush() {
|
||||
return false
|
||||
}
|
||||
return s.flushNotify(n)
|
||||
}
|
||||
s.pending = mergeBoringNotifies(s.pending, n)
|
||||
d := s.clock.Now().Sub(s.lastFlush)
|
||||
if d > s.interval {
|
||||
return s.flush()
|
||||
}
|
||||
nextFlushIn := s.interval - d
|
||||
if s.flushTimer == nil {
|
||||
s.flushTimer, s.flushTimerC = s.clock.NewTimer(nextFlushIn)
|
||||
} else {
|
||||
s.flushTimer.Reset(nextFlushIn)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *rateLimitingBusSender) Run(ctx context.Context, ch <-chan *ipn.Notify) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case n, ok := <-ch:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !s.send(n) {
|
||||
return
|
||||
}
|
||||
if f := s.didSendTestHook; f != nil {
|
||||
f()
|
||||
}
|
||||
case <-s.flushChan():
|
||||
if !s.flush() {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mergeBoringNotify merges new notify 'src' into possibly-nil 'dst',
|
||||
// either mutating 'dst' or allocating a new one if 'dst' is nil,
|
||||
// returning the merged result.
|
||||
//
|
||||
// dst and src must both be "boring" (i.e. not notable per isNotifiableNotify).
|
||||
func mergeBoringNotifies(dst, src *ipn.Notify) *ipn.Notify {
|
||||
if dst == nil {
|
||||
dst = &ipn.Notify{Version: src.Version}
|
||||
}
|
||||
if src.NetMap != nil {
|
||||
dst.NetMap = src.NetMap
|
||||
}
|
||||
if src.Engine != nil {
|
||||
dst.Engine = src.Engine
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// isNotableNotify reports whether n is a "notable" notification that
|
||||
// should be sent on the IPN bus immediately (e.g. to GUIs) without
|
||||
// rate limiting it for a few seconds.
|
||||
//
|
||||
// It effectively reports whether n contains any field set that's
|
||||
// not NetMap or Engine.
|
||||
func isNotableNotify(n *ipn.Notify) bool {
|
||||
if n == nil {
|
||||
return false
|
||||
}
|
||||
return n.State != nil ||
|
||||
n.SessionID != "" ||
|
||||
n.BrowseToURL != nil ||
|
||||
n.LocalTCPPort != nil ||
|
||||
n.ClientVersion != nil ||
|
||||
n.Prefs != nil ||
|
||||
n.ErrMessage != nil ||
|
||||
n.LoginFinished != nil ||
|
||||
!n.DriveShares.IsNil() ||
|
||||
n.Health != nil ||
|
||||
len(n.IncomingFiles) > 0 ||
|
||||
len(n.OutgoingFiles) > 0 ||
|
||||
n.FilesWaiting != nil
|
||||
}
|
||||
88
vendor/tailscale.com/ipn/ipnlocal/c2n.go
generated
vendored
88
vendor/tailscale.com/ipn/ipnlocal/c2n.go
generated
vendored
@@ -10,19 +10,16 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kortschak/wol"
|
||||
"tailscale.com/clientupdate"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
@@ -66,9 +63,6 @@ var c2nHandlers = map[methodAndPath]c2nHandler{
|
||||
req("GET /update"): handleC2NUpdateGet,
|
||||
req("POST /update"): handleC2NUpdatePost,
|
||||
|
||||
// Wake-on-LAN.
|
||||
req("POST /wol"): handleC2NWoL,
|
||||
|
||||
// Device posture.
|
||||
req("GET /posture/identity"): handleC2NPostureIdentityGet,
|
||||
|
||||
@@ -77,6 +71,21 @@ var c2nHandlers = map[methodAndPath]c2nHandler{
|
||||
|
||||
// Linux netfilter.
|
||||
req("POST /netfilter-kind"): handleC2NSetNetfilterKind,
|
||||
|
||||
// VIP services.
|
||||
req("GET /vip-services"): handleC2NVIPServicesGet,
|
||||
}
|
||||
|
||||
// RegisterC2N registers a new c2n handler for the given pattern.
|
||||
//
|
||||
// A pattern is like "GET /foo" (specific to an HTTP method) or "/foo" (all
|
||||
// methods). It panics if the pattern is already registered.
|
||||
func RegisterC2N(pattern string, h func(*LocalBackend, http.ResponseWriter, *http.Request)) {
|
||||
k := req(pattern)
|
||||
if _, ok := c2nHandlers[k]; ok {
|
||||
panic(fmt.Sprintf("c2n: duplicate handler for %q", pattern))
|
||||
}
|
||||
c2nHandlers[k] = h
|
||||
}
|
||||
|
||||
type c2nHandler func(*LocalBackend, http.ResponseWriter, *http.Request)
|
||||
@@ -269,6 +278,16 @@ func handleC2NSetNetfilterKind(b *LocalBackend, w http.ResponseWriter, r *http.R
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func handleC2NVIPServicesGet(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
||||
b.logf("c2n: GET /vip-services received")
|
||||
var res tailcfg.C2NVIPServicesResponse
|
||||
res.VIPServices = b.VIPServices()
|
||||
res.ServicesHash = b.vipServiceHash(res.VIPServices)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
func handleC2NUpdateGet(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
||||
b.logf("c2n: GET /update received")
|
||||
|
||||
@@ -332,12 +351,10 @@ func handleC2NPostureIdentityGet(b *LocalBackend, w http.ResponseWriter, r *http
|
||||
}
|
||||
|
||||
if choice.ShouldEnable(b.Prefs().PostureChecking()) {
|
||||
sns, err := posture.GetSerialNumbers(b.logf)
|
||||
res.SerialNumbers, err = posture.GetSerialNumbers(b.logf)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
b.logf("c2n: GetSerialNumbers returned error: %v", err)
|
||||
}
|
||||
res.SerialNumbers = sns
|
||||
|
||||
// TODO(tailscale/corp#21371, 2024-07-10): once this has landed in a stable release
|
||||
// and looks good in client metrics, remove this parameter and always report MAC
|
||||
@@ -352,6 +369,8 @@ func handleC2NPostureIdentityGet(b *LocalBackend, w http.ResponseWriter, r *http
|
||||
res.PostureDisabled = true
|
||||
}
|
||||
|
||||
b.logf("c2n: posture identity disabled=%v reported %d serials %d hwaddrs", res.PostureDisabled, len(res.SerialNumbers), len(res.IfaceHardwareAddrs))
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
@@ -490,55 +509,6 @@ func regularFileExists(path string) bool {
|
||||
return err == nil && fi.Mode().IsRegular()
|
||||
}
|
||||
|
||||
func handleC2NWoL(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
var macs []net.HardwareAddr
|
||||
for _, macStr := range r.Form["mac"] {
|
||||
mac, err := net.ParseMAC(macStr)
|
||||
if err != nil {
|
||||
http.Error(w, "bad 'mac' param", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
macs = append(macs, mac)
|
||||
}
|
||||
var res struct {
|
||||
SentTo []string
|
||||
Errors []string
|
||||
}
|
||||
st := b.sys.NetMon.Get().InterfaceState()
|
||||
if st == nil {
|
||||
res.Errors = append(res.Errors, "no interface state")
|
||||
writeJSON(w, &res)
|
||||
return
|
||||
}
|
||||
var password []byte // TODO(bradfitz): support? does anything use WoL passwords?
|
||||
for _, mac := range macs {
|
||||
for ifName, ips := range st.InterfaceIPs {
|
||||
for _, ip := range ips {
|
||||
if ip.Addr().IsLoopback() || ip.Addr().Is6() {
|
||||
continue
|
||||
}
|
||||
local := &net.UDPAddr{
|
||||
IP: ip.Addr().AsSlice(),
|
||||
Port: 0,
|
||||
}
|
||||
remote := &net.UDPAddr{
|
||||
IP: net.IPv4bcast,
|
||||
Port: 0,
|
||||
}
|
||||
if err := wol.Wake(mac, password, local, remote); err != nil {
|
||||
res.Errors = append(res.Errors, err.Error())
|
||||
} else {
|
||||
res.SentTo = append(res.SentTo, ifName)
|
||||
}
|
||||
break // one per interface is enough
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(res.SentTo)
|
||||
writeJSON(w, &res)
|
||||
}
|
||||
|
||||
// handleC2NTLSCertStatus returns info about the last TLS certificate issued for the
|
||||
// provided domain. This can be called by the controlplane to clean up DNS TXT
|
||||
// records when they're no longer needed by LetsEncrypt.
|
||||
|
||||
136
vendor/tailscale.com/ipn/ipnlocal/cert.go
generated
vendored
136
vendor/tailscale.com/ipn/ipnlocal/cert.go
generated
vendored
@@ -32,7 +32,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/golang-x-crypto/acme"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/hostinfo"
|
||||
@@ -40,6 +39,8 @@ import (
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/ipn/store"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/net/bakedroots"
|
||||
"tailscale.com/tempfork/acme"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/testenv"
|
||||
"tailscale.com/version"
|
||||
@@ -118,6 +119,9 @@ func (b *LocalBackend) GetCertPEMWithValidity(ctx context.Context, domain string
|
||||
}
|
||||
|
||||
if pair, err := getCertPEMCached(cs, domain, now); err == nil {
|
||||
if envknob.IsCertShareReadOnlyMode() {
|
||||
return pair, nil
|
||||
}
|
||||
// If we got here, we have a valid unexpired cert.
|
||||
// Check whether we should start an async renewal.
|
||||
shouldRenew, err := b.shouldStartDomainRenewal(cs, domain, now, pair, minValidity)
|
||||
@@ -133,7 +137,7 @@ func (b *LocalBackend) GetCertPEMWithValidity(ctx context.Context, domain string
|
||||
if minValidity == 0 {
|
||||
logf("starting async renewal")
|
||||
// Start renewal in the background, return current valid cert.
|
||||
go b.getCertPEM(context.Background(), cs, logf, traceACME, domain, now, minValidity)
|
||||
b.goTracker.Go(func() { getCertPEM(context.Background(), b, cs, logf, traceACME, domain, now, minValidity) })
|
||||
return pair, nil
|
||||
}
|
||||
// If the caller requested a specific validity duration, fall through
|
||||
@@ -141,7 +145,11 @@ func (b *LocalBackend) GetCertPEMWithValidity(ctx context.Context, domain string
|
||||
logf("starting sync renewal")
|
||||
}
|
||||
|
||||
pair, err := b.getCertPEM(ctx, cs, logf, traceACME, domain, now, minValidity)
|
||||
if envknob.IsCertShareReadOnlyMode() {
|
||||
return nil, fmt.Errorf("retrieving cached TLS certificate failed and cert store is configured in read-only mode, not attempting to issue a new certificate: %w", err)
|
||||
}
|
||||
|
||||
pair, err := getCertPEM(ctx, b, cs, logf, traceACME, domain, now, minValidity)
|
||||
if err != nil {
|
||||
logf("getCertPEM: %v", err)
|
||||
return nil, err
|
||||
@@ -249,15 +257,13 @@ type certStore interface {
|
||||
// for now. If they're expired, it returns errCertExpired.
|
||||
// If they don't exist, it returns ipn.ErrStateNotExist.
|
||||
Read(domain string, now time.Time) (*TLSCertKeyPair, error)
|
||||
// WriteCert writes the cert for domain.
|
||||
WriteCert(domain string, cert []byte) error
|
||||
// WriteKey writes the key for domain.
|
||||
WriteKey(domain string, key []byte) error
|
||||
// ACMEKey returns the value previously stored via WriteACMEKey.
|
||||
// It is a PEM encoded ECDSA key.
|
||||
ACMEKey() ([]byte, error)
|
||||
// WriteACMEKey stores the provided PEM encoded ECDSA key.
|
||||
WriteACMEKey([]byte) error
|
||||
// WriteTLSCertAndKey writes the cert and key for domain.
|
||||
WriteTLSCertAndKey(domain string, cert, key []byte) error
|
||||
}
|
||||
|
||||
var errCertExpired = errors.New("cert expired")
|
||||
@@ -343,6 +349,13 @@ func (f certFileStore) WriteKey(domain string, key []byte) error {
|
||||
return atomicfile.WriteFile(keyFile(f.dir, domain), key, 0600)
|
||||
}
|
||||
|
||||
func (f certFileStore) WriteTLSCertAndKey(domain string, cert, key []byte) error {
|
||||
if err := f.WriteKey(domain, key); err != nil {
|
||||
return err
|
||||
}
|
||||
return f.WriteCert(domain, cert)
|
||||
}
|
||||
|
||||
// certStateStore implements certStore by storing the cert & key files in an ipn.StateStore.
|
||||
type certStateStore struct {
|
||||
ipn.StateStore
|
||||
@@ -352,7 +365,29 @@ type certStateStore struct {
|
||||
testRoots *x509.CertPool
|
||||
}
|
||||
|
||||
// TLSCertKeyReader is an interface implemented by state stores where it makes
|
||||
// sense to read the TLS cert and key in a single operation that can be
|
||||
// distinguished from generic state value reads. Currently this is only implemented
|
||||
// by the kubestore.Store, which, in some cases, need to read cert and key from a
|
||||
// non-cached TLS Secret.
|
||||
type TLSCertKeyReader interface {
|
||||
ReadTLSCertAndKey(domain string) ([]byte, []byte, error)
|
||||
}
|
||||
|
||||
func (s certStateStore) Read(domain string, now time.Time) (*TLSCertKeyPair, error) {
|
||||
// If we're using a store that supports atomic reads, use that
|
||||
if kr, ok := s.StateStore.(TLSCertKeyReader); ok {
|
||||
cert, key, err := kr.ReadTLSCertAndKey(domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !validCertPEM(domain, key, cert, s.testRoots, now) {
|
||||
return nil, errCertExpired
|
||||
}
|
||||
return &TLSCertKeyPair{CertPEM: cert, KeyPEM: key, Cached: true}, nil
|
||||
}
|
||||
|
||||
// Otherwise fall back to separate reads
|
||||
certPEM, err := s.ReadState(ipn.StateKey(domain + ".crt"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -383,6 +418,27 @@ func (s certStateStore) WriteACMEKey(key []byte) error {
|
||||
return ipn.WriteState(s.StateStore, ipn.StateKey(acmePEMName), key)
|
||||
}
|
||||
|
||||
// TLSCertKeyWriter is an interface implemented by state stores that can write the TLS
|
||||
// cert and key in a single atomic operation. Currently this is only implemented
|
||||
// by the kubestore.StoreKube.
|
||||
type TLSCertKeyWriter interface {
|
||||
WriteTLSCertAndKey(domain string, cert, key []byte) error
|
||||
}
|
||||
|
||||
// WriteTLSCertAndKey writes the TLS cert and key for domain to the current
|
||||
// LocalBackend's StateStore.
|
||||
func (s certStateStore) WriteTLSCertAndKey(domain string, cert, key []byte) error {
|
||||
// If we're using a store that supports atomic writes, use that.
|
||||
if aw, ok := s.StateStore.(TLSCertKeyWriter); ok {
|
||||
return aw.WriteTLSCertAndKey(domain, cert, key)
|
||||
}
|
||||
// Otherwise fall back to separate writes for cert and key.
|
||||
if err := s.WriteKey(domain, key); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.WriteCert(domain, cert)
|
||||
}
|
||||
|
||||
// TLSCertKeyPair is a TLS public and private key, and whether they were obtained
|
||||
// from cache or freshly obtained.
|
||||
type TLSCertKeyPair struct {
|
||||
@@ -419,7 +475,9 @@ func getCertPEMCached(cs certStore, domain string, now time.Time) (p *TLSCertKey
|
||||
return cs.Read(domain, now)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) getCertPEM(ctx context.Context, cs certStore, logf logger.Logf, traceACME func(any), domain string, now time.Time, minValidity time.Duration) (*TLSCertKeyPair, error) {
|
||||
// getCertPem checks if a cert needs to be renewed and if so, renews it.
|
||||
// It can be overridden in tests.
|
||||
var getCertPEM = func(ctx context.Context, b *LocalBackend, cs certStore, logf logger.Logf, traceACME func(any), domain string, now time.Time, minValidity time.Duration) (*TLSCertKeyPair, error) {
|
||||
acmeMu.Lock()
|
||||
defer acmeMu.Unlock()
|
||||
|
||||
@@ -444,6 +502,10 @@ func (b *LocalBackend) getCertPEM(ctx context.Context, cs certStore, logf logger
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !isDefaultDirectoryURL(ac.DirectoryURL) {
|
||||
logf("acme: using Directory URL %q", ac.DirectoryURL)
|
||||
}
|
||||
|
||||
a, err := ac.GetReg(ctx, "" /* pre-RFC param */)
|
||||
switch {
|
||||
case err == nil:
|
||||
@@ -545,9 +607,6 @@ func (b *LocalBackend) getCertPEM(ctx context.Context, cs certStore, logf logger
|
||||
if err := encodeECDSAKey(&privPEM, certPrivKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := cs.WriteKey(domain, privPEM.Bytes()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
csr, err := certRequest(certPrivKey, domain, nil)
|
||||
if err != nil {
|
||||
@@ -555,6 +614,7 @@ func (b *LocalBackend) getCertPEM(ctx context.Context, cs certStore, logf logger
|
||||
}
|
||||
|
||||
logf("requesting cert...")
|
||||
traceACME(csr)
|
||||
der, _, err := ac.CreateOrderCert(ctx, order.FinalizeURL, csr, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("CreateOrder: %v", err)
|
||||
@@ -568,7 +628,7 @@ func (b *LocalBackend) getCertPEM(ctx context.Context, cs certStore, logf logger
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := cs.WriteCert(domain, certPEM.Bytes()); err != nil {
|
||||
if err := cs.WriteTLSCertAndKey(domain, certPEM.Bytes(), privPEM.Bytes()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.domainRenewed(domain)
|
||||
@@ -577,10 +637,10 @@ func (b *LocalBackend) getCertPEM(ctx context.Context, cs certStore, logf logger
|
||||
}
|
||||
|
||||
// certRequest generates a CSR for the given common name cn and optional SANs.
|
||||
func certRequest(key crypto.Signer, cn string, ext []pkix.Extension, san ...string) ([]byte, error) {
|
||||
func certRequest(key crypto.Signer, name string, ext []pkix.Extension) ([]byte, error) {
|
||||
req := &x509.CertificateRequest{
|
||||
Subject: pkix.Name{CommonName: cn},
|
||||
DNSNames: san,
|
||||
Subject: pkix.Name{CommonName: name},
|
||||
DNSNames: []string{name},
|
||||
ExtraExtensions: ext,
|
||||
}
|
||||
return x509.CreateCertificateRequest(rand.Reader, req, key)
|
||||
@@ -657,15 +717,16 @@ func acmeClient(cs certStore) (*acme.Client, error) {
|
||||
// LetsEncrypt), we should make sure that they support ARI extension (see
|
||||
// shouldStartDomainRenewalARI).
|
||||
return &acme.Client{
|
||||
Key: key,
|
||||
UserAgent: "tailscaled/" + version.Long(),
|
||||
Key: key,
|
||||
UserAgent: "tailscaled/" + version.Long(),
|
||||
DirectoryURL: envknob.String("TS_DEBUG_ACME_DIRECTORY_URL"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// validCertPEM reports whether the given certificate is valid for domain at now.
|
||||
//
|
||||
// If roots != nil, it is used instead of the system root pool. This is meant
|
||||
// to support testing, and production code should pass roots == nil.
|
||||
// to support testing; production code should pass roots == nil.
|
||||
func validCertPEM(domain string, keyPEM, certPEM []byte, roots *x509.CertPool, now time.Time) bool {
|
||||
if len(keyPEM) == 0 || len(certPEM) == 0 {
|
||||
return false
|
||||
@@ -688,16 +749,51 @@ func validCertPEM(domain string, keyPEM, certPEM []byte, roots *x509.CertPool, n
|
||||
intermediates.AddCert(cert)
|
||||
}
|
||||
}
|
||||
return validateLeaf(leaf, intermediates, domain, now, roots)
|
||||
}
|
||||
|
||||
// validateLeaf is a helper for [validCertPEM].
|
||||
//
|
||||
// If called with roots == nil, it will use the system root pool as well as the
|
||||
// baked-in roots. If non-nil, only those roots are used.
|
||||
func validateLeaf(leaf *x509.Certificate, intermediates *x509.CertPool, domain string, now time.Time, roots *x509.CertPool) bool {
|
||||
if leaf == nil {
|
||||
return false
|
||||
}
|
||||
_, err = leaf.Verify(x509.VerifyOptions{
|
||||
_, err := leaf.Verify(x509.VerifyOptions{
|
||||
DNSName: domain,
|
||||
CurrentTime: now,
|
||||
Roots: roots,
|
||||
Intermediates: intermediates,
|
||||
})
|
||||
return err == nil
|
||||
if err != nil && roots == nil {
|
||||
// If validation failed and they specified nil for roots (meaning to use
|
||||
// the system roots), then give it another chance to validate using the
|
||||
// binary's baked-in roots (LetsEncrypt). See tailscale/tailscale#14690.
|
||||
return validateLeaf(leaf, intermediates, domain, now, bakedroots.Get())
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// When pointed at a non-prod ACME server, we don't expect to have the CA
|
||||
// in our system or baked-in roots. Verify only throws UnknownAuthorityError
|
||||
// after first checking the leaf cert's expiry, hostnames etc, so we know
|
||||
// that the only reason for an error is to do with constructing a full chain.
|
||||
// Allow this error so that cert caching still works in testing environments.
|
||||
if errors.As(err, &x509.UnknownAuthorityError{}) {
|
||||
acmeURL := envknob.String("TS_DEBUG_ACME_DIRECTORY_URL")
|
||||
if !isDefaultDirectoryURL(acmeURL) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func isDefaultDirectoryURL(u string) bool {
|
||||
return u == "" || u == acme.LetsEncryptURL
|
||||
}
|
||||
|
||||
// validLookingCertDomain reports whether name looks like a valid domain name that
|
||||
|
||||
178
vendor/tailscale.com/ipn/ipnlocal/desktop_sessions.go
generated
vendored
Normal file
178
vendor/tailscale.com/ipn/ipnlocal/desktop_sessions.go
generated
vendored
Normal file
@@ -0,0 +1,178 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Both the desktop session manager and multi-user support
|
||||
// are currently available only on Windows.
|
||||
// This file does not need to be built for other platforms.
|
||||
|
||||
//go:build windows && !ts_omit_desktop_sessions
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"tailscale.com/feature"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/desktop"
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/syspolicy"
|
||||
)
|
||||
|
||||
func init() {
|
||||
feature.Register("desktop-sessions")
|
||||
RegisterExtension("desktop-sessions", newDesktopSessionsExt)
|
||||
}
|
||||
|
||||
// desktopSessionsExt implements [localBackendExtension].
|
||||
var _ localBackendExtension = (*desktopSessionsExt)(nil)
|
||||
|
||||
// desktopSessionsExt extends [LocalBackend] with desktop session management.
|
||||
// It keeps Tailscale running in the background if Always-On mode is enabled,
|
||||
// and switches to an appropriate profile when a user signs in or out,
|
||||
// locks their screen, or disconnects a remote session.
|
||||
type desktopSessionsExt struct {
|
||||
logf logger.Logf
|
||||
sm desktop.SessionManager
|
||||
|
||||
*LocalBackend // or nil, until Init is called
|
||||
cleanup []func() // cleanup functions to call on shutdown
|
||||
|
||||
// mu protects all following fields.
|
||||
// When both mu and [LocalBackend.mu] need to be taken,
|
||||
// [LocalBackend.mu] must be taken before mu.
|
||||
mu sync.Mutex
|
||||
id2sess map[desktop.SessionID]*desktop.Session
|
||||
}
|
||||
|
||||
// newDesktopSessionsExt returns a new [desktopSessionsExt],
|
||||
// or an error if [desktop.SessionManager] is not available.
|
||||
func newDesktopSessionsExt(logf logger.Logf, sys *tsd.System) (localBackendExtension, error) {
|
||||
sm, ok := sys.SessionManager.GetOK()
|
||||
if !ok {
|
||||
return nil, errors.New("session manager is not available")
|
||||
}
|
||||
return &desktopSessionsExt{logf: logf, sm: sm, id2sess: make(map[desktop.SessionID]*desktop.Session)}, nil
|
||||
}
|
||||
|
||||
// Init implements [localBackendExtension].
|
||||
func (e *desktopSessionsExt) Init(lb *LocalBackend) (err error) {
|
||||
e.LocalBackend = lb
|
||||
unregisterResolver := lb.RegisterBackgroundProfileResolver(e.getBackgroundProfile)
|
||||
unregisterSessionCb, err := e.sm.RegisterStateCallback(e.updateDesktopSessionState)
|
||||
if err != nil {
|
||||
unregisterResolver()
|
||||
return fmt.Errorf("session callback registration failed: %w", err)
|
||||
}
|
||||
e.cleanup = []func(){unregisterResolver, unregisterSessionCb}
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateDesktopSessionState is a [desktop.SessionStateCallback]
|
||||
// invoked by [desktop.SessionManager] once for each existing session
|
||||
// and whenever the session state changes. It updates the session map
|
||||
// and switches to the best profile if necessary.
|
||||
func (e *desktopSessionsExt) updateDesktopSessionState(session *desktop.Session) {
|
||||
e.mu.Lock()
|
||||
if session.Status != desktop.ClosedSession {
|
||||
e.id2sess[session.ID] = session
|
||||
} else {
|
||||
delete(e.id2sess, session.ID)
|
||||
}
|
||||
e.mu.Unlock()
|
||||
|
||||
var action string
|
||||
switch session.Status {
|
||||
case desktop.ForegroundSession:
|
||||
// The user has either signed in or unlocked their session.
|
||||
// For remote sessions, this may also mean the user has connected.
|
||||
// The distinction isn't important for our purposes,
|
||||
// so let's always say "signed in".
|
||||
action = "signed in to"
|
||||
case desktop.BackgroundSession:
|
||||
action = "locked"
|
||||
case desktop.ClosedSession:
|
||||
action = "signed out from"
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
maybeUsername, _ := session.User.Username()
|
||||
userIdentifier := cmp.Or(maybeUsername, string(session.User.UserID()), "user")
|
||||
reason := fmt.Sprintf("%s %s session %v", userIdentifier, action, session.ID)
|
||||
|
||||
e.SwitchToBestProfile(reason)
|
||||
}
|
||||
|
||||
// getBackgroundProfile is a [profileResolver] that works as follows:
|
||||
//
|
||||
// If Always-On mode is disabled, it returns no profile ("","",false).
|
||||
//
|
||||
// If AlwaysOn mode is enabled, it returns the current profile unless:
|
||||
// - The current user has signed out.
|
||||
// - Another user has a foreground (i.e. active/unlocked) session.
|
||||
//
|
||||
// If the current user's session runs in the background and no other user
|
||||
// has a foreground session, it returns the current profile. This applies
|
||||
// when a locally signed-in user locks their screen or when a remote user
|
||||
// disconnects without signing out.
|
||||
//
|
||||
// In all other cases, it returns no profile ("","",false).
|
||||
//
|
||||
// It is called with [LocalBackend.mu] locked.
|
||||
func (e *desktopSessionsExt) getBackgroundProfile() (_ ipn.WindowsUserID, _ ipn.ProfileID, ok bool) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
if alwaysOn, _ := syspolicy.GetBoolean(syspolicy.AlwaysOn, false); !alwaysOn {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
isCurrentUserSingedIn := false
|
||||
var foregroundUIDs []ipn.WindowsUserID
|
||||
for _, s := range e.id2sess {
|
||||
switch uid := s.User.UserID(); uid {
|
||||
case e.pm.CurrentUserID():
|
||||
isCurrentUserSingedIn = true
|
||||
if s.Status == desktop.ForegroundSession {
|
||||
// Keep the current profile if the user has a foreground session.
|
||||
return e.pm.CurrentUserID(), e.pm.CurrentProfile().ID(), true
|
||||
}
|
||||
default:
|
||||
if s.Status == desktop.ForegroundSession {
|
||||
foregroundUIDs = append(foregroundUIDs, uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there's no current user (e.g., tailscaled just started), or if the current
|
||||
// user has no foreground session, switch to the default profile of the first user
|
||||
// with a foreground session, if any.
|
||||
for _, uid := range foregroundUIDs {
|
||||
if profileID := e.pm.DefaultUserProfileID(uid); profileID != "" {
|
||||
return uid, profileID, true
|
||||
}
|
||||
}
|
||||
|
||||
// If no user has a foreground session but the current user is still signed in,
|
||||
// keep the current profile even if the session is not in the foreground,
|
||||
// such as when the screen is locked or a remote session is disconnected.
|
||||
if len(foregroundUIDs) == 0 && isCurrentUserSingedIn {
|
||||
return e.pm.CurrentUserID(), e.pm.CurrentProfile().ID(), true
|
||||
}
|
||||
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
// Shutdown implements [localBackendExtension].
|
||||
func (e *desktopSessionsExt) Shutdown() error {
|
||||
for _, f := range e.cleanup {
|
||||
f()
|
||||
}
|
||||
e.cleanup = nil
|
||||
e.LocalBackend = nil
|
||||
return nil
|
||||
}
|
||||
8
vendor/tailscale.com/ipn/ipnlocal/drive.go
generated
vendored
8
vendor/tailscale.com/ipn/ipnlocal/drive.go
generated
vendored
@@ -347,16 +347,14 @@ func (b *LocalBackend) driveRemotesFromPeers(nm *netmap.NetworkMap) []*drive.Rem
|
||||
// TODO(oxtoacart): for some reason, this correctly
|
||||
// catches when a node goes from offline to online,
|
||||
// but not the other way around...
|
||||
online := peer.Online()
|
||||
if online == nil || !*online {
|
||||
if !peer.Online().Get() {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check that the peer is allowed to share with us.
|
||||
addresses := peer.Addresses()
|
||||
for i := range addresses.Len() {
|
||||
addr := addresses.At(i)
|
||||
capsMap := b.PeerCaps(addr.Addr())
|
||||
for _, p := range addresses.All() {
|
||||
capsMap := b.PeerCaps(p.Addr())
|
||||
if capsMap.HasCapability(tailcfg.PeerCapabilityTaildriveSharer) {
|
||||
return true
|
||||
}
|
||||
|
||||
2
vendor/tailscale.com/ipn/ipnlocal/expiry.go
generated
vendored
2
vendor/tailscale.com/ipn/ipnlocal/expiry.go
generated
vendored
@@ -116,7 +116,7 @@ func (em *expiryManager) flagExpiredPeers(netmap *netmap.NetworkMap, localNow ti
|
||||
// since we discover endpoints via DERP, and due to DERP return
|
||||
// path optimization.
|
||||
mut.Endpoints = nil
|
||||
mut.DERP = ""
|
||||
mut.HomeDERP = 0
|
||||
|
||||
// Defense-in-depth: break the node's public key as well, in
|
||||
// case something tries to communicate.
|
||||
|
||||
1890
vendor/tailscale.com/ipn/ipnlocal/local.go
generated
vendored
1890
vendor/tailscale.com/ipn/ipnlocal/local.go
generated
vendored
File diff suppressed because it is too large
Load Diff
10
vendor/tailscale.com/ipn/ipnlocal/network-lock.go
generated
vendored
10
vendor/tailscale.com/ipn/ipnlocal/network-lock.go
generated
vendored
@@ -407,7 +407,7 @@ func (b *LocalBackend) tkaApplyDisablementLocked(secret []byte) error {
|
||||
//
|
||||
// b.mu must be held.
|
||||
func (b *LocalBackend) chonkPathLocked() string {
|
||||
return filepath.Join(b.TailscaleVarRoot(), "tka-profiles", string(b.pm.CurrentProfile().ID))
|
||||
return filepath.Join(b.TailscaleVarRoot(), "tka-profiles", string(b.pm.CurrentProfile().ID()))
|
||||
}
|
||||
|
||||
// tkaBootstrapFromGenesisLocked initializes the local (on-disk) state of the
|
||||
@@ -430,8 +430,7 @@ func (b *LocalBackend) tkaBootstrapFromGenesisLocked(g tkatype.MarshaledAUM, per
|
||||
}
|
||||
bootstrapStateID := fmt.Sprintf("%d:%d", genesis.State.StateID1, genesis.State.StateID2)
|
||||
|
||||
for i := range persist.DisallowedTKAStateIDs().Len() {
|
||||
stateID := persist.DisallowedTKAStateIDs().At(i)
|
||||
for _, stateID := range persist.DisallowedTKAStateIDs().All() {
|
||||
if stateID == bootstrapStateID {
|
||||
return fmt.Errorf("TKA with stateID of %q is disallowed on this node", stateID)
|
||||
}
|
||||
@@ -456,7 +455,7 @@ func (b *LocalBackend) tkaBootstrapFromGenesisLocked(g tkatype.MarshaledAUM, per
|
||||
}
|
||||
|
||||
b.tka = &tkaState{
|
||||
profile: b.pm.CurrentProfile().ID,
|
||||
profile: b.pm.CurrentProfile().ID(),
|
||||
authority: authority,
|
||||
storage: chonk,
|
||||
}
|
||||
@@ -572,8 +571,7 @@ func tkaStateFromPeer(p tailcfg.NodeView) ipnstate.TKAPeer {
|
||||
TailscaleIPs: make([]netip.Addr, 0, p.Addresses().Len()),
|
||||
NodeKey: p.Key(),
|
||||
}
|
||||
for i := range p.Addresses().Len() {
|
||||
addr := p.Addresses().At(i)
|
||||
for _, addr := range p.Addresses().All() {
|
||||
if addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.Addr()) {
|
||||
fp.TailscaleIPs = append(fp.TailscaleIPs, addr.Addr())
|
||||
}
|
||||
|
||||
135
vendor/tailscale.com/ipn/ipnlocal/peerapi.go
generated
vendored
135
vendor/tailscale.com/ipn/ipnlocal/peerapi.go
generated
vendored
@@ -20,13 +20,11 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/kortschak/wol"
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
"golang.org/x/net/http/httpguts"
|
||||
"tailscale.com/drive"
|
||||
@@ -226,6 +224,23 @@ type peerAPIHandler struct {
|
||||
peerUser tailcfg.UserProfile // profile of peerNode
|
||||
}
|
||||
|
||||
// PeerAPIHandler is the interface implemented by [peerAPIHandler] and needed by
|
||||
// module features registered via tailscale.com/feature/*.
|
||||
type PeerAPIHandler interface {
|
||||
Peer() tailcfg.NodeView
|
||||
PeerCaps() tailcfg.PeerCapMap
|
||||
Self() tailcfg.NodeView
|
||||
LocalBackend() *LocalBackend
|
||||
IsSelfUntagged() bool // whether the peer is untagged and the same as this user
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) IsSelfUntagged() bool {
|
||||
return !h.selfNode.IsTagged() && !h.peerNode.IsTagged() && h.isSelf
|
||||
}
|
||||
func (h *peerAPIHandler) Peer() tailcfg.NodeView { return h.peerNode }
|
||||
func (h *peerAPIHandler) Self() tailcfg.NodeView { return h.selfNode }
|
||||
func (h *peerAPIHandler) LocalBackend() *LocalBackend { return h.ps.b }
|
||||
|
||||
func (h *peerAPIHandler) logf(format string, a ...any) {
|
||||
h.ps.b.logf("peerapi: "+format, a...)
|
||||
}
|
||||
@@ -233,11 +248,13 @@ func (h *peerAPIHandler) logf(format string, a ...any) {
|
||||
// isAddressValid reports whether addr is a valid destination address for this
|
||||
// node originating from the peer.
|
||||
func (h *peerAPIHandler) isAddressValid(addr netip.Addr) bool {
|
||||
if v := h.peerNode.SelfNodeV4MasqAddrForThisPeer(); v != nil {
|
||||
return *v == addr
|
||||
if !addr.IsValid() {
|
||||
return false
|
||||
}
|
||||
if v := h.peerNode.SelfNodeV6MasqAddrForThisPeer(); v != nil {
|
||||
return *v == addr
|
||||
v4MasqAddr, hasMasqV4 := h.peerNode.SelfNodeV4MasqAddrForThisPeer().GetOk()
|
||||
v6MasqAddr, hasMasqV6 := h.peerNode.SelfNodeV6MasqAddrForThisPeer().GetOk()
|
||||
if hasMasqV4 || hasMasqV6 {
|
||||
return addr == v4MasqAddr || addr == v6MasqAddr
|
||||
}
|
||||
pfx := netip.PrefixFrom(addr, addr.BitLen())
|
||||
return views.SliceContains(h.selfNode.Addresses(), pfx)
|
||||
@@ -300,6 +317,20 @@ func peerAPIRequestShouldGetSecurityHeaders(r *http.Request) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// RegisterPeerAPIHandler registers a PeerAPI handler.
|
||||
//
|
||||
// The path should be of the form "/v0/foo".
|
||||
//
|
||||
// It panics if the path is already registered.
|
||||
func RegisterPeerAPIHandler(path string, f func(PeerAPIHandler, http.ResponseWriter, *http.Request)) {
|
||||
if _, ok := peerAPIHandlers[path]; ok {
|
||||
panic(fmt.Sprintf("duplicate PeerAPI handler %q", path))
|
||||
}
|
||||
peerAPIHandlers[path] = f
|
||||
}
|
||||
|
||||
var peerAPIHandlers = map[string]func(PeerAPIHandler, http.ResponseWriter, *http.Request){} // by URL.Path
|
||||
|
||||
func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if err := h.validatePeerAPIRequest(r); err != nil {
|
||||
metricInvalidRequests.Add(1)
|
||||
@@ -344,10 +375,6 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
case "/v0/dnsfwd":
|
||||
h.handleServeDNSFwd(w, r)
|
||||
return
|
||||
case "/v0/wol":
|
||||
metricWakeOnLANCalls.Add(1)
|
||||
h.handleWakeOnLAN(w, r)
|
||||
return
|
||||
case "/v0/interfaces":
|
||||
h.handleServeInterfaces(w, r)
|
||||
return
|
||||
@@ -362,6 +389,10 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.handleServeIngress(w, r)
|
||||
return
|
||||
}
|
||||
if ph, ok := peerAPIHandlers[r.URL.Path]; ok {
|
||||
ph(h, w, r)
|
||||
return
|
||||
}
|
||||
who := h.peerUser.DisplayName
|
||||
fmt.Fprintf(w, `<html>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
@@ -450,7 +481,7 @@ func (h *peerAPIHandler) handleServeInterfaces(w http.ResponseWriter, r *http.Re
|
||||
fmt.Fprintf(w, "<h3>Could not get the default route: %s</h3>\n", html.EscapeString(err.Error()))
|
||||
}
|
||||
|
||||
if hasCGNATInterface, err := netmon.HasCGNATInterface(); hasCGNATInterface {
|
||||
if hasCGNATInterface, err := h.ps.b.sys.NetMon.Get().HasCGNATInterface(); hasCGNATInterface {
|
||||
fmt.Fprintln(w, "<p>There is another interface using the CGNAT range.</p>")
|
||||
} else if err != nil {
|
||||
fmt.Fprintf(w, "<p>Could not check for CGNAT interfaces: %s</p>\n", html.EscapeString(err.Error()))
|
||||
@@ -622,14 +653,6 @@ func (h *peerAPIHandler) canDebug() bool {
|
||||
return h.isSelf || h.peerHasCap(tailcfg.PeerCapabilityDebugPeer)
|
||||
}
|
||||
|
||||
// canWakeOnLAN reports whether h can send a Wake-on-LAN packet from this node.
|
||||
func (h *peerAPIHandler) canWakeOnLAN() bool {
|
||||
if h.peerNode.UnsignedPeerAPIOnly() {
|
||||
return false
|
||||
}
|
||||
return h.isSelf || h.peerHasCap(tailcfg.PeerCapabilityWakeOnLAN)
|
||||
}
|
||||
|
||||
var allowSelfIngress = envknob.RegisterBool("TS_ALLOW_SELF_INGRESS")
|
||||
|
||||
// canIngress reports whether h can send ingress requests to this node.
|
||||
@@ -638,10 +661,10 @@ func (h *peerAPIHandler) canIngress() bool {
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) peerHasCap(wantCap tailcfg.PeerCapability) bool {
|
||||
return h.peerCaps().HasCapability(wantCap)
|
||||
return h.PeerCaps().HasCapability(wantCap)
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) peerCaps() tailcfg.PeerCapMap {
|
||||
func (h *peerAPIHandler) PeerCaps() tailcfg.PeerCapMap {
|
||||
return h.ps.b.PeerCaps(h.remoteAddr.Addr())
|
||||
}
|
||||
|
||||
@@ -815,61 +838,6 @@ func (h *peerAPIHandler) handleServeDNSFwd(w http.ResponseWriter, r *http.Reques
|
||||
dh.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handleWakeOnLAN(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.canWakeOnLAN() {
|
||||
http.Error(w, "no WoL access", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "bad method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
macStr := r.FormValue("mac")
|
||||
if macStr == "" {
|
||||
http.Error(w, "missing 'mac' param", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
mac, err := net.ParseMAC(macStr)
|
||||
if err != nil {
|
||||
http.Error(w, "bad 'mac' param", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var password []byte // TODO(bradfitz): support? does anything use WoL passwords?
|
||||
st := h.ps.b.sys.NetMon.Get().InterfaceState()
|
||||
if st == nil {
|
||||
http.Error(w, "failed to get interfaces state", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
var res struct {
|
||||
SentTo []string
|
||||
Errors []string
|
||||
}
|
||||
for ifName, ips := range st.InterfaceIPs {
|
||||
for _, ip := range ips {
|
||||
if ip.Addr().IsLoopback() || ip.Addr().Is6() {
|
||||
continue
|
||||
}
|
||||
local := &net.UDPAddr{
|
||||
IP: ip.Addr().AsSlice(),
|
||||
Port: 0,
|
||||
}
|
||||
remote := &net.UDPAddr{
|
||||
IP: net.IPv4bcast,
|
||||
Port: 0,
|
||||
}
|
||||
if err := wol.Wake(mac, password, local, remote); err != nil {
|
||||
res.Errors = append(res.Errors, err.Error())
|
||||
} else {
|
||||
res.SentTo = append(res.SentTo, ifName)
|
||||
}
|
||||
break // one per interface is enough
|
||||
}
|
||||
}
|
||||
sort.Strings(res.SentTo)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) replyToDNSQueries() bool {
|
||||
if h.isSelf {
|
||||
// If the peer is owned by the same user, just allow it
|
||||
@@ -964,7 +932,11 @@ func (h *peerAPIHandler) handleDNSQuery(w http.ResponseWriter, r *http.Request)
|
||||
// instead to avoid re-parsing the DNS response for improved performance in
|
||||
// the future.
|
||||
if h.ps.b.OfferingAppConnector() {
|
||||
h.ps.b.ObserveDNSResponse(res)
|
||||
if err := h.ps.b.ObserveDNSResponse(res); err != nil {
|
||||
h.logf("ObserveDNSResponse error: %v", err)
|
||||
// This is not fatal, we probably just failed to parse the upstream
|
||||
// response. Return it to the caller anyway.
|
||||
}
|
||||
}
|
||||
|
||||
if pretty {
|
||||
@@ -1148,7 +1120,7 @@ func (h *peerAPIHandler) handleServeDrive(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
capsMap := h.peerCaps()
|
||||
capsMap := h.PeerCaps()
|
||||
driveCaps, ok := capsMap[tailcfg.PeerCapabilityTaildrive]
|
||||
if !ok {
|
||||
h.logf("taildrive: not permitted")
|
||||
@@ -1272,8 +1244,7 @@ var (
|
||||
metricInvalidRequests = clientmetric.NewCounter("peerapi_invalid_requests")
|
||||
|
||||
// Non-debug PeerAPI endpoints.
|
||||
metricPutCalls = clientmetric.NewCounter("peerapi_put")
|
||||
metricDNSCalls = clientmetric.NewCounter("peerapi_dns")
|
||||
metricWakeOnLANCalls = clientmetric.NewCounter("peerapi_wol")
|
||||
metricIngressCalls = clientmetric.NewCounter("peerapi_ingress")
|
||||
metricPutCalls = clientmetric.NewCounter("peerapi_put")
|
||||
metricDNSCalls = clientmetric.NewCounter("peerapi_dns")
|
||||
metricIngressCalls = clientmetric.NewCounter("peerapi_ingress")
|
||||
)
|
||||
|
||||
271
vendor/tailscale.com/ipn/ipnlocal/profiles.go
generated
vendored
271
vendor/tailscale.com/ipn/ipnlocal/profiles.go
generated
vendored
@@ -35,9 +35,9 @@ type profileManager struct {
|
||||
health *health.Tracker
|
||||
|
||||
currentUserID ipn.WindowsUserID
|
||||
knownProfiles map[ipn.ProfileID]*ipn.LoginProfile // always non-nil
|
||||
currentProfile *ipn.LoginProfile // always non-nil
|
||||
prefs ipn.PrefsView // always Valid.
|
||||
knownProfiles map[ipn.ProfileID]ipn.LoginProfileView // always non-nil
|
||||
currentProfile ipn.LoginProfileView // always Valid.
|
||||
prefs ipn.PrefsView // always Valid.
|
||||
}
|
||||
|
||||
func (pm *profileManager) dlogf(format string, args ...any) {
|
||||
@@ -77,6 +77,49 @@ func (pm *profileManager) SetCurrentUserID(uid ipn.WindowsUserID) {
|
||||
}
|
||||
}
|
||||
|
||||
// SetCurrentUserAndProfile sets the current user ID and switches the specified
|
||||
// profile, if it is accessible to the user. If the profile does not exist,
|
||||
// or is not accessible, it switches to the user's default profile,
|
||||
// creating a new one if necessary.
|
||||
//
|
||||
// It is a shorthand for [profileManager.SetCurrentUserID] followed by
|
||||
// [profileManager.SwitchProfile], but it is more efficient as it switches
|
||||
// directly to the specified profile rather than switching to the user's
|
||||
// default profile first.
|
||||
//
|
||||
// As a special case, if the specified profile ID "", it creates a new
|
||||
// profile for the user and switches to it, unless the current profile
|
||||
// is already a new, empty profile owned by the user.
|
||||
//
|
||||
// It returns the current profile and whether the call resulted
|
||||
// in a profile switch.
|
||||
func (pm *profileManager) SetCurrentUserAndProfile(uid ipn.WindowsUserID, profileID ipn.ProfileID) (cp ipn.LoginProfileView, changed bool) {
|
||||
pm.currentUserID = uid
|
||||
|
||||
if profileID == "" {
|
||||
if pm.currentProfile.ID() == "" && pm.currentProfile.LocalUserID() == uid {
|
||||
return pm.currentProfile, false
|
||||
}
|
||||
pm.NewProfileForUser(uid)
|
||||
return pm.currentProfile, true
|
||||
}
|
||||
|
||||
if profile, err := pm.ProfileByID(profileID); err == nil {
|
||||
if pm.CurrentProfile().ID() == profileID {
|
||||
return pm.currentProfile, false
|
||||
}
|
||||
if err := pm.SwitchProfile(profile.ID()); err == nil {
|
||||
return pm.currentProfile, true
|
||||
}
|
||||
}
|
||||
|
||||
if err := pm.SwitchToDefaultProfile(); err != nil {
|
||||
pm.logf("%q's default profile cannot be used; creating a new one: %v", uid, err)
|
||||
pm.NewProfile()
|
||||
}
|
||||
return pm.currentProfile, true
|
||||
}
|
||||
|
||||
// DefaultUserProfileID returns [ipn.ProfileID] of the default (last used) profile for the specified user,
|
||||
// or an empty string if the specified user does not have a default profile.
|
||||
func (pm *profileManager) DefaultUserProfileID(uid ipn.WindowsUserID) ipn.ProfileID {
|
||||
@@ -89,7 +132,7 @@ func (pm *profileManager) DefaultUserProfileID(uid ipn.WindowsUserID) ipn.Profil
|
||||
pm.dlogf("DefaultUserProfileID: windows: migrating from legacy preferences")
|
||||
profile, err := pm.migrateFromLegacyPrefs(uid, false)
|
||||
if err == nil {
|
||||
return profile.ID
|
||||
return profile.ID()
|
||||
}
|
||||
pm.logf("failed to migrate from legacy preferences: %v", err)
|
||||
}
|
||||
@@ -97,41 +140,48 @@ func (pm *profileManager) DefaultUserProfileID(uid ipn.WindowsUserID) ipn.Profil
|
||||
}
|
||||
|
||||
pk := ipn.StateKey(string(b))
|
||||
prof := pm.findProfileByKey(pk)
|
||||
if prof == nil {
|
||||
prof := pm.findProfileByKey(uid, pk)
|
||||
if !prof.Valid() {
|
||||
pm.dlogf("DefaultUserProfileID: no profile found for key: %q", pk)
|
||||
return ""
|
||||
}
|
||||
return prof.ID
|
||||
return prof.ID()
|
||||
}
|
||||
|
||||
// checkProfileAccess returns an [errProfileAccessDenied] if the current user
|
||||
// does not have access to the specified profile.
|
||||
func (pm *profileManager) checkProfileAccess(profile *ipn.LoginProfile) error {
|
||||
if pm.currentUserID != "" && profile.LocalUserID != pm.currentUserID {
|
||||
func (pm *profileManager) checkProfileAccess(profile ipn.LoginProfileView) error {
|
||||
return pm.checkProfileAccessAs(pm.currentUserID, profile)
|
||||
}
|
||||
|
||||
// checkProfileAccessAs returns an [errProfileAccessDenied] if the specified user
|
||||
// does not have access to the specified profile.
|
||||
func (pm *profileManager) checkProfileAccessAs(uid ipn.WindowsUserID, profile ipn.LoginProfileView) error {
|
||||
if uid != "" && profile.LocalUserID() != uid {
|
||||
return errProfileAccessDenied
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// allProfiles returns all profiles accessible to the current user.
|
||||
// allProfilesFor returns all profiles accessible to the specified user.
|
||||
// The returned profiles are sorted by Name.
|
||||
func (pm *profileManager) allProfiles() (out []*ipn.LoginProfile) {
|
||||
func (pm *profileManager) allProfilesFor(uid ipn.WindowsUserID) []ipn.LoginProfileView {
|
||||
out := make([]ipn.LoginProfileView, 0, len(pm.knownProfiles))
|
||||
for _, p := range pm.knownProfiles {
|
||||
if pm.checkProfileAccess(p) == nil {
|
||||
if pm.checkProfileAccessAs(uid, p) == nil {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
slices.SortFunc(out, func(a, b *ipn.LoginProfile) int {
|
||||
return cmp.Compare(a.Name, b.Name)
|
||||
slices.SortFunc(out, func(a, b ipn.LoginProfileView) int {
|
||||
return cmp.Compare(a.Name(), b.Name())
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
// matchingProfiles is like [profileManager.allProfiles], but returns only profiles
|
||||
// matchingProfiles is like [profileManager.allProfilesFor], but returns only profiles
|
||||
// matching the given predicate.
|
||||
func (pm *profileManager) matchingProfiles(f func(*ipn.LoginProfile) bool) (out []*ipn.LoginProfile) {
|
||||
all := pm.allProfiles()
|
||||
func (pm *profileManager) matchingProfiles(uid ipn.WindowsUserID, f func(ipn.LoginProfileView) bool) (out []ipn.LoginProfileView) {
|
||||
all := pm.allProfilesFor(uid)
|
||||
out = all[:0]
|
||||
for _, p := range all {
|
||||
if f(p) {
|
||||
@@ -144,11 +194,11 @@ func (pm *profileManager) matchingProfiles(f func(*ipn.LoginProfile) bool) (out
|
||||
// findMatchingProfiles returns all profiles accessible to the current user
|
||||
// that represent the same node/user as prefs.
|
||||
// The returned profiles are sorted by Name.
|
||||
func (pm *profileManager) findMatchingProfiles(prefs ipn.PrefsView) []*ipn.LoginProfile {
|
||||
return pm.matchingProfiles(func(p *ipn.LoginProfile) bool {
|
||||
return p.ControlURL == prefs.ControlURL() &&
|
||||
(p.UserProfile.ID == prefs.Persist().UserProfile().ID ||
|
||||
p.NodeID == prefs.Persist().NodeID())
|
||||
func (pm *profileManager) findMatchingProfiles(uid ipn.WindowsUserID, prefs ipn.PrefsView) []ipn.LoginProfileView {
|
||||
return pm.matchingProfiles(uid, func(p ipn.LoginProfileView) bool {
|
||||
return p.ControlURL() == prefs.ControlURL() &&
|
||||
(p.UserProfile().ID == prefs.Persist().UserProfile().ID ||
|
||||
p.NodeID() == prefs.Persist().NodeID())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -156,19 +206,19 @@ func (pm *profileManager) findMatchingProfiles(prefs ipn.PrefsView) []*ipn.Login
|
||||
// given name. It returns "" if no such profile exists among profiles
|
||||
// accessible to the current user.
|
||||
func (pm *profileManager) ProfileIDForName(name string) ipn.ProfileID {
|
||||
p := pm.findProfileByName(name)
|
||||
if p == nil {
|
||||
p := pm.findProfileByName(pm.currentUserID, name)
|
||||
if !p.Valid() {
|
||||
return ""
|
||||
}
|
||||
return p.ID
|
||||
return p.ID()
|
||||
}
|
||||
|
||||
func (pm *profileManager) findProfileByName(name string) *ipn.LoginProfile {
|
||||
out := pm.matchingProfiles(func(p *ipn.LoginProfile) bool {
|
||||
return p.Name == name
|
||||
func (pm *profileManager) findProfileByName(uid ipn.WindowsUserID, name string) ipn.LoginProfileView {
|
||||
out := pm.matchingProfiles(uid, func(p ipn.LoginProfileView) bool {
|
||||
return p.Name() == name && pm.checkProfileAccessAs(uid, p) == nil
|
||||
})
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
return ipn.LoginProfileView{}
|
||||
}
|
||||
if len(out) > 1 {
|
||||
pm.logf("[unexpected] multiple profiles with the same name")
|
||||
@@ -176,12 +226,12 @@ func (pm *profileManager) findProfileByName(name string) *ipn.LoginProfile {
|
||||
return out[0]
|
||||
}
|
||||
|
||||
func (pm *profileManager) findProfileByKey(key ipn.StateKey) *ipn.LoginProfile {
|
||||
out := pm.matchingProfiles(func(p *ipn.LoginProfile) bool {
|
||||
return p.Key == key
|
||||
func (pm *profileManager) findProfileByKey(uid ipn.WindowsUserID, key ipn.StateKey) ipn.LoginProfileView {
|
||||
out := pm.matchingProfiles(uid, func(p ipn.LoginProfileView) bool {
|
||||
return p.Key() == key && pm.checkProfileAccessAs(uid, p) == nil
|
||||
})
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
return ipn.LoginProfileView{}
|
||||
}
|
||||
if len(out) > 1 {
|
||||
pm.logf("[unexpected] multiple profiles with the same key")
|
||||
@@ -194,8 +244,8 @@ func (pm *profileManager) setUnattendedModeAsConfigured() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if pm.currentProfile.Key != "" && pm.prefs.ForceDaemon() {
|
||||
return pm.WriteState(ipn.ServerModeStartKey, []byte(pm.currentProfile.Key))
|
||||
if pm.currentProfile.Key() != "" && pm.prefs.ForceDaemon() {
|
||||
return pm.WriteState(ipn.ServerModeStartKey, []byte(pm.currentProfile.Key()))
|
||||
} else {
|
||||
return pm.WriteState(ipn.ServerModeStartKey, nil)
|
||||
}
|
||||
@@ -222,36 +272,43 @@ func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView, np ipn.NetworkProfile)
|
||||
}
|
||||
|
||||
// Check if we already have an existing profile that matches the user/node.
|
||||
if existing := pm.findMatchingProfiles(prefsIn); len(existing) > 0 {
|
||||
if existing := pm.findMatchingProfiles(pm.currentUserID, prefsIn); len(existing) > 0 {
|
||||
// We already have a profile for this user/node we should reuse it. Also
|
||||
// cleanup any other duplicate profiles.
|
||||
cp = existing[0]
|
||||
existing = existing[1:]
|
||||
for _, p := range existing {
|
||||
// Clear the state.
|
||||
if err := pm.store.WriteState(p.Key, nil); err != nil {
|
||||
if err := pm.store.WriteState(p.Key(), nil); err != nil {
|
||||
// We couldn't delete the state, so keep the profile around.
|
||||
continue
|
||||
}
|
||||
// Remove the profile, knownProfiles will be persisted
|
||||
// in [profileManager.setProfilePrefs] below.
|
||||
delete(pm.knownProfiles, p.ID)
|
||||
delete(pm.knownProfiles, p.ID())
|
||||
}
|
||||
}
|
||||
pm.currentProfile = cp
|
||||
if err := pm.SetProfilePrefs(cp, prefsIn, np); err != nil {
|
||||
cp, err := pm.setProfilePrefs(nil, prefsIn, np)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return pm.setProfileAsUserDefault(cp)
|
||||
|
||||
}
|
||||
|
||||
// SetProfilePrefs is like [profileManager.SetPrefs], but sets prefs for the specified [ipn.LoginProfile]
|
||||
// which is not necessarily the [profileManager.CurrentProfile]. It returns an [errProfileAccessDenied]
|
||||
// if the specified profile is not accessible by the current user.
|
||||
func (pm *profileManager) SetProfilePrefs(lp *ipn.LoginProfile, prefsIn ipn.PrefsView, np ipn.NetworkProfile) error {
|
||||
if err := pm.checkProfileAccess(lp); err != nil {
|
||||
return err
|
||||
// setProfilePrefs is like [profileManager.SetPrefs], but sets prefs for the specified [ipn.LoginProfile],
|
||||
// returning a read-only view of the updated profile on success. If the specified profile is nil,
|
||||
// it defaults to the current profile. If the profile is not accessible by the current user,
|
||||
// the method returns an [errProfileAccessDenied].
|
||||
func (pm *profileManager) setProfilePrefs(lp *ipn.LoginProfile, prefsIn ipn.PrefsView, np ipn.NetworkProfile) (ipn.LoginProfileView, error) {
|
||||
isCurrentProfile := lp == nil || (lp.ID != "" && lp.ID == pm.currentProfile.ID())
|
||||
if isCurrentProfile {
|
||||
lp = pm.CurrentProfile().AsStruct()
|
||||
}
|
||||
|
||||
if err := pm.checkProfileAccess(lp.View()); err != nil {
|
||||
return ipn.LoginProfileView{}, err
|
||||
}
|
||||
|
||||
// An empty profile.ID indicates that the profile is new, the node info wasn't available,
|
||||
@@ -291,23 +348,29 @@ func (pm *profileManager) SetProfilePrefs(lp *ipn.LoginProfile, prefsIn ipn.Pref
|
||||
lp.UserProfile = up
|
||||
lp.NetworkProfile = np
|
||||
|
||||
// Update the current profile view to reflect the changes
|
||||
// if the specified profile is the current profile.
|
||||
if isCurrentProfile {
|
||||
pm.currentProfile = lp.View()
|
||||
}
|
||||
|
||||
// An empty profile.ID indicates that the node info is not available yet,
|
||||
// and the profile doesn't need to be saved on disk.
|
||||
if lp.ID != "" {
|
||||
pm.knownProfiles[lp.ID] = lp
|
||||
pm.knownProfiles[lp.ID] = lp.View()
|
||||
if err := pm.writeKnownProfiles(); err != nil {
|
||||
return err
|
||||
return ipn.LoginProfileView{}, err
|
||||
}
|
||||
// Clone prefsIn and create a read-only view as a safety measure to
|
||||
// prevent accidental preference mutations, both externally and internally.
|
||||
if err := pm.setProfilePrefsNoPermCheck(lp, prefsIn.AsStruct().View()); err != nil {
|
||||
return err
|
||||
if err := pm.setProfilePrefsNoPermCheck(lp.View(), prefsIn.AsStruct().View()); err != nil {
|
||||
return ipn.LoginProfileView{}, err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return lp.View(), nil
|
||||
}
|
||||
|
||||
func newUnusedID(knownProfiles map[ipn.ProfileID]*ipn.LoginProfile) (ipn.ProfileID, ipn.StateKey) {
|
||||
func newUnusedID(knownProfiles map[ipn.ProfileID]ipn.LoginProfileView) (ipn.ProfileID, ipn.StateKey) {
|
||||
var idb [2]byte
|
||||
for {
|
||||
rand.Read(idb[:])
|
||||
@@ -326,14 +389,14 @@ func newUnusedID(knownProfiles map[ipn.ProfileID]*ipn.LoginProfile) (ipn.Profile
|
||||
// The method does not perform any additional checks on the specified
|
||||
// profile, such as verifying the caller's access rights or checking
|
||||
// if another profile for the same node already exists.
|
||||
func (pm *profileManager) setProfilePrefsNoPermCheck(profile *ipn.LoginProfile, clonedPrefs ipn.PrefsView) error {
|
||||
func (pm *profileManager) setProfilePrefsNoPermCheck(profile ipn.LoginProfileView, clonedPrefs ipn.PrefsView) error {
|
||||
isCurrentProfile := pm.currentProfile == profile
|
||||
if isCurrentProfile {
|
||||
pm.prefs = clonedPrefs
|
||||
pm.updateHealth()
|
||||
}
|
||||
if profile.Key != "" {
|
||||
if err := pm.writePrefsToStore(profile.Key, clonedPrefs); err != nil {
|
||||
if profile.Key() != "" {
|
||||
if err := pm.writePrefsToStore(profile.Key(), clonedPrefs); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if !isCurrentProfile {
|
||||
@@ -362,38 +425,33 @@ func (pm *profileManager) writePrefsToStore(key ipn.StateKey, prefs ipn.PrefsVie
|
||||
}
|
||||
|
||||
// Profiles returns the list of known profiles accessible to the current user.
|
||||
func (pm *profileManager) Profiles() []ipn.LoginProfile {
|
||||
allProfiles := pm.allProfiles()
|
||||
out := make([]ipn.LoginProfile, len(allProfiles))
|
||||
for i, p := range allProfiles {
|
||||
out[i] = *p
|
||||
}
|
||||
return out
|
||||
func (pm *profileManager) Profiles() []ipn.LoginProfileView {
|
||||
return pm.allProfilesFor(pm.currentUserID)
|
||||
}
|
||||
|
||||
// ProfileByID returns a profile with the given id, if it is accessible to the current user.
|
||||
// If the profile exists but is not accessible to the current user, it returns an [errProfileAccessDenied].
|
||||
// If the profile does not exist, it returns an [errProfileNotFound].
|
||||
func (pm *profileManager) ProfileByID(id ipn.ProfileID) (ipn.LoginProfile, error) {
|
||||
func (pm *profileManager) ProfileByID(id ipn.ProfileID) (ipn.LoginProfileView, error) {
|
||||
kp, err := pm.profileByIDNoPermCheck(id)
|
||||
if err != nil {
|
||||
return ipn.LoginProfile{}, err
|
||||
return ipn.LoginProfileView{}, err
|
||||
}
|
||||
if err := pm.checkProfileAccess(kp); err != nil {
|
||||
return ipn.LoginProfile{}, err
|
||||
return ipn.LoginProfileView{}, err
|
||||
}
|
||||
return *kp, nil
|
||||
return kp, nil
|
||||
}
|
||||
|
||||
// profileByIDNoPermCheck is like [profileManager.ProfileByID], but it doesn't
|
||||
// check user's access rights to the profile.
|
||||
func (pm *profileManager) profileByIDNoPermCheck(id ipn.ProfileID) (*ipn.LoginProfile, error) {
|
||||
if id == pm.currentProfile.ID {
|
||||
func (pm *profileManager) profileByIDNoPermCheck(id ipn.ProfileID) (ipn.LoginProfileView, error) {
|
||||
if id == pm.currentProfile.ID() {
|
||||
return pm.currentProfile, nil
|
||||
}
|
||||
kp, ok := pm.knownProfiles[id]
|
||||
if !ok {
|
||||
return nil, errProfileNotFound
|
||||
return ipn.LoginProfileView{}, errProfileNotFound
|
||||
}
|
||||
return kp, nil
|
||||
}
|
||||
@@ -412,11 +470,11 @@ func (pm *profileManager) ProfilePrefs(id ipn.ProfileID) (ipn.PrefsView, error)
|
||||
return pm.profilePrefs(kp)
|
||||
}
|
||||
|
||||
func (pm *profileManager) profilePrefs(p *ipn.LoginProfile) (ipn.PrefsView, error) {
|
||||
if p.ID == pm.currentProfile.ID {
|
||||
func (pm *profileManager) profilePrefs(p ipn.LoginProfileView) (ipn.PrefsView, error) {
|
||||
if p.ID() == pm.currentProfile.ID() {
|
||||
return pm.prefs, nil
|
||||
}
|
||||
return pm.loadSavedPrefs(p.Key)
|
||||
return pm.loadSavedPrefs(p.Key())
|
||||
}
|
||||
|
||||
// SwitchProfile switches to the profile with the given id.
|
||||
@@ -429,14 +487,14 @@ func (pm *profileManager) SwitchProfile(id ipn.ProfileID) error {
|
||||
if !ok {
|
||||
return errProfileNotFound
|
||||
}
|
||||
if pm.currentProfile != nil && kp.ID == pm.currentProfile.ID && pm.prefs.Valid() {
|
||||
if pm.currentProfile.Valid() && kp.ID() == pm.currentProfile.ID() && pm.prefs.Valid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := pm.checkProfileAccess(kp); err != nil {
|
||||
return fmt.Errorf("%w: profile %q is not accessible to the current user", err, id)
|
||||
}
|
||||
prefs, err := pm.loadSavedPrefs(kp.Key)
|
||||
prefs, err := pm.loadSavedPrefs(kp.Key())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -459,8 +517,8 @@ func (pm *profileManager) SwitchToDefaultProfile() error {
|
||||
|
||||
// setProfileAsUserDefault sets the specified profile as the default for the current user.
|
||||
// It returns an [errProfileAccessDenied] if the specified profile is not accessible to the current user.
|
||||
func (pm *profileManager) setProfileAsUserDefault(profile *ipn.LoginProfile) error {
|
||||
if profile.Key == "" {
|
||||
func (pm *profileManager) setProfileAsUserDefault(profile ipn.LoginProfileView) error {
|
||||
if profile.Key() == "" {
|
||||
// The profile has not been persisted yet; ignore it for now.
|
||||
return nil
|
||||
}
|
||||
@@ -468,7 +526,7 @@ func (pm *profileManager) setProfileAsUserDefault(profile *ipn.LoginProfile) err
|
||||
return errProfileAccessDenied
|
||||
}
|
||||
k := ipn.CurrentProfileKey(string(pm.currentUserID))
|
||||
return pm.WriteState(k, []byte(profile.Key))
|
||||
return pm.WriteState(k, []byte(profile.Key()))
|
||||
}
|
||||
|
||||
func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error) {
|
||||
@@ -507,10 +565,10 @@ func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error
|
||||
return savedPrefs.View(), nil
|
||||
}
|
||||
|
||||
// CurrentProfile returns the current LoginProfile.
|
||||
// CurrentProfile returns a read-only [ipn.LoginProfileView] of the current profile.
|
||||
// The value may be zero if the profile is not persisted.
|
||||
func (pm *profileManager) CurrentProfile() ipn.LoginProfile {
|
||||
return *pm.currentProfile
|
||||
func (pm *profileManager) CurrentProfile() ipn.LoginProfileView {
|
||||
return pm.currentProfile
|
||||
}
|
||||
|
||||
// errProfileNotFound is returned by methods that accept a ProfileID
|
||||
@@ -533,7 +591,7 @@ var errProfileAccessDenied = errors.New("profile access denied")
|
||||
// recommended to call [profileManager.SwitchProfile] first.
|
||||
func (pm *profileManager) DeleteProfile(id ipn.ProfileID) error {
|
||||
metricDeleteProfile.Add(1)
|
||||
if id == pm.currentProfile.ID {
|
||||
if id == pm.currentProfile.ID() {
|
||||
return pm.deleteCurrentProfile()
|
||||
}
|
||||
kp, ok := pm.knownProfiles[id]
|
||||
@@ -550,7 +608,7 @@ func (pm *profileManager) deleteCurrentProfile() error {
|
||||
if err := pm.checkProfileAccess(pm.currentProfile); err != nil {
|
||||
return err
|
||||
}
|
||||
if pm.currentProfile.ID == "" {
|
||||
if pm.currentProfile.ID() == "" {
|
||||
// Deleting the in-memory only new profile, just create a new one.
|
||||
pm.NewProfile()
|
||||
return nil
|
||||
@@ -560,14 +618,14 @@ func (pm *profileManager) deleteCurrentProfile() error {
|
||||
|
||||
// deleteProfileNoPermCheck is like [profileManager.DeleteProfile],
|
||||
// but it doesn't check user's access rights to the profile.
|
||||
func (pm *profileManager) deleteProfileNoPermCheck(profile *ipn.LoginProfile) error {
|
||||
if profile.ID == pm.currentProfile.ID {
|
||||
func (pm *profileManager) deleteProfileNoPermCheck(profile ipn.LoginProfileView) error {
|
||||
if profile.ID() == pm.currentProfile.ID() {
|
||||
pm.NewProfile()
|
||||
}
|
||||
if err := pm.WriteState(profile.Key, nil); err != nil {
|
||||
if err := pm.WriteState(profile.Key(), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
delete(pm.knownProfiles, profile.ID)
|
||||
delete(pm.knownProfiles, profile.ID())
|
||||
return pm.writeKnownProfiles()
|
||||
}
|
||||
|
||||
@@ -578,7 +636,7 @@ func (pm *profileManager) DeleteAllProfilesForUser() error {
|
||||
|
||||
currentProfileDeleted := false
|
||||
writeKnownProfiles := func() error {
|
||||
if currentProfileDeleted || pm.currentProfile.ID == "" {
|
||||
if currentProfileDeleted || pm.currentProfile.ID() == "" {
|
||||
pm.NewProfile()
|
||||
}
|
||||
return pm.writeKnownProfiles()
|
||||
@@ -589,14 +647,14 @@ func (pm *profileManager) DeleteAllProfilesForUser() error {
|
||||
// Skip profiles we don't have access to.
|
||||
continue
|
||||
}
|
||||
if err := pm.WriteState(kp.Key, nil); err != nil {
|
||||
if err := pm.WriteState(kp.Key(), nil); err != nil {
|
||||
// Write to remove references to profiles we've already deleted, but
|
||||
// return the original error.
|
||||
writeKnownProfiles()
|
||||
return err
|
||||
}
|
||||
delete(pm.knownProfiles, kp.ID)
|
||||
if kp.ID == pm.currentProfile.ID {
|
||||
delete(pm.knownProfiles, kp.ID())
|
||||
if kp.ID() == pm.currentProfile.ID() {
|
||||
currentProfileDeleted = true
|
||||
}
|
||||
}
|
||||
@@ -633,26 +691,27 @@ func (pm *profileManager) NewProfileForUser(uid ipn.WindowsUserID) {
|
||||
|
||||
pm.prefs = defaultPrefs
|
||||
pm.updateHealth()
|
||||
pm.currentProfile = &ipn.LoginProfile{LocalUserID: uid}
|
||||
newProfile := &ipn.LoginProfile{LocalUserID: uid}
|
||||
pm.currentProfile = newProfile.View()
|
||||
}
|
||||
|
||||
// newProfileWithPrefs creates a new profile with the specified prefs and assigns
|
||||
// the specified uid as the profile owner. If switchNow is true, it switches to the
|
||||
// newly created profile immediately. It returns the newly created profile on success,
|
||||
// or an error on failure.
|
||||
func (pm *profileManager) newProfileWithPrefs(uid ipn.WindowsUserID, prefs ipn.PrefsView, switchNow bool) (*ipn.LoginProfile, error) {
|
||||
func (pm *profileManager) newProfileWithPrefs(uid ipn.WindowsUserID, prefs ipn.PrefsView, switchNow bool) (ipn.LoginProfileView, error) {
|
||||
metricNewProfile.Add(1)
|
||||
|
||||
profile := &ipn.LoginProfile{LocalUserID: uid}
|
||||
if err := pm.SetProfilePrefs(profile, prefs, ipn.NetworkProfile{}); err != nil {
|
||||
return nil, err
|
||||
profile, err := pm.setProfilePrefs(&ipn.LoginProfile{LocalUserID: uid}, prefs, ipn.NetworkProfile{})
|
||||
if err != nil {
|
||||
return ipn.LoginProfileView{}, err
|
||||
}
|
||||
if switchNow {
|
||||
pm.currentProfile = profile
|
||||
pm.prefs = prefs.AsStruct().View()
|
||||
pm.updateHealth()
|
||||
if err := pm.setProfileAsUserDefault(profile); err != nil {
|
||||
return nil, err
|
||||
return ipn.LoginProfileView{}, err
|
||||
}
|
||||
}
|
||||
return profile, nil
|
||||
@@ -711,8 +770,8 @@ func readAutoStartKey(store ipn.StateStore, goos string) (ipn.StateKey, error) {
|
||||
return ipn.StateKey(autoStartKey), nil
|
||||
}
|
||||
|
||||
func readKnownProfiles(store ipn.StateStore) (map[ipn.ProfileID]*ipn.LoginProfile, error) {
|
||||
var knownProfiles map[ipn.ProfileID]*ipn.LoginProfile
|
||||
func readKnownProfiles(store ipn.StateStore) (map[ipn.ProfileID]ipn.LoginProfileView, error) {
|
||||
var knownProfiles map[ipn.ProfileID]ipn.LoginProfileView
|
||||
prfB, err := store.ReadState(ipn.KnownProfilesStateKey)
|
||||
switch err {
|
||||
case nil:
|
||||
@@ -720,7 +779,7 @@ func readKnownProfiles(store ipn.StateStore) (map[ipn.ProfileID]*ipn.LoginProfil
|
||||
return nil, fmt.Errorf("unmarshaling known profiles: %w", err)
|
||||
}
|
||||
case ipn.ErrStateNotExist:
|
||||
knownProfiles = make(map[ipn.ProfileID]*ipn.LoginProfile)
|
||||
knownProfiles = make(map[ipn.ProfileID]ipn.LoginProfileView)
|
||||
default:
|
||||
return nil, fmt.Errorf("calling ReadState on state store: %w", err)
|
||||
}
|
||||
@@ -749,17 +808,17 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, ht *healt
|
||||
|
||||
if stateKey != "" {
|
||||
for _, v := range knownProfiles {
|
||||
if v.Key == stateKey {
|
||||
if v.Key() == stateKey {
|
||||
pm.currentProfile = v
|
||||
}
|
||||
}
|
||||
if pm.currentProfile == nil {
|
||||
if !pm.currentProfile.Valid() {
|
||||
if suf, ok := strings.CutPrefix(string(stateKey), "user-"); ok {
|
||||
pm.currentUserID = ipn.WindowsUserID(suf)
|
||||
}
|
||||
pm.NewProfile()
|
||||
} else {
|
||||
pm.currentUserID = pm.currentProfile.LocalUserID
|
||||
pm.currentUserID = pm.currentProfile.LocalUserID()
|
||||
}
|
||||
prefs, err := pm.loadSavedPrefs(stateKey)
|
||||
if err != nil {
|
||||
@@ -788,18 +847,18 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, ht *healt
|
||||
return pm, nil
|
||||
}
|
||||
|
||||
func (pm *profileManager) migrateFromLegacyPrefs(uid ipn.WindowsUserID, switchNow bool) (*ipn.LoginProfile, error) {
|
||||
func (pm *profileManager) migrateFromLegacyPrefs(uid ipn.WindowsUserID, switchNow bool) (ipn.LoginProfileView, error) {
|
||||
metricMigration.Add(1)
|
||||
sentinel, prefs, err := pm.loadLegacyPrefs(uid)
|
||||
if err != nil {
|
||||
metricMigrationError.Add(1)
|
||||
return nil, fmt.Errorf("load legacy prefs: %w", err)
|
||||
return ipn.LoginProfileView{}, fmt.Errorf("load legacy prefs: %w", err)
|
||||
}
|
||||
pm.dlogf("loaded legacy preferences; sentinel=%q", sentinel)
|
||||
profile, err := pm.newProfileWithPrefs(uid, prefs, switchNow)
|
||||
if err != nil {
|
||||
metricMigrationError.Add(1)
|
||||
return nil, fmt.Errorf("migrating _daemon profile: %w", err)
|
||||
return ipn.LoginProfileView{}, fmt.Errorf("migrating _daemon profile: %w", err)
|
||||
}
|
||||
pm.completeMigration(sentinel)
|
||||
pm.dlogf("completed legacy preferences migration with sentinel=%q", sentinel)
|
||||
@@ -809,8 +868,8 @@ func (pm *profileManager) migrateFromLegacyPrefs(uid ipn.WindowsUserID, switchNo
|
||||
|
||||
func (pm *profileManager) requiresBackfill() bool {
|
||||
return pm != nil &&
|
||||
pm.currentProfile != nil &&
|
||||
pm.currentProfile.NetworkProfile.RequiresBackfill()
|
||||
pm.currentProfile.Valid() &&
|
||||
pm.currentProfile.NetworkProfile().RequiresBackfill()
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
133
vendor/tailscale.com/ipn/ipnlocal/serve.go
generated
vendored
133
vendor/tailscale.com/ipn/ipnlocal/serve.go
generated
vendored
@@ -54,8 +54,9 @@ var ErrETagMismatch = errors.New("etag mismatch")
|
||||
var serveHTTPContextKey ctxkey.Key[*serveHTTPContext]
|
||||
|
||||
type serveHTTPContext struct {
|
||||
SrcAddr netip.AddrPort
|
||||
DestPort uint16
|
||||
SrcAddr netip.AddrPort
|
||||
ForVIPService tailcfg.ServiceName // "" means local
|
||||
DestPort uint16
|
||||
|
||||
// provides funnel-specific context, nil if not funneled
|
||||
Funnel *funnelFlow
|
||||
@@ -242,8 +243,7 @@ func (b *LocalBackend) updateServeTCPPortNetMapAddrListenersLocked(ports []uint1
|
||||
}
|
||||
|
||||
addrs := nm.GetAddresses()
|
||||
for i := range addrs.Len() {
|
||||
a := addrs.At(i)
|
||||
for _, a := range addrs.All() {
|
||||
for _, p := range ports {
|
||||
addrPort := netip.AddrPortFrom(a.Addr(), p)
|
||||
if _, ok := b.serveListeners[addrPort]; ok {
|
||||
@@ -276,6 +276,12 @@ func (b *LocalBackend) setServeConfigLocked(config *ipn.ServeConfig, etag string
|
||||
return errors.New("can't reconfigure tailscaled when using a config file; config file is locked")
|
||||
}
|
||||
|
||||
if config != nil {
|
||||
if err := config.CheckValidServicesConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
nm := b.netMap
|
||||
if nm == nil {
|
||||
return errors.New("netMap is nil")
|
||||
@@ -312,7 +318,7 @@ func (b *LocalBackend) setServeConfigLocked(config *ipn.ServeConfig, etag string
|
||||
bs = j
|
||||
}
|
||||
|
||||
profileID := b.pm.CurrentProfile().ID
|
||||
profileID := b.pm.CurrentProfile().ID()
|
||||
confKey := ipn.ServeConfigKey(profileID)
|
||||
if err := b.store.WriteState(confKey, bs); err != nil {
|
||||
return fmt.Errorf("writing ServeConfig to StateStore: %w", err)
|
||||
@@ -327,7 +333,7 @@ func (b *LocalBackend) setServeConfigLocked(config *ipn.ServeConfig, etag string
|
||||
if b.serveConfig.Valid() {
|
||||
has = b.serveConfig.Foreground().Contains
|
||||
}
|
||||
prevConfig.Foreground().Range(func(k string, v ipn.ServeConfigView) (cont bool) {
|
||||
for k := range prevConfig.Foreground().All() {
|
||||
if !has(k) {
|
||||
for _, sess := range b.notifyWatchers {
|
||||
if sess.sessionID == k {
|
||||
@@ -335,8 +341,7 @@ func (b *LocalBackend) setServeConfigLocked(config *ipn.ServeConfig, etag string
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -434,6 +439,105 @@ func (b *LocalBackend) HandleIngressTCPConn(ingressPeer tailcfg.NodeView, target
|
||||
handler(c)
|
||||
}
|
||||
|
||||
// tcpHandlerForVIPService returns a handler for a TCP connection to a VIP service
|
||||
// that is being served via the ipn.ServeConfig. It returns nil if the destination
|
||||
// address is not a VIP service or if the VIP service does not have a TCP handler set.
|
||||
func (b *LocalBackend) tcpHandlerForVIPService(dstAddr, srcAddr netip.AddrPort) (handler func(net.Conn) error) {
|
||||
b.mu.Lock()
|
||||
sc := b.serveConfig
|
||||
ipVIPServiceMap := b.ipVIPServiceMap
|
||||
b.mu.Unlock()
|
||||
|
||||
if !sc.Valid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
dport := dstAddr.Port()
|
||||
|
||||
dstSvc, ok := ipVIPServiceMap[dstAddr.Addr()]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
tcph, ok := sc.FindServiceTCP(dstSvc, dstAddr.Port())
|
||||
if !ok {
|
||||
b.logf("The destination service doesn't have a TCP handler set.")
|
||||
return nil
|
||||
}
|
||||
|
||||
if tcph.HTTPS() || tcph.HTTP() {
|
||||
hs := &http.Server{
|
||||
Handler: http.HandlerFunc(b.serveWebHandler),
|
||||
BaseContext: func(_ net.Listener) context.Context {
|
||||
return serveHTTPContextKey.WithValue(context.Background(), &serveHTTPContext{
|
||||
SrcAddr: srcAddr,
|
||||
ForVIPService: dstSvc,
|
||||
DestPort: dport,
|
||||
})
|
||||
},
|
||||
}
|
||||
if tcph.HTTPS() {
|
||||
// TODO(kevinliang10): just leaving this TLS cert creation as if we don't have other
|
||||
// hostnames, but for services this getTLSServeCetForPort will need a version that also take
|
||||
// in the hostname. How to store the TLS cert is still being discussed.
|
||||
hs.TLSConfig = &tls.Config{
|
||||
GetCertificate: b.getTLSServeCertForPort(dport, dstSvc),
|
||||
}
|
||||
return func(c net.Conn) error {
|
||||
return hs.ServeTLS(netutil.NewOneConnListener(c, nil), "", "")
|
||||
}
|
||||
}
|
||||
|
||||
return func(c net.Conn) error {
|
||||
return hs.Serve(netutil.NewOneConnListener(c, nil))
|
||||
}
|
||||
}
|
||||
|
||||
if backDst := tcph.TCPForward(); backDst != "" {
|
||||
return func(conn net.Conn) error {
|
||||
defer conn.Close()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
backConn, err := b.dialer.SystemDial(ctx, "tcp", backDst)
|
||||
cancel()
|
||||
if err != nil {
|
||||
b.logf("localbackend: failed to TCP proxy port %v (from %v) to %s: %v", dport, srcAddr, backDst, err)
|
||||
return nil
|
||||
}
|
||||
defer backConn.Close()
|
||||
if sni := tcph.TerminateTLS(); sni != "" {
|
||||
conn = tls.Server(conn, &tls.Config{
|
||||
GetCertificate: func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
pair, err := b.GetCertPEM(ctx, sni)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert, err := tls.X509KeyPair(pair.CertPEM, pair.KeyPEM)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cert, nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
errc := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := io.Copy(backConn, conn)
|
||||
errc <- err
|
||||
}()
|
||||
go func() {
|
||||
_, err := io.Copy(conn, backConn)
|
||||
errc <- err
|
||||
}()
|
||||
return <-errc
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// tcpHandlerForServe returns a handler for a TCP connection to be served via
|
||||
// the ipn.ServeConfig. The funnelFlow can be nil if this is not a funneled
|
||||
// connection.
|
||||
@@ -464,7 +568,7 @@ func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort,
|
||||
}
|
||||
if tcph.HTTPS() {
|
||||
hs.TLSConfig = &tls.Config{
|
||||
GetCertificate: b.getTLSServeCertForPort(dport),
|
||||
GetCertificate: b.getTLSServeCertForPort(dport, ""),
|
||||
}
|
||||
return func(c net.Conn) error {
|
||||
return hs.ServeTLS(netutil.NewOneConnListener(c, nil), "", "")
|
||||
@@ -544,7 +648,7 @@ func (b *LocalBackend) getServeHandler(r *http.Request) (_ ipn.HTTPHandlerView,
|
||||
b.logf("[unexpected] localbackend: no serveHTTPContext in request")
|
||||
return z, "", false
|
||||
}
|
||||
wsc, ok := b.webServerConfig(hostname, sctx.DestPort)
|
||||
wsc, ok := b.webServerConfig(hostname, sctx.ForVIPService, sctx.DestPort)
|
||||
if !ok {
|
||||
return z, "", false
|
||||
}
|
||||
@@ -902,7 +1006,7 @@ func allNumeric(s string) bool {
|
||||
return s != ""
|
||||
}
|
||||
|
||||
func (b *LocalBackend) webServerConfig(hostname string, port uint16) (c ipn.WebServerConfigView, ok bool) {
|
||||
func (b *LocalBackend) webServerConfig(hostname string, forVIPService tailcfg.ServiceName, port uint16) (c ipn.WebServerConfigView, ok bool) {
|
||||
key := ipn.HostPort(fmt.Sprintf("%s:%v", hostname, port))
|
||||
|
||||
b.mu.Lock()
|
||||
@@ -911,15 +1015,18 @@ func (b *LocalBackend) webServerConfig(hostname string, port uint16) (c ipn.WebS
|
||||
if !b.serveConfig.Valid() {
|
||||
return c, false
|
||||
}
|
||||
if forVIPService != "" {
|
||||
return b.serveConfig.FindServiceWeb(forVIPService, key)
|
||||
}
|
||||
return b.serveConfig.FindWeb(key)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) getTLSServeCertForPort(port uint16) func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
func (b *LocalBackend) getTLSServeCertForPort(port uint16, forVIPService tailcfg.ServiceName) func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
return func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if hi == nil || hi.ServerName == "" {
|
||||
return nil, errors.New("no SNI ServerName")
|
||||
}
|
||||
_, ok := b.webServerConfig(hi.ServerName, port)
|
||||
_, ok := b.webServerConfig(hi.ServerName, forVIPService, port)
|
||||
if !ok {
|
||||
return nil, errors.New("no webserver configured for name/port")
|
||||
}
|
||||
|
||||
24
vendor/tailscale.com/ipn/ipnlocal/ssh.go
generated
vendored
24
vendor/tailscale.com/ipn/ipnlocal/ssh.go
generated
vendored
@@ -24,10 +24,10 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/tailscale/golang-x-crypto/ssh"
|
||||
"go4.org/mem"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/lineread"
|
||||
"tailscale.com/util/lineiter"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
@@ -80,30 +80,32 @@ func (b *LocalBackend) getSSHUsernames(req *tailcfg.C2NSSHUsernamesRequest) (*ta
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lineread.Reader(bytes.NewReader(out), func(line []byte) error {
|
||||
for line := range lineiter.Bytes(out) {
|
||||
line = bytes.TrimSpace(line)
|
||||
if len(line) == 0 || line[0] == '_' {
|
||||
return nil
|
||||
continue
|
||||
}
|
||||
add(string(line))
|
||||
return nil
|
||||
})
|
||||
}
|
||||
default:
|
||||
lineread.File("/etc/passwd", func(line []byte) error {
|
||||
for lr := range lineiter.File("/etc/passwd") {
|
||||
line, err := lr.Value()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
line = bytes.TrimSpace(line)
|
||||
if len(line) == 0 || line[0] == '#' || line[0] == '_' {
|
||||
return nil
|
||||
continue
|
||||
}
|
||||
if mem.HasSuffix(mem.B(line), mem.S("/nologin")) ||
|
||||
mem.HasSuffix(mem.B(line), mem.S("/false")) {
|
||||
return nil
|
||||
continue
|
||||
}
|
||||
colon := bytes.IndexByte(line, ':')
|
||||
if colon != -1 {
|
||||
add(string(line[:colon]))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
14
vendor/tailscale.com/ipn/ipnlocal/web_client.go
generated
vendored
14
vendor/tailscale.com/ipn/ipnlocal/web_client.go
generated
vendored
@@ -17,7 +17,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/client/web"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/netutil"
|
||||
@@ -36,16 +36,16 @@ type webClient struct {
|
||||
|
||||
server *web.Server // or nil, initialized lazily
|
||||
|
||||
// lc optionally specifies a LocalClient to use to connect
|
||||
// lc optionally specifies a local.Client to use to connect
|
||||
// to the localapi for this tailscaled instance.
|
||||
// If nil, a default is used.
|
||||
lc *tailscale.LocalClient
|
||||
lc *local.Client
|
||||
}
|
||||
|
||||
// ConfigureWebClient configures b.web prior to use.
|
||||
// Specifially, it sets b.web.lc to the provided LocalClient.
|
||||
// Specifially, it sets b.web.lc to the provided local.Client.
|
||||
// If provided as nil, b.web.lc is cleared out.
|
||||
func (b *LocalBackend) ConfigureWebClient(lc *tailscale.LocalClient) {
|
||||
func (b *LocalBackend) ConfigureWebClient(lc *local.Client) {
|
||||
b.webClient.mu.Lock()
|
||||
defer b.webClient.mu.Unlock()
|
||||
b.webClient.lc = lc
|
||||
@@ -121,8 +121,8 @@ func (b *LocalBackend) updateWebClientListenersLocked() {
|
||||
}
|
||||
|
||||
addrs := b.netMap.GetAddresses()
|
||||
for i := range addrs.Len() {
|
||||
addrPort := netip.AddrPortFrom(addrs.At(i).Addr(), webClientPort)
|
||||
for _, pfx := range addrs.All() {
|
||||
addrPort := netip.AddrPortFrom(pfx.Addr(), webClientPort)
|
||||
if _, ok := b.webClientListeners[addrPort]; ok {
|
||||
continue // already listening
|
||||
}
|
||||
|
||||
4
vendor/tailscale.com/ipn/ipnlocal/web_client_stub.go
generated
vendored
4
vendor/tailscale.com/ipn/ipnlocal/web_client_stub.go
generated
vendored
@@ -9,14 +9,14 @@ import (
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/client/local"
|
||||
)
|
||||
|
||||
const webClientPort = 5252
|
||||
|
||||
type webClient struct{}
|
||||
|
||||
func (b *LocalBackend) ConfigureWebClient(lc *tailscale.LocalClient) {}
|
||||
func (b *LocalBackend) ConfigureWebClient(lc *local.Client) {}
|
||||
|
||||
func (b *LocalBackend) webClientGetOrInit() error {
|
||||
return errors.New("not implemented")
|
||||
|
||||
Reference in New Issue
Block a user