Skip to content

Commit

Permalink
Merge tag 'v1.38.3' into sunos-1.38
Browse files Browse the repository at this point in the history
Release 1.38.3
  • Loading branch information
nshalman committed Mar 30, 2023
2 parents 7db9e12 + 47ebe6f commit d02885d
Show file tree
Hide file tree
Showing 9 changed files with 111 additions and 50 deletions.
2 changes: 1 addition & 1 deletion VERSION.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.38.2
1.38.3
2 changes: 1 addition & 1 deletion cmd/tailscale/cli/funnel.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ var funnelCmd = newFunnelCommand(&serveEnv{lc: &localClient})
func newFunnelCommand(e *serveEnv) *ffcli.Command {
return &ffcli.Command{
Name: "funnel",
ShortHelp: "[ALPHA] turn Tailscale Funnel on or off",
ShortHelp: "[BETA] turn Tailscale Funnel on or off",
ShortUsage: strings.TrimSpace(`
funnel <serve-port> {on|off}
funnel status [--json]
Expand Down
4 changes: 2 additions & 2 deletions cmd/tailscale/cli/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ var serveCmd = newServeCommand(&serveEnv{lc: &localClient})
func newServeCommand(e *serveEnv) *ffcli.Command {
return &ffcli.Command{
Name: "serve",
ShortHelp: "[ALPHA] Serve from your Tailscale node",
ShortHelp: "[BETA] Serve from your Tailscale node",
ShortUsage: strings.TrimSpace(`
serve https:<port> <mount-point> <source> [off]
serve tcp:<port> tcp://localhost:<local-port> [off]
serve tls-terminated-tcp:<port> tcp://localhost:<local-port> [off]
serve status [--json]
`),
LongHelp: strings.TrimSpace(`
*** ALPHA; all of this is subject to change ***
*** BETA; all of this is subject to change ***
The 'tailscale serve' set of commands allows you to serve
content and local servers from your Tailscale node to
Expand Down
2 changes: 1 addition & 1 deletion cmd/tailscaled/depaware.txt
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/ipn/ipnstate from tailscale.com/control/controlclient+
tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
tailscale.com/ipn/store from tailscale.com/cmd/tailscaled
tailscale.com/ipn/store from tailscale.com/cmd/tailscaled+
L tailscale.com/ipn/store/awsstore from tailscale.com/ipn/store
L tailscale.com/ipn/store/kubestore from tailscale.com/ipn/store
tailscale.com/ipn/store/mem from tailscale.com/ipn/store+
Expand Down
105 changes: 76 additions & 29 deletions ipn/ipnlocal/cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,13 @@ import (
"time"

"golang.org/x/crypto/acme"
"tailscale.com/atomicfile"
"tailscale.com/envknob"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/ipn/store"
"tailscale.com/ipn/store/mem"
"tailscale.com/types/logger"
"tailscale.com/version"
"tailscale.com/version/distro"
Expand Down Expand Up @@ -82,11 +85,6 @@ func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertK
return nil, errors.New("invalid domain")
}
logf := logger.WithPrefix(b.logf, fmt.Sprintf("cert(%q): ", domain))
dir, err := b.certDir()
if err != nil {
logf("failed to get certDir: %v", err)
return nil, err
}
now := time.Now()
traceACME := func(v any) {
if !acmeDebug() {
Expand All @@ -96,25 +94,30 @@ func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertK
log.Printf("acme %T: %s", v, j)
}

if pair, err := b.getCertPEMCached(dir, domain, now); err == nil {
cs, err := b.getCertStore()
if err != nil {
return nil, err
}

if pair, err := getCertPEMCached(cs, domain, now); err == nil {
future := now.AddDate(0, 0, 14)
if b.shouldStartDomainRenewal(dir, domain, future) {
if b.shouldStartDomainRenewal(cs, domain, future) {
logf("starting async renewal")
// Start renewal in the background.
go b.getCertPEM(context.Background(), logf, traceACME, dir, domain, future)
go b.getCertPEM(context.Background(), cs, logf, traceACME, domain, future)
}
return pair, nil
}

pair, err := b.getCertPEM(ctx, logf, traceACME, dir, domain, now)
pair, err := b.getCertPEM(ctx, cs, logf, traceACME, domain, now)
if err != nil {
logf("getCertPEM: %v", err)
return nil, err
}
return pair, nil
}

func (b *LocalBackend) shouldStartDomainRenewal(dir, domain string, future time.Time) bool {
func (b *LocalBackend) shouldStartDomainRenewal(cs certStore, domain string, future time.Time) bool {
renewMu.Lock()
defer renewMu.Unlock()
now := time.Now()
Expand All @@ -124,7 +127,7 @@ func (b *LocalBackend) shouldStartDomainRenewal(dir, domain string, future time.
return false
}
lastRenewCheck[domain] = now
_, err := b.getCertPEMCached(dir, domain, future)
_, err := getCertPEMCached(cs, domain, future)
return errors.Is(err, errCertExpired)
}

Expand All @@ -140,15 +143,32 @@ type certStore interface {
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
}

var errCertExpired = errors.New("cert expired")

func (b *LocalBackend) getCertStore(dir string) certStore {
if hostinfo.GetEnvType() == hostinfo.Kubernetes && dir == "/tmp" {
return certStateStore{StateStore: b.store}
func (b *LocalBackend) getCertStore() (certStore, error) {
switch b.store.(type) {
case *store.FileStore:
case *mem.Store:
default:
if hostinfo.GetEnvType() == hostinfo.Kubernetes {
// We're running in Kubernetes with a custom StateStore,
// use that instead of the cert directory.
// TODO(maisem): expand this to other environments?
return certStateStore{StateStore: b.store}, nil
}
}
return certFileStore{dir: dir}
dir, err := b.certDir()
if err != nil {
return nil, err
}
return certFileStore{dir: dir}, nil
}

// certFileStore implements certStore by storing the cert & key files in the named directory.
Expand All @@ -160,6 +180,25 @@ type certFileStore struct {
testRoots *x509.CertPool
}

const acmePEMName = "acme-account.key.pem"

func (f certFileStore) ACMEKey() ([]byte, error) {
pemName := filepath.Join(f.dir, acmePEMName)
v, err := os.ReadFile(pemName)
if err != nil {
if os.IsNotExist(err) {
return nil, ipn.ErrStateNotExist
}
return nil, err
}
return v, nil
}

func (f certFileStore) WriteACMEKey(b []byte) error {
pemName := filepath.Join(f.dir, acmePEMName)
return atomicfile.WriteFile(pemName, b, 0600)
}

func (f certFileStore) Read(domain string, now time.Time) (*TLSCertKeyPair, error) {
certPEM, err := os.ReadFile(certFile(f.dir, domain))
if err != nil {
Expand All @@ -182,11 +221,11 @@ func (f certFileStore) Read(domain string, now time.Time) (*TLSCertKeyPair, erro
}

func (f certFileStore) WriteCert(domain string, cert []byte) error {
return os.WriteFile(certFile(f.dir, domain), cert, 0644)
return atomicfile.WriteFile(certFile(f.dir, domain), cert, 0644)
}

func (f certFileStore) WriteKey(domain string, key []byte) error {
return os.WriteFile(keyFile(f.dir, domain), key, 0600)
return atomicfile.WriteFile(keyFile(f.dir, domain), key, 0600)
}

// certStateStore implements certStore by storing the cert & key files in an ipn.StateStore.
Expand Down Expand Up @@ -221,6 +260,14 @@ func (s certStateStore) WriteKey(domain string, key []byte) error {
return s.WriteState(ipn.StateKey(domain+".key"), key)
}

func (s certStateStore) ACMEKey() ([]byte, error) {
return s.ReadState(ipn.StateKey(acmePEMName))
}

func (s certStateStore) WriteACMEKey(key []byte) error {
return s.WriteState(ipn.StateKey(acmePEMName), key)
}

// TLSCertKeyPair is a TLS public and private key, and whether they were obtained
// from cache or freshly obtained.
type TLSCertKeyPair struct {
Expand All @@ -236,26 +283,26 @@ func certFile(dir, domain string) string { return filepath.Join(dir, domain+".cr
// domain exists on disk in dir that is valid at the provided now time.
// If the keypair is expired, it returns errCertExpired.
// If the keypair doesn't exist, it returns ipn.ErrStateNotExist.
func (b *LocalBackend) getCertPEMCached(dir, domain string, now time.Time) (p *TLSCertKeyPair, err error) {
func getCertPEMCached(cs certStore, domain string, now time.Time) (p *TLSCertKeyPair, err error) {
if !validLookingCertDomain(domain) {
// Before we read files from disk using it, validate it's halfway
// reasonable looking.
return nil, fmt.Errorf("invalid domain %q", domain)
}
return b.getCertStore(dir).Read(domain, now)
return cs.Read(domain, now)
}

func (b *LocalBackend) getCertPEM(ctx context.Context, logf logger.Logf, traceACME func(any), dir, domain string, now time.Time) (*TLSCertKeyPair, error) {
func (b *LocalBackend) getCertPEM(ctx context.Context, cs certStore, logf logger.Logf, traceACME func(any), domain string, now time.Time) (*TLSCertKeyPair, error) {
acmeMu.Lock()
defer acmeMu.Unlock()

if p, err := b.getCertPEMCached(dir, domain, now); err == nil {
if p, err := getCertPEMCached(cs, domain, now); err == nil {
return p, nil
} else if !errors.Is(err, ipn.ErrStateNotExist) && !errors.Is(err, errCertExpired) {
return nil, err
}

key, err := acmeKey(dir)
key, err := acmeKey(cs)
if err != nil {
return nil, fmt.Errorf("acmeKey: %w", err)
}
Expand Down Expand Up @@ -366,8 +413,7 @@ func (b *LocalBackend) getCertPEM(ctx context.Context, logf logger.Logf, traceAC
if err := encodeECDSAKey(&privPEM, certPrivKey); err != nil {
return nil, err
}
certStore := b.getCertStore(dir)
if err := certStore.WriteKey(domain, privPEM.Bytes()); err != nil {
if err := cs.WriteKey(domain, privPEM.Bytes()); err != nil {
return nil, err
}

Expand All @@ -390,7 +436,7 @@ func (b *LocalBackend) getCertPEM(ctx context.Context, logf logger.Logf, traceAC
return nil, err
}
}
if err := certStore.WriteCert(domain, certPEM.Bytes()); err != nil {
if err := cs.WriteCert(domain, certPEM.Bytes()); err != nil {
return nil, err
}

Expand Down Expand Up @@ -444,14 +490,15 @@ func parsePrivateKey(der []byte) (crypto.Signer, error) {
return nil, errors.New("acme/autocert: failed to parse private key")
}

func acmeKey(dir string) (crypto.Signer, error) {
pemName := filepath.Join(dir, "acme-account.key.pem")
if v, err := os.ReadFile(pemName); err == nil {
func acmeKey(cs certStore) (crypto.Signer, error) {
if v, err := cs.ACMEKey(); err == nil {
priv, _ := pem.Decode(v)
if priv == nil || !strings.Contains(priv.Type, "PRIVATE") {
return nil, errors.New("acme/autocert: invalid account key found in cache")
}
return parsePrivateKey(priv.Bytes)
} else if err != nil && !errors.Is(err, ipn.ErrStateNotExist) {
return nil, err
}

privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
Expand All @@ -462,7 +509,7 @@ func acmeKey(dir string) (crypto.Signer, error) {
if err := encodeECDSAKey(&pemBuf, privKey); err != nil {
return nil, err
}
if err := os.WriteFile(pemName, pemBuf.Bytes(), 0600); err != nil {
if err := cs.WriteACMEKey(pemBuf.Bytes()); err != nil {
return nil, err
}
return privKey, nil
Expand Down
2 changes: 1 addition & 1 deletion ipn/ipnlocal/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, diale
statsLogf: logger.LogOnChange(logf, 5*time.Minute, time.Now),
e: e,
pm: pm,
store: pm.Store(),
store: store,
dialer: dialer,
backendLogID: logid,
state: ipn.NoState,
Expand Down
37 changes: 25 additions & 12 deletions ipn/ipnlocal/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -439,18 +439,26 @@ func (b *LocalBackend) proxyHandlerForBackend(backend string) (*httputil.Reverse
if err != nil {
return nil, fmt.Errorf("invalid url %s: %w", targetURL, err)
}
rp := httputil.NewSingleHostReverseProxy(u)
rp.Transport = &http.Transport{
DialContext: b.dialer.SystemDial,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: insecure,
rp := &httputil.ReverseProxy{
Rewrite: func(r *httputil.ProxyRequest) {
r.SetURL(u)
r.Out.Host = r.In.Host
if c, ok := r.Out.Context().Value(serveHTTPContextKey{}).(*serveHTTPContext); ok {
r.Out.Header.Set("X-Forwarded-For", c.SrcAddr.Addr().String())
}
},
Transport: &http.Transport{
DialContext: b.dialer.SystemDial,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: insecure,
},
// Values for the following parameters have been copied from http.DefaultTransport.
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
},
// Values for the following parameters have been copied from http.DefaultTransport.
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
return rp, nil
}
Expand All @@ -476,7 +484,12 @@ func (b *LocalBackend) serveWebHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "unknown proxy destination", http.StatusInternalServerError)
return
}
p.(http.Handler).ServeHTTP(w, r)
h := p.(http.Handler)
// Trim the mount point from the URL path before proxying. (#6571)
if r.URL.Path != "/" {
h = http.StripPrefix(strings.TrimSuffix(mountPoint, "/"), h)
}
h.ServeHTTP(w, r)
return
}

Expand Down
4 changes: 2 additions & 2 deletions ipn/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ func (sc *ServeConfig) IsFunnelOn() bool {
// CheckFunnelAccess checks whether Funnel access is allowed for the given node
// and port.
// It checks:
// 1. an invite was used to join the Funnel alpha
// 1. Funnel is enabled on the Tailnet
// 2. HTTPS is enabled on the Tailnet
// 3. the node has the "funnel" nodeAttr
// 4. the port is allowed for Funnel
Expand All @@ -190,7 +190,7 @@ func (sc *ServeConfig) IsFunnelOn() bool {
// Funnel.
func CheckFunnelAccess(port uint16, nodeAttrs []string) error {
if slices.Contains(nodeAttrs, tailcfg.CapabilityWarnFunnelNoInvite) {
return errors.New("Funnel not available; an invite is required to join the alpha. See https://tailscale.com/s/no-funnel.")
return errors.New("Funnel not enabled; See https://tailscale.com/s/no-funnel.")
}
if slices.Contains(nodeAttrs, tailcfg.CapabilityWarnFunnelNoHTTPS) {
return errors.New("Funnel not available; HTTPS must be enabled. See https://tailscale.com/s/https.")
Expand Down
3 changes: 2 additions & 1 deletion tailcfg/tailcfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -1822,7 +1822,8 @@ const (

// Funnel warning capabilities used for reporting errors to the user.

// CapabilityWarnFunnelNoInvite indicates an invite has not been accepted for the Funnel alpha.
// CapabilityWarnFunnelNoInvite indicates whether Funnel is enabled for the tailnet.
// NOTE: In transition from Alpha to Beta, this capability is being reused as the enablement.
CapabilityWarnFunnelNoInvite = "https://tailscale.com/cap/warn-funnel-no-invite"

// CapabilityWarnFunnelNoHTTPS indicates HTTPS has not been enabled for the tailnet.
Expand Down

0 comments on commit d02885d

Please sign in to comment.