Compare commits

...

7 Commits

Author SHA1 Message Date
Jonathan Nobels 95a957cdd7 VERSION.txt: this is 1.92.2
Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
2 months ago
Raj Singh 7508ea4760 cmd/derper: add GCP Certificate Manager support (#18161)
Add --certmode=gcp for using Google Cloud Certificate Manager's
public CA instead of Let's Encrypt. GCP requires External Account
Binding (EAB) credentials for ACME registration, so this adds
--acme-eab-kid and --acme-eab-key flags.

The EAB key accepts both base64url and standard base64 encoding
to support both ACME spec format and gcloud output.

Fixes tailscale/corp#34881

Signed-off-by: Raj Singh <raj@tailscale.com>
Co-authored-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 8eda947530)
2 months ago
Jonathan Nobels 2078eb56f3 VERSION.txt: this is v1.92.1
Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
2 months ago
Brad Fitzpatrick 826f16dc87
go.toolchain.rev: update to Go 1.25.5 (#18123) (#18134)
Updates #18122


(cherry picked from commit 7bc25f77f4)

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
Co-authored-by: Andrew Lytvynov <awly@tailscale.com>
2 months ago
Brad Fitzpatrick 28e8e6b25f wgengine: fix TSMP/ICMP callback leak
Fixes #18112

Change-Id: I85d5c482b01673799d51faeb6cb0579903597502
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit b8c58ca7c1)
2 months ago
Nick Khyl fd7dd6433f ipn/ipnlocal: fix LocalBackend deadlock when packet arrives during profile switch (#18126)
If a packet arrives while WireGuard is being reconfigured with b.mu held, such as during a profile switch,
calling back into (*LocalBackend).GetPeerAPIPort from (*Wrapper).filterPacketInboundFromWireGuard
may deadlock when it tries to acquire b.mu.

This occurs because a peer cannot be removed while an inbound packet is being processed.
The reconfig and profile switch wait for (*Peer).RoutineSequentialReceiver to return, but it never finishes
because GetPeerAPIPort needs b.mu, which the waiting goroutine already holds.

In this PR, we make peerAPIPorts a new syncs.AtomicValue field that is written with b.mu held
but can be read by GetPeerAPIPort without holding the mutex, which fixes the deadlock.

There might be other long-term ways to address the issue, such as moving peer API listeners
from LocalBackend to nodeBackend so they can be accessed without holding b.mu,
but these changes are too large and risky at this stage in the v1.92 release cycle.

Updates #18124

Signed-off-by: Nick Khyl <nickk@tailscale.com>
(cherry picked from commit 557457f3c2)
2 months ago
Jonathan Nobels 822adaa259 VERSION.txt: this is v1.92.0
Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
2 months ago

@ -1 +1 @@
1.91.0
1.92.2

@ -11,6 +11,7 @@ import (
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
@ -24,6 +25,7 @@ import (
"regexp"
"time"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"
"tailscale.com/tailcfg"
)
@ -42,17 +44,33 @@ type certProvider interface {
HTTPHandler(fallback http.Handler) http.Handler
}
func certProviderByCertMode(mode, dir, hostname string) (certProvider, error) {
func certProviderByCertMode(mode, dir, hostname, eabKID, eabKey string) (certProvider, error) {
if dir == "" {
return nil, errors.New("missing required --certdir flag")
}
switch mode {
case "letsencrypt":
case "letsencrypt", "gcp":
certManager := &autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(hostname),
Cache: autocert.DirCache(dir),
}
if mode == "gcp" {
if eabKID == "" || eabKey == "" {
return nil, errors.New("--certmode=gcp requires --acme-eab-kid and --acme-eab-key flags")
}
keyBytes, err := decodeEABKey(eabKey)
if err != nil {
return nil, err
}
certManager.Client = &acme.Client{
DirectoryURL: "https://dv.acme-v02.api.pki.goog/directory",
}
certManager.ExternalAccountBinding = &acme.ExternalAccountBinding{
KID: eabKID,
Key: keyBytes,
}
}
if hostname == "derp.tailscale.com" {
certManager.HostPolicy = prodAutocertHostPolicy
certManager.Email = "security@tailscale.com"
@ -209,3 +227,17 @@ func createSelfSignedIPCert(crtPath, keyPath, ipStr string) (*tls.Certificate, e
}
return &tlsCert, nil
}
// decodeEABKey decodes a base64-encoded EAB key.
// It accepts both standard base64 (with padding) and base64url (without padding).
func decodeEABKey(s string) ([]byte, error) {
// Try base64url first (no padding), then standard base64 (with padding).
// This handles both ACME spec format and gcloud output format.
if b, err := base64.RawURLEncoding.DecodeString(s); err == nil {
return b, nil
}
if b, err := base64.StdEncoding.DecodeString(s); err == nil {
return b, nil
}
return nil, errors.New("invalid base64 encoding for EAB key")
}

@ -91,7 +91,7 @@ func TestCertIP(t *testing.T) {
t.Fatalf("Error closing key.pem: %v", err)
}
cp, err := certProviderByCertMode("manual", dir, hostname)
cp, err := certProviderByCertMode("manual", dir, hostname, "", "")
if err != nil {
t.Fatal(err)
}
@ -169,3 +169,37 @@ func TestPinnedCertRawIP(t *testing.T) {
}
defer connClose.Close()
}
func TestGCPCertMode(t *testing.T) {
dir := t.TempDir()
// Missing EAB credentials
_, err := certProviderByCertMode("gcp", dir, "test.example.com", "", "")
if err == nil {
t.Fatal("expected error when EAB credentials are missing")
}
// Invalid base64
_, err = certProviderByCertMode("gcp", dir, "test.example.com", "kid", "not-valid!")
if err == nil {
t.Fatal("expected error for invalid base64")
}
// Valid base64url (no padding)
cp, err := certProviderByCertMode("gcp", dir, "test.example.com", "kid", "dGVzdC1rZXk")
if err != nil {
t.Fatalf("base64url: %v", err)
}
if cp == nil {
t.Fatal("base64url: nil certProvider")
}
// Valid standard base64 (with padding, gcloud format)
cp, err = certProviderByCertMode("gcp", dir, "test.example.com", "kid", "dGVzdC1rZXk=")
if err != nil {
t.Fatalf("base64: %v", err)
}
if cp == nil {
t.Fatal("base64: nil certProvider")
}
}

@ -168,7 +168,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/version from tailscale.com/cmd/derper+
tailscale.com/version/distro from tailscale.com/envknob+
tailscale.com/wgengine/filter/filtertype from tailscale.com/types/netmap
golang.org/x/crypto/acme from golang.org/x/crypto/acme/autocert
golang.org/x/crypto/acme from golang.org/x/crypto/acme/autocert+
golang.org/x/crypto/acme/autocert from tailscale.com/cmd/derper
golang.org/x/crypto/argon2 from tailscale.com/tka
golang.org/x/crypto/blake2b from golang.org/x/crypto/argon2+

@ -60,9 +60,11 @@ var (
httpPort = flag.Int("http-port", 80, "The port on which to serve HTTP. Set to -1 to disable. The listener is bound to the same IP (if any) as specified in the -a flag.")
stunPort = flag.Int("stun-port", 3478, "The UDP port on which to serve STUN. The listener is bound to the same IP (if any) as specified in the -a flag.")
configPath = flag.String("c", "", "config file path")
certMode = flag.String("certmode", "letsencrypt", "mode for getting a cert. possible options: manual, letsencrypt")
certDir = flag.String("certdir", tsweb.DefaultCertDir("derper-certs"), "directory to store LetsEncrypt certs, if addr's port is :443")
hostname = flag.String("hostname", "derp.tailscale.com", "LetsEncrypt host name, if addr's port is :443. When --certmode=manual, this can be an IP address to avoid SNI checks")
certMode = flag.String("certmode", "letsencrypt", "mode for getting a cert. possible options: manual, letsencrypt, gcp")
certDir = flag.String("certdir", tsweb.DefaultCertDir("derper-certs"), "directory to store ACME (e.g. LetsEncrypt) certs, if addr's port is :443")
hostname = flag.String("hostname", "derp.tailscale.com", "TLS host name for certs, if addr's port is :443. When --certmode=manual, this can be an IP address to avoid SNI checks")
acmeEABKid = flag.String("acme-eab-kid", "", "ACME External Account Binding (EAB) Key ID (required for --certmode=gcp)")
acmeEABKey = flag.String("acme-eab-key", "", "ACME External Account Binding (EAB) HMAC key, base64-encoded (required for --certmode=gcp)")
runSTUN = flag.Bool("stun", true, "whether to run a STUN server. It will bind to the same IP (if any) as the --addr flag value.")
runDERP = flag.Bool("derp", true, "whether to run a DERP server. The only reason to set this false is if you're decommissioning a server but want to keep its bootstrap DNS functionality still running.")
flagHome = flag.String("home", "", "what to serve at the root path. It may be left empty (the default, for a default homepage), \"blank\" for a blank page, or a URL to redirect to")
@ -343,7 +345,7 @@ func main() {
if serveTLS {
log.Printf("derper: serving on %s with TLS", *addr)
var certManager certProvider
certManager, err = certProviderByCertMode(*certMode, *certDir, *hostname)
certManager, err = certProviderByCertMode(*certMode, *certDir, *hostname, *acmeEABKid, *acmeEABKey)
if err != nil {
log.Fatalf("derper: can not start cert provider: %v", err)
}

@ -1,6 +1,6 @@
module tailscale.com
go 1.25.3
go 1.25.5
require (
filippo.io/mkcert v1.4.4

@ -1 +1 @@
5c01b77ad0d27a8bd4ef89ef7e713fd7043c5a91
0bab982699fa5903259ba9b4cba3e5fd6cb3baf2

@ -1 +1 @@
sha256-2TYziJLJrFOW2FehhahKficnDACJEwjuvVYyeQZbrcc=
sha256-fBezkBGRHCnfJiOUmMMqBCPCqjlGC4F6KEt5h1JhsCg=

@ -1 +1 @@
1.25.3
1.25.5

@ -245,6 +245,8 @@ type LocalBackend struct {
// to prevent state changes while invoking callbacks.
extHost *ExtensionHost
peerAPIPorts syncs.AtomicValue[map[netip.Addr]int] // can be read without b.mu held; TODO(nickkhyl): remove or move to nodeBackend?
// The mutex protects the following elements.
mu syncs.Mutex
@ -295,8 +297,8 @@ type LocalBackend struct {
authActor ipnauth.Actor // an actor who called [LocalBackend.StartLoginInteractive] last, or nil; TODO(nickkhyl): move to nodeBackend
egg bool
prevIfState *netmon.State
peerAPIServer *peerAPIServer // or nil
peerAPIListeners []*peerAPIListener
peerAPIServer *peerAPIServer // or nil
peerAPIListeners []*peerAPIListener // TODO(nickkhyl): move to nodeBackend
loginFlags controlclient.LoginFlags
notifyWatchers map[string]*watchSession // by session ID
lastStatusTime time.Time // status.AsOf value of the last processed status update
@ -4701,14 +4703,8 @@ func (b *LocalBackend) GetPeerAPIPort(ip netip.Addr) (port uint16, ok bool) {
if !buildfeatures.HasPeerAPIServer {
return 0, false
}
b.mu.Lock()
defer b.mu.Unlock()
for _, pln := range b.peerAPIListeners {
if pln.ip == ip {
return uint16(pln.port), true
}
}
return 0, false
portInt, ok := b.peerAPIPorts.Load()[ip]
return uint16(portInt), ok
}
// handlePeerAPIConn serves an already-accepted connection c.
@ -5200,6 +5196,7 @@ func (b *LocalBackend) closePeerAPIListenersLocked() {
pln.Close()
}
b.peerAPIListeners = nil
b.peerAPIPorts.Store(nil)
}
// peerAPIListenAsync is whether the operating system requires that we
@ -5272,6 +5269,7 @@ func (b *LocalBackend) initPeerAPIListenerLocked() {
b.peerAPIServer = ps
isNetstack := b.sys.IsNetstack()
peerAPIPorts := make(map[netip.Addr]int)
for i, a := range addrs.All() {
var ln net.Listener
var err error
@ -5304,7 +5302,9 @@ func (b *LocalBackend) initPeerAPIListenerLocked() {
b.logf("peerapi: serving on %s", pln.urlStr)
go pln.serve()
b.peerAPIListeners = append(b.peerAPIListeners, pln)
peerAPIPorts[a.Addr()] = pln.port
}
b.peerAPIPorts.Store(peerAPIPorts)
b.goTracker.Go(b.doSetHostinfoFilterServices)
}

@ -451,6 +451,7 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
cb := e.pongCallback[pong.Data]
e.logf("wgengine: got TSMP pong %02x, peerAPIPort=%v; cb=%v", pong.Data, pong.PeerAPIPort, cb != nil)
if cb != nil {
delete(e.pongCallback, pong.Data)
go cb(pong)
}
}
@ -464,6 +465,7 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
// We didn't swallow it, so let it flow to the host.
return false
}
delete(e.icmpEchoResponseCallback, idSeq)
e.logf("wgengine: got diagnostic ICMP response %02x", idSeq)
go cb()
return true

Loading…
Cancel
Save