Files
netbird/proxy/internal/acme/manager.go
mlsmaycon 99e6b1eda4 attempt to trigger ssl before first request
1. When AddDomain() is called (when proxy receives a new mapping), it now spawns a goroutine to prefetch the certificate
  2. prefetchCertificate() creates a synthetic tls.ClientHelloInfo and calls GetCertificate() to trigger the ACME flow
  3. The certificate is cached by autocert.DirCache, so subsequent real requests will use the cached cert
  4. If the cert is already cached (e.g., proxy restart), GetCertificate just returns it without making ACME requests
2026-02-08 10:59:36 +01:00

120 lines
3.1 KiB
Go

package acme
import (
"context"
"crypto/tls"
"fmt"
"net"
"sync"
"time"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"
)
type certificateNotifier interface {
NotifyCertificateIssued(ctx context.Context, accountID, reverseProxyID, domain string) error
}
type Manager struct {
*autocert.Manager
domainsMux sync.RWMutex
domains map[string]struct {
accountID string
reverseProxyID string
}
certNotifier certificateNotifier
}
func NewManager(certDir, acmeURL string, notifier certificateNotifier) *Manager {
mgr := &Manager{
domains: make(map[string]struct {
accountID string
reverseProxyID string
}),
certNotifier: notifier,
}
mgr.Manager = &autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: mgr.hostPolicy,
Cache: autocert.DirCache(certDir),
Client: &acme.Client{
DirectoryURL: acmeURL,
},
}
return mgr
}
func (mgr *Manager) hostPolicy(ctx context.Context, domain string) error {
mgr.domainsMux.RLock()
info, exists := mgr.domains[domain]
mgr.domainsMux.RUnlock()
if !exists {
return fmt.Errorf("unknown domain %q", domain)
}
if mgr.certNotifier != nil {
if err := mgr.certNotifier.NotifyCertificateIssued(ctx, info.accountID, info.reverseProxyID, domain); err != nil {
log.Warnf("failed to notify certificate issued for domain %q: %v", domain, err)
}
}
return nil
}
func (mgr *Manager) AddDomain(domain, accountID, reverseProxyID string) {
mgr.domainsMux.Lock()
mgr.domains[domain] = struct {
accountID string
reverseProxyID string
}{
accountID: accountID,
reverseProxyID: reverseProxyID,
}
mgr.domainsMux.Unlock()
go mgr.prefetchCertificate(domain)
}
// prefetchCertificate proactively triggers certificate generation for a domain.
func (mgr *Manager) prefetchCertificate(domain string) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
hello := &tls.ClientHelloInfo{
ServerName: domain,
Conn: &dummyConn{ctx: ctx},
}
log.Infof("prefetching certificate for domain %q", domain)
_, err := mgr.GetCertificate(hello)
if err != nil {
log.Warnf("prefetch certificate for domain %q: %v", domain, err)
return
}
log.Infof("successfully prefetched certificate for domain %q", domain)
}
// dummyConn implements net.Conn to provide context for certificate fetching.
type dummyConn struct {
ctx context.Context
}
func (c *dummyConn) Read(b []byte) (n int, err error) { return 0, nil }
func (c *dummyConn) Write(b []byte) (n int, err error) { return len(b), nil }
func (c *dummyConn) Close() error { return nil }
func (c *dummyConn) LocalAddr() net.Addr { return nil }
func (c *dummyConn) RemoteAddr() net.Addr { return nil }
func (c *dummyConn) SetDeadline(t time.Time) error { return nil }
func (c *dummyConn) SetReadDeadline(t time.Time) error { return nil }
func (c *dummyConn) SetWriteDeadline(t time.Time) error { return nil }
func (mgr *Manager) RemoveDomain(domain string) {
mgr.domainsMux.Lock()
defer mgr.domainsMux.Unlock()
delete(mgr.domains, domain)
}