mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-16 15:26:40 +00:00
add cert manager with self signed cert support
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
"listen_address": ":443",
|
||||
"management_url": "https://api.netbird.io",
|
||||
"http_listen_address": ":80",
|
||||
"enable_https": false,
|
||||
"cert_mode": "letsencrypt",
|
||||
"tls_email": "your-email@example.com",
|
||||
"cert_cache_dir": "./certs",
|
||||
"oidc_config": {
|
||||
|
||||
28
proxy/internal/reverseproxy/certmanager/interface.go
Normal file
28
proxy/internal/reverseproxy/certmanager/interface.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package certmanager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Manager defines the interface for certificate management
|
||||
type Manager interface {
|
||||
// IsEnabled returns whether certificate management is enabled
|
||||
IsEnabled() bool
|
||||
|
||||
// AddDomain adds a domain to the allowed hosts list
|
||||
AddDomain(domain string)
|
||||
|
||||
// RemoveDomain removes a domain from the allowed hosts list
|
||||
RemoveDomain(domain string)
|
||||
|
||||
// IssueCertificate eagerly issues a certificate for a domain
|
||||
IssueCertificate(ctx context.Context, domain string) error
|
||||
|
||||
// TLSConfig returns the TLS configuration for the HTTPS server
|
||||
TLSConfig() *tls.Config
|
||||
|
||||
// HTTPHandler returns the HTTP handler for ACME challenges (or fallback)
|
||||
HTTPHandler(fallback http.Handler) http.Handler
|
||||
}
|
||||
113
proxy/internal/reverseproxy/certmanager/letsencrypt.go
Normal file
113
proxy/internal/reverseproxy/certmanager/letsencrypt.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package certmanager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
)
|
||||
|
||||
// LetsEncryptManager handles TLS certificate issuance via Let's Encrypt
|
||||
type LetsEncryptManager struct {
|
||||
autocertManager *autocert.Manager
|
||||
allowedHosts map[string]bool
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// LetsEncryptConfig holds Let's Encrypt certificate manager configuration
|
||||
type LetsEncryptConfig struct {
|
||||
// Email for Let's Encrypt registration (required)
|
||||
Email string
|
||||
|
||||
// CertCacheDir is the directory to cache certificates
|
||||
CertCacheDir string
|
||||
}
|
||||
|
||||
// NewLetsEncrypt creates a new Let's Encrypt certificate manager
|
||||
func NewLetsEncrypt(config LetsEncryptConfig) *LetsEncryptManager {
|
||||
m := &LetsEncryptManager{
|
||||
allowedHosts: make(map[string]bool),
|
||||
}
|
||||
|
||||
m.autocertManager = &autocert.Manager{
|
||||
Prompt: autocert.AcceptTOS,
|
||||
HostPolicy: m.hostPolicy,
|
||||
Cache: autocert.DirCache(config.CertCacheDir),
|
||||
Email: config.Email,
|
||||
RenewBefore: 0, // Use default
|
||||
}
|
||||
|
||||
log.Info("Let's Encrypt certificate manager initialized")
|
||||
return m
|
||||
}
|
||||
|
||||
// IsEnabled returns whether certificate management is enabled
|
||||
func (m *LetsEncryptManager) IsEnabled() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// AddDomain adds a domain to the allowed hosts list
|
||||
func (m *LetsEncryptManager) AddDomain(domain string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.allowedHosts[domain] = true
|
||||
log.Infof("Added domain to Let's Encrypt manager: %s", domain)
|
||||
}
|
||||
|
||||
// RemoveDomain removes a domain from the allowed hosts list
|
||||
func (m *LetsEncryptManager) RemoveDomain(domain string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
delete(m.allowedHosts, domain)
|
||||
log.Infof("Removed domain from Let's Encrypt manager: %s", domain)
|
||||
}
|
||||
|
||||
// IssueCertificate eagerly issues a Let's Encrypt certificate for a domain
|
||||
func (m *LetsEncryptManager) IssueCertificate(ctx context.Context, domain string) error {
|
||||
log.Infof("Issuing Let's Encrypt certificate for domain: %s", domain)
|
||||
|
||||
// Use GetCertificate to trigger certificate issuance
|
||||
// This will go through the ACME challenge flow
|
||||
hello := &tls.ClientHelloInfo{
|
||||
ServerName: domain,
|
||||
}
|
||||
|
||||
cert, err := m.autocertManager.GetCertificate(hello)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to issue certificate for domain %s: %w", domain, err)
|
||||
}
|
||||
|
||||
log.Infof("Successfully issued Let's Encrypt certificate for domain: %s (expires: %s)",
|
||||
domain, cert.Leaf.NotAfter.Format(time.RFC3339))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TLSConfig returns the TLS configuration for the HTTPS server
|
||||
func (m *LetsEncryptManager) TLSConfig() *tls.Config {
|
||||
return m.autocertManager.TLSConfig()
|
||||
}
|
||||
|
||||
// HTTPHandler returns the HTTP handler for ACME challenges
|
||||
func (m *LetsEncryptManager) HTTPHandler(fallback http.Handler) http.Handler {
|
||||
return m.autocertManager.HTTPHandler(fallback)
|
||||
}
|
||||
|
||||
// hostPolicy validates that a requested host is in the allowed hosts list
|
||||
func (m *LetsEncryptManager) hostPolicy(ctx context.Context, host string) error {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
if m.allowedHosts[host] {
|
||||
log.Debugf("ACME challenge accepted for domain: %s", host)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Warnf("ACME challenge rejected for unconfigured domain: %s", host)
|
||||
return fmt.Errorf("host %s not configured", host)
|
||||
}
|
||||
166
proxy/internal/reverseproxy/certmanager/selfsigned_manager.go
Normal file
166
proxy/internal/reverseproxy/certmanager/selfsigned_manager.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package certmanager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// SelfSignedManager handles self-signed certificate generation for local testing
|
||||
type SelfSignedManager struct {
|
||||
certificates map[string]*tls.Certificate // domain -> certificate cache
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewSelfSigned creates a new self-signed certificate manager
|
||||
func NewSelfSigned() *SelfSignedManager {
|
||||
log.Info("Self-signed certificate manager initialized")
|
||||
return &SelfSignedManager{
|
||||
certificates: make(map[string]*tls.Certificate),
|
||||
}
|
||||
}
|
||||
|
||||
// IsEnabled returns whether certificate management is enabled
|
||||
func (m *SelfSignedManager) IsEnabled() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// AddDomain adds a domain to the manager (no-op for self-signed, but maintains interface)
|
||||
func (m *SelfSignedManager) AddDomain(domain string) {
|
||||
log.Infof("Added domain to self-signed manager: %s", domain)
|
||||
}
|
||||
|
||||
// RemoveDomain removes a domain from the manager
|
||||
func (m *SelfSignedManager) RemoveDomain(domain string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
delete(m.certificates, domain)
|
||||
log.Infof("Removed domain from self-signed manager: %s", domain)
|
||||
}
|
||||
|
||||
// IssueCertificate generates and caches a self-signed certificate for a domain
|
||||
func (m *SelfSignedManager) IssueCertificate(ctx context.Context, domain string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
// Check if we already have a certificate for this domain
|
||||
if _, exists := m.certificates[domain]; exists {
|
||||
log.Debugf("Self-signed certificate already exists for domain: %s", domain)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Generate self-signed certificate
|
||||
cert, err := m.generateCertificate(domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Cache the certificate
|
||||
m.certificates[domain] = cert
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TLSConfig returns the TLS configuration for the HTTPS server
|
||||
func (m *SelfSignedManager) TLSConfig() *tls.Config {
|
||||
return &tls.Config{
|
||||
GetCertificate: m.getCertificate,
|
||||
}
|
||||
}
|
||||
|
||||
// HTTPHandler returns the fallback handler (no ACME challenges for self-signed)
|
||||
func (m *SelfSignedManager) HTTPHandler(fallback http.Handler) http.Handler {
|
||||
return fallback
|
||||
}
|
||||
|
||||
// getCertificate returns a self-signed certificate for the requested domain
|
||||
func (m *SelfSignedManager) getCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
m.mu.RLock()
|
||||
cert, exists := m.certificates[hello.ServerName]
|
||||
m.mu.RUnlock()
|
||||
|
||||
if exists {
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
// Generate certificate on-demand if not cached
|
||||
log.Infof("Generating self-signed certificate on-demand for: %s", hello.ServerName)
|
||||
|
||||
newCert, err := m.generateCertificate(hello.ServerName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Cache it
|
||||
m.mu.Lock()
|
||||
m.certificates[hello.ServerName] = newCert
|
||||
m.mu.Unlock()
|
||||
|
||||
return newCert, nil
|
||||
}
|
||||
|
||||
// generateCertificate generates a self-signed certificate for a domain
|
||||
func (m *SelfSignedManager) generateCertificate(domain string) (*tls.Certificate, error) {
|
||||
// Generate private key
|
||||
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate private key: %w", err)
|
||||
}
|
||||
|
||||
// Create certificate template
|
||||
notBefore := time.Now()
|
||||
notAfter := notBefore.Add(365 * 24 * time.Hour) // Valid for 1 year
|
||||
|
||||
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate serial number: %w", err)
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"NetBird Local Development"},
|
||||
CommonName: domain,
|
||||
},
|
||||
NotBefore: notBefore,
|
||||
NotAfter: notAfter,
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
DNSNames: []string{domain},
|
||||
}
|
||||
|
||||
// Create self-signed certificate
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create certificate: %w", err)
|
||||
}
|
||||
|
||||
// Parse certificate
|
||||
cert, err := x509.ParseCertificate(certDER)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse certificate: %w", err)
|
||||
}
|
||||
|
||||
tlsCert := &tls.Certificate{
|
||||
Certificate: [][]byte{certDER},
|
||||
PrivateKey: priv,
|
||||
Leaf: cert,
|
||||
}
|
||||
|
||||
log.Infof("Generated self-signed certificate for domain: %s (expires: %s)",
|
||||
domain, cert.NotAfter.Format(time.RFC3339))
|
||||
|
||||
return tlsCert, nil
|
||||
}
|
||||
@@ -18,16 +18,18 @@ type Config struct {
|
||||
ManagementURL string `env:"NB_REVERSE_PROXY_MANAGEMENT_URL" json:"management_url"`
|
||||
|
||||
// HTTPListenAddress is the address for HTTP (default ":80")
|
||||
// Used for ACME challenges when HTTPS is enabled, or as main listener when HTTPS is disabled
|
||||
// Used for ACME challenges (Let's Encrypt HTTP-01 challenge)
|
||||
HTTPListenAddress string `env:"NB_REVERSE_PROXY_HTTP_LISTEN_ADDRESS" envDefault:":80" json:"http_listen_address"`
|
||||
|
||||
// EnableHTTPS enables automatic HTTPS with Let's Encrypt
|
||||
EnableHTTPS bool `env:"NB_REVERSE_PROXY_ENABLE_HTTPS" envDefault:"false" json:"enable_https"`
|
||||
// CertMode specifies certificate mode: "letsencrypt" or "selfsigned" (default: "letsencrypt")
|
||||
// "letsencrypt" - Uses Let's Encrypt for production certificates (requires public domain)
|
||||
// "selfsigned" - Generates self-signed certificates for local testing
|
||||
CertMode string `env:"NB_REVERSE_PROXY_CERT_MODE" envDefault:"letsencrypt" json:"cert_mode"`
|
||||
|
||||
// TLSEmail is the email for Let's Encrypt registration
|
||||
// TLSEmail is the email for Let's Encrypt registration (required for letsencrypt mode)
|
||||
TLSEmail string `env:"NB_REVERSE_PROXY_TLS_EMAIL" json:"tls_email"`
|
||||
|
||||
// CertCacheDir is the directory to cache certificates (default "./certs")
|
||||
// CertCacheDir is the directory to cache certificates (for letsencrypt mode, default "./certs")
|
||||
CertCacheDir string `env:"NB_REVERSE_PROXY_CERT_CACHE_DIR" envDefault:"./certs" json:"cert_cache_dir"`
|
||||
|
||||
// OIDCConfig is the global OIDC/OAuth configuration for authentication
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
package reverseproxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/proxy/internal/auth/oidc"
|
||||
"github.com/netbirdio/netbird/proxy/internal/reverseproxy/certmanager"
|
||||
)
|
||||
|
||||
// Proxy wraps a reverse proxy with dynamic routing
|
||||
@@ -17,7 +19,7 @@ type Proxy struct {
|
||||
routes map[string]*RouteConfig // key is host/domain (for fast O(1) lookup)
|
||||
server *http.Server
|
||||
httpServer *http.Server
|
||||
autocertManager *autocert.Manager
|
||||
certManager certmanager.Manager
|
||||
isRunning bool
|
||||
requestCallback RequestDataCallback
|
||||
oidcHandler *oidc.Handler
|
||||
@@ -36,11 +38,14 @@ func New(config Config) (*Proxy, error) {
|
||||
config.CertCacheDir = "./certs"
|
||||
}
|
||||
|
||||
// Validate HTTPS config
|
||||
if config.EnableHTTPS {
|
||||
if config.TLSEmail == "" {
|
||||
return nil, fmt.Errorf("TLSEmail is required when EnableHTTPS is true")
|
||||
}
|
||||
// Set default cert mode
|
||||
if config.CertMode == "" {
|
||||
config.CertMode = "letsencrypt"
|
||||
}
|
||||
|
||||
// Validate config based on cert mode
|
||||
if config.CertMode == "letsencrypt" && config.TLSEmail == "" {
|
||||
return nil, fmt.Errorf("TLSEmail is required for letsencrypt mode")
|
||||
}
|
||||
|
||||
// Set default OIDC session cookie name if not provided
|
||||
@@ -48,10 +53,24 @@ func New(config Config) (*Proxy, error) {
|
||||
config.OIDCConfig.SessionCookieName = "auth_session"
|
||||
}
|
||||
|
||||
// Initialize certificate manager based on mode
|
||||
var certMgr certmanager.Manager
|
||||
if config.CertMode == "selfsigned" {
|
||||
// HTTPS with self-signed certificates (for local testing)
|
||||
certMgr = certmanager.NewSelfSigned()
|
||||
} else {
|
||||
// HTTPS with Let's Encrypt (for production)
|
||||
certMgr = certmanager.NewLetsEncrypt(certmanager.LetsEncryptConfig{
|
||||
Email: config.TLSEmail,
|
||||
CertCacheDir: config.CertCacheDir,
|
||||
})
|
||||
}
|
||||
|
||||
p := &Proxy{
|
||||
config: config,
|
||||
routes: make(map[string]*RouteConfig),
|
||||
isRunning: false,
|
||||
config: config,
|
||||
routes: make(map[string]*RouteConfig),
|
||||
certManager: certMgr,
|
||||
isRunning: false,
|
||||
}
|
||||
|
||||
// Initialize OIDC handler if OIDC is configured
|
||||
@@ -64,6 +83,91 @@ func New(config Config) (*Proxy, error) {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Start starts the reverse proxy server (non-blocking)
|
||||
func (p *Proxy) Start() error {
|
||||
p.mu.Lock()
|
||||
if p.isRunning {
|
||||
p.mu.Unlock()
|
||||
return fmt.Errorf("reverse proxy already running")
|
||||
}
|
||||
p.isRunning = true
|
||||
p.mu.Unlock()
|
||||
|
||||
// Build the main HTTP handler
|
||||
handler := p.buildHandler()
|
||||
|
||||
return p.startHTTPS(handler)
|
||||
}
|
||||
|
||||
// startHTTPS starts the proxy with HTTPS (non-blocking)
|
||||
func (p *Proxy) startHTTPS(handler http.Handler) error {
|
||||
// Start HTTP server for ACME challenges (Let's Encrypt HTTP-01)
|
||||
p.httpServer = &http.Server{
|
||||
Addr: p.config.HTTPListenAddress,
|
||||
Handler: p.certManager.HTTPHandler(nil),
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Infof("Starting HTTP server for ACME challenges on %s", p.config.HTTPListenAddress)
|
||||
if err := p.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Errorf("HTTP server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Start HTTPS server in background
|
||||
p.server = &http.Server{
|
||||
Addr: p.config.ListenAddress,
|
||||
Handler: handler,
|
||||
TLSConfig: p.certManager.TLSConfig(),
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Infof("Starting HTTPS reverse proxy server on %s", p.config.ListenAddress)
|
||||
if err := p.server.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
|
||||
log.Errorf("HTTPS server failed: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop gracefully stops the reverse proxy server
|
||||
func (p *Proxy) Stop(ctx context.Context) error {
|
||||
p.mu.Lock()
|
||||
if !p.isRunning {
|
||||
p.mu.Unlock()
|
||||
return fmt.Errorf("reverse proxy not running")
|
||||
}
|
||||
p.isRunning = false
|
||||
p.mu.Unlock()
|
||||
|
||||
log.Info("Stopping reverse proxy server...")
|
||||
|
||||
// Stop HTTP server (for ACME challenges)
|
||||
if p.httpServer != nil {
|
||||
if err := p.httpServer.Shutdown(ctx); err != nil {
|
||||
log.Errorf("Error shutting down HTTP server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop main server
|
||||
if p.server != nil {
|
||||
if err := p.server.Shutdown(ctx); err != nil {
|
||||
return fmt.Errorf("error shutting down server: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("Reverse proxy server stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsRunning returns whether the proxy is running
|
||||
func (p *Proxy) IsRunning() bool {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
return p.isRunning
|
||||
}
|
||||
|
||||
// SetRequestCallback sets the callback for request metrics
|
||||
func (p *Proxy) SetRequestCallback(callback RequestDataCallback) {
|
||||
p.mu.Lock()
|
||||
|
||||
@@ -56,15 +56,24 @@ func (p *Proxy) AddRoute(route *RouteConfig) error {
|
||||
// Add route with domain as key
|
||||
p.routes[route.Domain] = route
|
||||
|
||||
// Register domain with certificate manager
|
||||
p.certManager.AddDomain(route.Domain)
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"route_id": route.ID,
|
||||
"domain": route.Domain,
|
||||
"paths": len(route.PathMappings),
|
||||
}).Info("Added route")
|
||||
|
||||
// Note: With this architecture, we don't need to reload the server
|
||||
// The handler dynamically looks up routes on each request
|
||||
// Certificates will be obtained automatically when the domain is first accessed
|
||||
// Eagerly issue certificate in background
|
||||
go func(domain string) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
if err := p.certManager.IssueCertificate(ctx, domain); err != nil {
|
||||
log.Errorf("Failed to issue certificate: %v", err)
|
||||
}
|
||||
}(route.Domain)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -82,6 +91,9 @@ func (p *Proxy) RemoveRoute(domain string) error {
|
||||
// Remove route
|
||||
delete(p.routes, domain)
|
||||
|
||||
// Unregister domain from certificate manager
|
||||
p.certManager.RemoveDomain(domain)
|
||||
|
||||
log.Infof("Removed route for domain: %s", domain)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
package reverseproxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
)
|
||||
|
||||
// Start starts the reverse proxy server (non-blocking)
|
||||
func (p *Proxy) Start() error {
|
||||
p.mu.Lock()
|
||||
if p.isRunning {
|
||||
p.mu.Unlock()
|
||||
return fmt.Errorf("reverse proxy already running")
|
||||
}
|
||||
p.isRunning = true
|
||||
p.mu.Unlock()
|
||||
|
||||
// Build the main HTTP handler
|
||||
handler := p.buildHandler()
|
||||
|
||||
if p.config.EnableHTTPS {
|
||||
return p.startHTTPS(handler)
|
||||
}
|
||||
|
||||
return p.startHTTP(handler)
|
||||
}
|
||||
|
||||
// startHTTPS starts the proxy with HTTPS and Let's Encrypt (non-blocking)
|
||||
func (p *Proxy) startHTTPS(handler http.Handler) error {
|
||||
// Setup autocert manager with dynamic host policy
|
||||
p.autocertManager = &autocert.Manager{
|
||||
Prompt: autocert.AcceptTOS,
|
||||
HostPolicy: p.dynamicHostPolicy,
|
||||
Cache: autocert.DirCache(p.config.CertCacheDir),
|
||||
Email: p.config.TLSEmail,
|
||||
RenewBefore: 0, // Use default
|
||||
}
|
||||
|
||||
// Start HTTP server for ACME challenges
|
||||
p.httpServer = &http.Server{
|
||||
Addr: p.config.HTTPListenAddress,
|
||||
Handler: p.autocertManager.HTTPHandler(nil),
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Infof("Starting HTTP server for ACME challenges on %s", p.config.HTTPListenAddress)
|
||||
if err := p.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Errorf("HTTP server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Start HTTPS server in background
|
||||
p.server = &http.Server{
|
||||
Addr: p.config.ListenAddress,
|
||||
Handler: handler,
|
||||
TLSConfig: p.autocertManager.TLSConfig(),
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Infof("Starting HTTPS reverse proxy server on %s", p.config.ListenAddress)
|
||||
if err := p.server.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
|
||||
log.Errorf("HTTPS server failed: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// startHTTP starts the proxy with HTTP only (non-blocking)
|
||||
func (p *Proxy) startHTTP(handler http.Handler) error {
|
||||
p.server = &http.Server{
|
||||
Addr: p.config.HTTPListenAddress,
|
||||
Handler: handler,
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Infof("Starting HTTP reverse proxy server on %s (HTTPS disabled)", p.config.HTTPListenAddress)
|
||||
if err := p.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Errorf("HTTP server failed: %w", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// dynamicHostPolicy validates that a requested host has a configured route
|
||||
func (p *Proxy) dynamicHostPolicy(ctx context.Context, host string) error {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
|
||||
// Check if we have a route for this host
|
||||
if _, exists := p.routes[host]; exists {
|
||||
log.Infof("ACME challenge accepted for configured host: %s", host)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Warnf("ACME challenge rejected for unconfigured host: %s", host)
|
||||
return fmt.Errorf("host %s not configured", host)
|
||||
}
|
||||
|
||||
// Stop gracefully stops the reverse proxy server
|
||||
func (p *Proxy) Stop(ctx context.Context) error {
|
||||
p.mu.Lock()
|
||||
if !p.isRunning {
|
||||
p.mu.Unlock()
|
||||
return fmt.Errorf("reverse proxy not running")
|
||||
}
|
||||
p.isRunning = false
|
||||
p.mu.Unlock()
|
||||
|
||||
log.Info("Stopping reverse proxy server...")
|
||||
|
||||
// Stop HTTP server (for ACME challenges)
|
||||
if p.httpServer != nil {
|
||||
if err := p.httpServer.Shutdown(ctx); err != nil {
|
||||
log.Errorf("Error shutting down HTTP server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop main server
|
||||
if p.server != nil {
|
||||
if err := p.server.Shutdown(ctx); err != nil {
|
||||
return fmt.Errorf("error shutting down server: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("Reverse proxy server stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsRunning returns whether the proxy is running
|
||||
func (p *Proxy) IsRunning() bool {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
return p.isRunning
|
||||
}
|
||||
Reference in New Issue
Block a user