mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-19 00:36:38 +00:00
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
120 lines
3.1 KiB
Go
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)
|
|
}
|