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:
2026-05-06 11:19:25 +00:00
parent a6b286b414
commit 9fd2f3bbf4
5 changed files with 238 additions and 9 deletions
+17 -1
View File
@@ -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 -1
View File
@@ -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
+4
View File
@@ -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
+5
View File
@@ -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
}