feat(tsconnect): add getCert, listenTLS, setFunnel + fix TLS cert for WASM
Enable ACME TLS certificates on js/wasm by dropping the !js build tag from cert.go and routing storage through the state store. Add getCert, listenTLS, and setFunnel WASM bindings with a combinedTLSListener that merges Funnel ingress and direct tailnet connections. Notify the control plane immediately after serve config changes to accelerate Funnel DNS provisioning. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+17
-1
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !js && !ts_omit_acme
|
||||
//go:build !ts_omit_acme
|
||||
|
||||
package ipnlocal
|
||||
|
||||
@@ -302,6 +302,9 @@ var errCertExpired = errors.New("cert expired")
|
||||
var testX509Roots *x509.CertPool // set non-nil by tests
|
||||
|
||||
func (b *LocalBackend) getCertStore() (certStore, error) {
|
||||
if runtime.GOOS == "js" {
|
||||
return certStateStore{StateStore: b.store}, nil
|
||||
}
|
||||
switch b.store.(type) {
|
||||
case *store.FileStore:
|
||||
case *mem.Store:
|
||||
@@ -333,6 +336,16 @@ func (b *LocalBackend) ConfigureCertsForTest(getCert func(hostname string) (*TLS
|
||||
b.mu.Unlock()
|
||||
}
|
||||
|
||||
// SetACMEHTTPClient sets a custom HTTP client for ACME certificate operations.
|
||||
// On js/wasm, this can be used to route requests through the Tailscale network
|
||||
// stack to bypass browser CORS if Let's Encrypt endpoints fail preflight.
|
||||
// A nil value (the default) uses the standard http.DefaultClient.
|
||||
func (b *LocalBackend) SetACMEHTTPClient(c *http.Client) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.acmeHTTPClient = c
|
||||
}
|
||||
|
||||
// certFileStore implements certStore by storing the cert & key files in the named directory.
|
||||
type certFileStore struct {
|
||||
dir string
|
||||
@@ -550,6 +563,9 @@ var getCertPEM = func(ctx context.Context, b *LocalBackend, cs certStore, logf l
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.mu.Lock()
|
||||
ac.HTTPClient = b.acmeHTTPClient
|
||||
b.mu.Unlock()
|
||||
|
||||
if !isDefaultDirectoryURL(ac.DirectoryURL) {
|
||||
logf("acme: using Directory URL %q", ac.DirectoryURL)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build js || ts_omit_acme
|
||||
//go:build ts_omit_acme
|
||||
|
||||
package ipnlocal
|
||||
|
||||
|
||||
@@ -412,6 +412,10 @@ type LocalBackend struct {
|
||||
// See [LocalBackend.ConfigureCertsForTest].
|
||||
getCertForTest func(hostname string) (*TLSCertKeyPair, error)
|
||||
|
||||
// acmeHTTPClient, if non-nil, is used for all ACME HTTP requests instead
|
||||
// of http.DefaultClient. Set via SetACMEHTTPClient before first cert use.
|
||||
acmeHTTPClient *http.Client
|
||||
|
||||
// existsPendingAuthReconfig tracks if a goroutine is waiting to
|
||||
// acquire [LocalBackend]'s mutex inside of [LocalBackend.AuthReconfig].
|
||||
// It is used to prevent goroutines from piling up to do the same
|
||||
|
||||
@@ -393,6 +393,11 @@ func (b *LocalBackend) setServeConfigLocked(config *ipn.ServeConfig, etag string
|
||||
}
|
||||
}
|
||||
|
||||
// Notify the control plane immediately so that changes to IngressEnabled /
|
||||
// WireIngress (required for Funnel DNS provisioning) are not delayed until
|
||||
// the next periodic heartbeat.
|
||||
b.authReconfigLocked()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user