mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-24 09:18:31 +02:00
chore(docs): add README.md across multiple packages
This commit is contained in:
560
internal/autocert/README.md
Normal file
560
internal/autocert/README.md
Normal file
@@ -0,0 +1,560 @@
|
||||
# Autocert Package
|
||||
|
||||
Automated SSL certificate management using the ACME protocol (Let's Encrypt and compatible CAs).
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────────────┐
|
||||
│ GoDoxy Proxy │
|
||||
├────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ┌──────────────────────┐ ┌─────────────────────────────────────────┐ │
|
||||
│ │ Config.State │────▶│ autocert.Provider │ │
|
||||
│ │ (config loading) │ │ ┌───────────────────────────────────┐ │ │
|
||||
│ └──────────────────────┘ │ │ main Provider │ │ │
|
||||
│ │ │ - Primary certificate │ │ │
|
||||
│ │ │ - SNI matcher │ │ │
|
||||
│ │ │ - Renewal scheduler │ │ │
|
||||
│ │ └───────────────────────────────────┘ │ │
|
||||
│ │ ┌───────────────────────────────────┐ │ │
|
||||
│ │ │ extraProviders[] │ │ │
|
||||
│ │ │ - Additional certifictes │ │ │
|
||||
│ │ │ - Different domains/A │ │ │
|
||||
│ │ └───────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────────────────┐ │
|
||||
│ │ TLS Handshake │ │
|
||||
│ │ GetCert(ClientHelloInf) │ │
|
||||
│ └────────────────────────────────┘ │
|
||||
└────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Certificate Lifecycle
|
||||
|
||||
```mermaid
|
||||
---
|
||||
config:
|
||||
theme: redux-dark-color
|
||||
---
|
||||
flowchart TD
|
||||
A[Start] --> B[Load Existing Cert]
|
||||
B --> C{Cert Exists?}
|
||||
C -->|Yes| D[Load Cert from Disk]
|
||||
C -->|No| E[Obtain New Cert]
|
||||
|
||||
D --> F{Valid & Not Expired?}
|
||||
F -->|Yes| G[Schedule Renewal]
|
||||
F -->|No| H{Renewal Time?}
|
||||
H -->|Yes| I[Renew Certificate]
|
||||
H -->|No| G
|
||||
|
||||
E --> J[Init ACME Client]
|
||||
J --> K[Register Account]
|
||||
K --> L[DNS-01 Challenge]
|
||||
L --> M[Complete Challenge]
|
||||
M --> N[Download Certificate]
|
||||
N --> O[Save to Disk]
|
||||
O --> G
|
||||
|
||||
G --> P[Wait Until Renewal Time]
|
||||
P --> Q[Trigger Renewal]
|
||||
Q --> I
|
||||
|
||||
I --> R[Renew via ACME]
|
||||
R --> S{Same Domains?}
|
||||
S -->|Yes| T[Bundle & Save]
|
||||
S -->|No| U[Re-obtain Certificate]
|
||||
U --> T
|
||||
|
||||
T --> V[Update SNI Matcher]
|
||||
V --> G
|
||||
|
||||
style E fill:#90EE90
|
||||
style I fill:#FFD700
|
||||
style N fill:#90EE90
|
||||
style U fill:#FFA07A
|
||||
```
|
||||
|
||||
## SNI Matching Flow
|
||||
|
||||
When a TLS client connects with Server Name Indication (SNI), the proxy needs to select the correct certificate.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Client["TLS Client"] -->|ClientHello SNI| Proxy["GoDoxy Proxy"]
|
||||
Proxy -->|Certificate| Client
|
||||
|
||||
subgraph "SNI Matching Process"
|
||||
direction TB
|
||||
A[Extract SNI from ClientHello] --> B{Normalize SNI}
|
||||
B --> C{Exact Match?}
|
||||
C -->|Yes| D[Return cert]
|
||||
C -->|No| E[Wildcard Suffix Tree]
|
||||
E --> F{Match Found?}
|
||||
F -->|Yes| D
|
||||
F -->|No| G[Return default cert]
|
||||
end
|
||||
|
||||
style C fill:#90EE90
|
||||
style E fill:#87CEEB
|
||||
style F fill:#FFD700
|
||||
```
|
||||
|
||||
### Suffix Tree Structure
|
||||
|
||||
The `sniMatcher` uses an optimized suffix tree for efficient wildcard matching:
|
||||
|
||||
```
|
||||
Certificate: *.example.com, example.com, *.api.example.com
|
||||
|
||||
exact:
|
||||
"example.com" → Provider_A
|
||||
|
||||
root:
|
||||
└── "com"
|
||||
└── "example"
|
||||
├── "*" → Provider_A [wildcard at *.example.com]
|
||||
└── "api"
|
||||
└── "*" → Provider_B [wildcard at *.api.example.com]
|
||||
```
|
||||
|
||||
## Key Components
|
||||
|
||||
### Config
|
||||
|
||||
Configuration for certificate management, loaded from `config/autocert.yml`.
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
Email string // ACME account email
|
||||
Domains []string // Domains to certifiy
|
||||
CertPath string // Output cert path
|
||||
KeyPath string // Output key path
|
||||
Extra []ConfigExtra // Additional cert configs
|
||||
ACMEKeyPath string // ACME account private key (shared by all extras)
|
||||
Provider string // DNS provider name
|
||||
Options map[string]strutils.Redacted // Provider-specific options
|
||||
Resolvers []string // DNS resolvers for DNS-01
|
||||
CADirURL string // Custom ACME CA directory
|
||||
CACerts []string // Custom CA certificates
|
||||
EABKid string // External Account Binding Key ID
|
||||
EABHmac string // External Account Binding HMAC
|
||||
|
||||
idx int // 0: main, 1+: extra[i]
|
||||
}
|
||||
|
||||
type ConfigExtra Config
|
||||
```
|
||||
|
||||
**Extra Provider Merging:** Extra configurations are merged with the main config using `MergeExtraConfig()`, inheriting most settings from the main provider while allowing per-certificate overrides for `Provider`, `Email`, `Domains`, `Options`, `Resolvers`, `CADirURL`, `CACerts`, `EABKid`, `EABHmac`, and `HTTPClient`. The `ACMEKeyPath` is shared across all providers.
|
||||
|
||||
**Validation:**
|
||||
|
||||
- Extra configs must have unique `cert_path` and `key_path` values (no duplicates across main or any extra provider)
|
||||
|
||||
### ConfigExtra
|
||||
|
||||
Extra certificate configuration type. Uses `MergeExtraConfig()` to inherit settings from the main provider:
|
||||
|
||||
```go
|
||||
func MergeExtraConfig(mainCfg *Config, extraCfg *ConfigExtra) ConfigExtra
|
||||
```
|
||||
|
||||
Fields that can be overridden per extra provider:
|
||||
|
||||
- `Provider` - DNS provider name
|
||||
- `Email` - ACME account email
|
||||
- `Domains` - Certificate domains
|
||||
- `Options` - Provider-specific options
|
||||
- `Resolvers` - DNS resolvers
|
||||
- `CADirURL` - Custom ACME CA directory
|
||||
- `CACerts` - Custom CA certificates
|
||||
- `EABKid` / `EABHmac` - External Account Binding credentials
|
||||
- `HTTPClient` - Custom HTTP client
|
||||
|
||||
Fields inherited from main config (shared):
|
||||
|
||||
- `ACMEKeyPath` - ACME account private key (same for all)
|
||||
|
||||
**Provider Types:**
|
||||
|
||||
- `local` - No ACME, use existing certificate (default)
|
||||
- `pseudo` - Mock provider for testing
|
||||
- `custom` - Custom ACME CA with `CADirURL`
|
||||
|
||||
### Provider
|
||||
|
||||
Main certificate management struct that handles:
|
||||
|
||||
- Certificate issuance and renewal
|
||||
- Loading certificates from disk
|
||||
- SNI-based certificate selection
|
||||
- Renewal scheduling
|
||||
|
||||
```go
|
||||
type Provider struct {
|
||||
logger zerolog.Logger // Provider-scoped logger
|
||||
|
||||
cfg *Config // Configuration
|
||||
user *User // ACME account
|
||||
legoCfg *lego.Config // LEGO client config
|
||||
client *lego.Client // ACME client
|
||||
lastFailure time.Time // Last renewal failure
|
||||
legoCert *certificate.Resource // Cached cert resource
|
||||
tlsCert *tls.Certificate // Parsed TLS certificate
|
||||
certExpiries CertExpiries // Domain → expiry map
|
||||
extraProviders []*Provider // Additional certificates
|
||||
sniMatcher sniMatcher // SNI → Provider mapping
|
||||
forceRenewalCh chan struct{} // Force renewal trigger channel
|
||||
scheduleRenewalOnce sync.Once // Prevents duplicate renewal scheduling
|
||||
}
|
||||
```
|
||||
|
||||
**Logging:** Each provider has a scoped logger with provider name ("main" or "extra[N]") for consistent log context.
|
||||
|
||||
**Key Methods:**
|
||||
|
||||
- `NewProvider(cfg *Config, user *User, legoCfg *lego.Config) (*Provider, error)` - Creates provider and initializes extra providers atomically
|
||||
- `GetCert(hello *tls.ClientHelloInfo)` - Returns certificate for TLS handshake
|
||||
- `GetName()` - Returns provider name ("main" or "extra[N]")
|
||||
- `ObtainCert()` - Obtains new certificate via ACME
|
||||
- `ObtainCertAll()` - Renews/obtains certificates for main and all extra providers
|
||||
- `ObtainCertIfNotExistsAll()` - Obtains certificates only if they don't exist on disk
|
||||
- `ForceExpiryAll()` - Triggers forced certificate renewal for main and all extra providers
|
||||
- `ScheduleRenewalAll(parent task.Parent)` - Schedules automatic renewal for all providers
|
||||
- `PrintCertExpiriesAll()` - Logs certificate expiry dates for all providers
|
||||
|
||||
### User
|
||||
|
||||
ACME account representation implementing lego's `acme.User` interface.
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
Email string // Account email
|
||||
Registration *registration.Resource // ACME registration
|
||||
Key crypto.PrivateKey // Account key
|
||||
}
|
||||
```
|
||||
|
||||
### sniMatcher
|
||||
|
||||
Efficient SNI-to-Provider lookup with exact and wildcard matching.
|
||||
|
||||
```go
|
||||
type sniMatcher struct {
|
||||
exact map[string]*Provider // Exact domain matches
|
||||
root sniTreeNode // Wildcard suffix tree
|
||||
}
|
||||
|
||||
type sniTreeNode struct {
|
||||
children map[string]*sniTreeNode // DNS label → child node
|
||||
wildcard *Provider // Wildcard match at this level
|
||||
}
|
||||
```
|
||||
|
||||
## DNS Providers
|
||||
|
||||
Supported DNS providers for DNS-01 challenge validation:
|
||||
|
||||
| Provider | Name | Description |
|
||||
| ------------ | -------------- | ---------------------------------------- |
|
||||
| Cloudflare | `cloudflare` | Cloudflare DNS |
|
||||
| Route 53 | `route53` | AWS Route 53 |
|
||||
| DigitalOcean | `digitalocean` | DigitalOcean DNS |
|
||||
| GoDaddy | `godaddy` | GoDaddy DNS |
|
||||
| OVH | `ovh` | OVHcloud DNS |
|
||||
| CloudDNS | `clouddns` | Google Cloud DNS |
|
||||
| AzureDNS | `azuredns` | Azure DNS |
|
||||
| DuckDNS | `duckdns` | DuckDNS |
|
||||
| and more... | | See `internal/dnsproviders/providers.go` |
|
||||
|
||||
### Provider Configuration
|
||||
|
||||
Each provider accepts configuration via the `options` map:
|
||||
|
||||
```yaml
|
||||
autocert:
|
||||
provider: cloudflare
|
||||
email: admin@example.com
|
||||
domains:
|
||||
- example.com
|
||||
- "*.example.com"
|
||||
options:
|
||||
CF_API_TOKEN: your-api-token
|
||||
CF_ZONE_API_TOKEN: your-zone-token
|
||||
resolvers:
|
||||
- 1.1.1.1:53
|
||||
```
|
||||
|
||||
## ACME Integration
|
||||
|
||||
### Account Registration
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Load or Generate ACME Key] --> B[Init LEGO Client]
|
||||
B --> C[Resolve Account by Key]
|
||||
C --> D{Account Exists?}
|
||||
D -->|Yes| E[Continue with existing]
|
||||
D -->|No| F{Has EAB?}
|
||||
F -->|Yes| G[Register with EAB]
|
||||
F -->|No| H[Register with TOS Agreement]
|
||||
G --> I[Save Registration]
|
||||
H --> I
|
||||
```
|
||||
|
||||
### DNS-01 Challenge
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as ACME CA
|
||||
participant P as GoDoxy
|
||||
participant D as DNS Provider
|
||||
|
||||
P->>C: Request certificate for domain
|
||||
C->>P: Present DNS-01 challenge
|
||||
P->>D: Create TXT record _acme-challenge.domain
|
||||
D-->>P: Record created
|
||||
P->>C: Challenge ready
|
||||
C->>D: Verify DNS TXT record
|
||||
D-->>C: Verification success
|
||||
C->>P: Issue certificate
|
||||
P->>D: Clean up TXT record
|
||||
```
|
||||
|
||||
## Multi-Certificate Support
|
||||
|
||||
The package supports multiple certificates through the `extra` configuration:
|
||||
|
||||
```yaml
|
||||
autocert:
|
||||
provider: cloudflare
|
||||
email: admin@example.com
|
||||
domains:
|
||||
- example.com
|
||||
- "*.example.com"
|
||||
cert_path: certs/example.com.crt
|
||||
key_path: certs/example.com.key
|
||||
extra:
|
||||
- domains:
|
||||
- api.example.com
|
||||
- "*.api.example.com"
|
||||
cert_path: certs/api.example.com.crt
|
||||
key_path: certs/api.example.com.key
|
||||
provider: cloudflare
|
||||
email: admin@api.example.com
|
||||
```
|
||||
|
||||
### Extra Provider Setup
|
||||
|
||||
Extra providers are initialized atomically within `NewProvider()`:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[NewProvider] --> B{Merge Config with Extra}
|
||||
B --> C[Create Provider per Extra]
|
||||
C --> D[Build SNI Matcher]
|
||||
D --> E[Register in SNI Tree]
|
||||
|
||||
style B fill:#87CEEB
|
||||
style C fill:#FFD700
|
||||
```
|
||||
|
||||
## Renewal Scheduling
|
||||
|
||||
### Renewal Timing
|
||||
|
||||
- **Initial Check**: Certificate expiry is checked at startup
|
||||
- **Renewal Window**: Renewal scheduled for 1 month before expiry
|
||||
- **Cooldown on Failure**: 1-hour cooldown after failed renewal
|
||||
- **Request Cooldown**: 15-second cooldown after startup (prevents rate limiting)
|
||||
- **Force Renewal**: `forceRenewalCh` channel allows triggering immediate renewal
|
||||
|
||||
### Force Renewal
|
||||
|
||||
The `forceRenewalCh` channel (buffered size 1) enables immediate certificate renewal on demand:
|
||||
|
||||
```go
|
||||
// Trigger forced renewal for main and all extra providers
|
||||
provider.ForceExpiryAll()
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Start] --> B[Calculate Renewal Time]
|
||||
B --> C[expiry - 30 days]
|
||||
C --> D[Start Timer]
|
||||
|
||||
D --> E{Event?}
|
||||
E -->|forceRenewalCh| F[Force Renewal]
|
||||
E -->|Timer| G[Check Failure Cooldown]
|
||||
E -->|Context Done| H[Exit]
|
||||
|
||||
G --> H1{Recently Failed?}
|
||||
H1 -->|Yes| I[Skip, Wait Next Event]
|
||||
H1 -->|No| J[Attempt Renewal]
|
||||
|
||||
J --> K{Renewal Success?}
|
||||
K -->|Yes| L[Reset Failure, Notify Success]
|
||||
K -->|No| M[Update Failure Time, Notify Failure]
|
||||
|
||||
L --> N[Reset Timer]
|
||||
I --> N
|
||||
M --> D
|
||||
|
||||
N --> D
|
||||
|
||||
style F fill:#FFD700
|
||||
style J fill:#FFD700
|
||||
style K fill:#90EE90
|
||||
style M fill:#FFA07A
|
||||
```
|
||||
|
||||
**Notifications:** Renewal success/failure triggers system notifications with provider name.
|
||||
|
||||
### CertState
|
||||
|
||||
Certificate state tracking:
|
||||
|
||||
```go
|
||||
const (
|
||||
CertStateValid // Certificate is valid and up-to-date
|
||||
CertStateExpired // Certificate has expired or needs renewal
|
||||
CertStateMismatch // Certificate domains don't match config
|
||||
)
|
||||
```
|
||||
|
||||
### RenewMode
|
||||
|
||||
Controls renewal behavior:
|
||||
|
||||
```go
|
||||
const (
|
||||
renewModeForce // Force renewal, bypass cooldown and state check
|
||||
renewModeIfNeeded // Renew only if expired or domain mismatch
|
||||
)
|
||||
```
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
internal/autocert/
|
||||
├── README.md # This file
|
||||
├── config.go # Config struct and validation
|
||||
├── provider.go # Provider implementation
|
||||
├── setup.go # Extra provider setup
|
||||
├── sni_matcher.go # SNI matching logic
|
||||
├── providers.go # DNS provider registration
|
||||
├── state.go # Certificate state enum
|
||||
├── user.go # ACME user/account
|
||||
├── paths.go # Default paths
|
||||
└── types/
|
||||
└── provider.go # Provider interface
|
||||
```
|
||||
|
||||
## Default Paths
|
||||
|
||||
| Constant | Default Value | Description |
|
||||
| -------------------- | ---------------- | ------------------------ |
|
||||
| `CertFileDefault` | `certs/cert.crt` | Default certificate path |
|
||||
| `KeyFileDefault` | `certs/priv.key` | Default private key path |
|
||||
| `ACMEKeyFileDefault` | `certs/acme.key` | Default ACME account key |
|
||||
|
||||
Failure tracking file is generated per-certificate: `<cert_dir>/.last_failure-<hash>`
|
||||
|
||||
## Error Handling
|
||||
|
||||
The package uses structured error handling with `gperr`:
|
||||
|
||||
- **ErrMissingField** - Required configuration field missing
|
||||
- **ErrDuplicatedPath** - Duplicate certificate/key paths in extras
|
||||
- **ErrInvalidDomain** - Invalid domain format
|
||||
- **ErrUnknownProvider** - Unknown DNS provider
|
||||
- **ErrGetCertFailure** - Certificate retrieval failed
|
||||
|
||||
**Error Context:** All errors are prefixed with provider name ("main" or "extra[N]") via `fmtError()` for clear attribution.
|
||||
|
||||
### Failure Tracking
|
||||
|
||||
Last failure is persisted per-certificate to prevent rate limiting:
|
||||
|
||||
```go
|
||||
// File: <cert_dir>/.last_failure-<hash> where hash is SHA256(certPath|keyPath)[:6]
|
||||
```
|
||||
|
||||
**Cooldown Checks:** Last failure is checked in `obtainCertIfNotExists()` (15-second startup cooldown) and `renew()` (1-hour failure cooldown). The `renewModeForce` bypasses cooldown checks entirely.
|
||||
|
||||
## Integration with GoDoxy
|
||||
|
||||
The autocert package integrates with GoDoxy's configuration system:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Config
|
||||
direction TB
|
||||
A[config.yml] --> B[Parse Config]
|
||||
B --> C[AutoCert Config]
|
||||
end
|
||||
|
||||
subgraph State
|
||||
C --> D[NewProvider]
|
||||
D --> E[Schedule Renewal]
|
||||
E --> F[Set Active Provider]
|
||||
end
|
||||
|
||||
subgraph Server
|
||||
F --> G[TLS Handshake]
|
||||
G --> H[GetCert via SNI]
|
||||
H --> I[Return Certificate]
|
||||
end
|
||||
```
|
||||
|
||||
### REST API
|
||||
|
||||
Force certificate renewal via WebSocket endpoint:
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
| -------------------- | ------ | ----------------------------------------- |
|
||||
| `/api/v1/cert/renew` | GET | Triggers `ForceExpiryAll()` via WebSocket |
|
||||
|
||||
The endpoint streams live logs during the renewal process.
|
||||
|
||||
## Usage Example
|
||||
|
||||
```yaml
|
||||
# config/config.yml
|
||||
autocert:
|
||||
provider: cloudflare
|
||||
email: admin@example.com
|
||||
domains:
|
||||
- example.com
|
||||
- "*.example.com"
|
||||
options:
|
||||
CF_API_TOKEN: ${CF_API_TOKEN}
|
||||
resolvers:
|
||||
- 1.1.1.1:53
|
||||
- 8.8.8.8:53
|
||||
```
|
||||
|
||||
```go
|
||||
// In config initialization
|
||||
autocertCfg := state.AutoCert
|
||||
user, legoCfg, err := autocertCfg.GetLegoConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
provider, err := autocert.NewProvider(autocertCfg, user, legoCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("autocert error: %w", err)
|
||||
}
|
||||
|
||||
if err := provider.ObtainCertIfNotExistsAll(); err != nil {
|
||||
return fmt.Errorf("failed to obtain certificates: %w", err)
|
||||
}
|
||||
|
||||
provider.ScheduleRenewalAll(state.Task())
|
||||
provider.PrintCertExpiriesAll()
|
||||
```
|
||||
Reference in New Issue
Block a user