chore: git cache cleanup
This commit is contained in:
271
backend/pkg/dnsprovider/custom/rfc2136_provider.go
Normal file
271
backend/pkg/dnsprovider/custom/rfc2136_provider.go
Normal file
@@ -0,0 +1,271 @@
|
||||
// Package custom provides custom DNS provider plugins for non-built-in integrations.
|
||||
package custom
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Wikid82/charon/backend/pkg/dnsprovider"
|
||||
)
|
||||
|
||||
// RFC 2136 provider constants.
|
||||
const (
|
||||
RFC2136DefaultPort = "53"
|
||||
RFC2136DefaultAlgorithm = "hmac-sha256"
|
||||
RFC2136DefaultPropagationTimeout = 60 * time.Second
|
||||
RFC2136DefaultPollingInterval = 2 * time.Second
|
||||
RFC2136MinPort = 1
|
||||
RFC2136MaxPort = 65535
|
||||
)
|
||||
|
||||
// TSIG algorithm constants.
|
||||
const (
|
||||
TSIGAlgorithmHMACSHA256 = "hmac-sha256"
|
||||
TSIGAlgorithmHMACSHA384 = "hmac-sha384"
|
||||
TSIGAlgorithmHMACSHA512 = "hmac-sha512"
|
||||
TSIGAlgorithmHMACSHA1 = "hmac-sha1"
|
||||
TSIGAlgorithmHMACMD5 = "hmac-md5"
|
||||
)
|
||||
|
||||
// ValidTSIGAlgorithms contains all supported TSIG algorithms.
|
||||
var ValidTSIGAlgorithms = map[string]bool{
|
||||
TSIGAlgorithmHMACSHA256: true,
|
||||
TSIGAlgorithmHMACSHA384: true,
|
||||
TSIGAlgorithmHMACSHA512: true,
|
||||
TSIGAlgorithmHMACSHA1: true,
|
||||
TSIGAlgorithmHMACMD5: true,
|
||||
}
|
||||
|
||||
// RFC2136Provider implements the ProviderPlugin interface for RFC 2136 Dynamic DNS Updates.
|
||||
// RFC 2136 is supported by BIND, PowerDNS, Knot DNS, and many self-hosted DNS servers.
|
||||
type RFC2136Provider struct {
|
||||
propagationTimeout time.Duration
|
||||
pollingInterval time.Duration
|
||||
}
|
||||
|
||||
// NewRFC2136Provider creates a new RFC2136Provider with default settings.
|
||||
func NewRFC2136Provider() *RFC2136Provider {
|
||||
return &RFC2136Provider{
|
||||
propagationTimeout: RFC2136DefaultPropagationTimeout,
|
||||
pollingInterval: RFC2136DefaultPollingInterval,
|
||||
}
|
||||
}
|
||||
|
||||
// Type returns the unique provider type identifier.
|
||||
func (p *RFC2136Provider) Type() string {
|
||||
return "rfc2136"
|
||||
}
|
||||
|
||||
// Metadata returns descriptive information about the provider.
|
||||
func (p *RFC2136Provider) Metadata() dnsprovider.ProviderMetadata {
|
||||
return dnsprovider.ProviderMetadata{
|
||||
Type: "rfc2136",
|
||||
Name: "RFC 2136 (Dynamic DNS)",
|
||||
Description: "Dynamic DNS Updates using RFC 2136 protocol with TSIG authentication. Compatible with BIND, PowerDNS, and Knot DNS.",
|
||||
DocumentationURL: "https://charon.dev/docs/features/rfc2136-dns",
|
||||
IsBuiltIn: false,
|
||||
Version: "1.0.0",
|
||||
InterfaceVersion: dnsprovider.InterfaceVersion,
|
||||
}
|
||||
}
|
||||
|
||||
// Init is called after the plugin is registered.
|
||||
func (p *RFC2136Provider) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cleanup is called before the plugin is unregistered.
|
||||
func (p *RFC2136Provider) Cleanup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RequiredCredentialFields returns credential fields that must be provided.
|
||||
func (p *RFC2136Provider) RequiredCredentialFields() []dnsprovider.CredentialFieldSpec {
|
||||
return []dnsprovider.CredentialFieldSpec{
|
||||
{
|
||||
Name: "nameserver",
|
||||
Label: "DNS Server",
|
||||
Type: "text",
|
||||
Placeholder: "ns1.example.com",
|
||||
Hint: "Hostname or IP address of the DNS server accepting dynamic updates",
|
||||
},
|
||||
{
|
||||
Name: "tsig_key_name",
|
||||
Label: "TSIG Key Name",
|
||||
Type: "text",
|
||||
Placeholder: "acme-update-key.example.com",
|
||||
Hint: "The name of the TSIG key configured on your DNS server",
|
||||
},
|
||||
{
|
||||
Name: "tsig_key_secret",
|
||||
Label: "TSIG Key Secret",
|
||||
Type: "password",
|
||||
Placeholder: "",
|
||||
Hint: "Base64-encoded TSIG secret (from tsig-keygen or dnssec-keygen)",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// OptionalCredentialFields returns credential fields that may be provided.
|
||||
func (p *RFC2136Provider) OptionalCredentialFields() []dnsprovider.CredentialFieldSpec {
|
||||
return []dnsprovider.CredentialFieldSpec{
|
||||
{
|
||||
Name: "port",
|
||||
Label: "Port",
|
||||
Type: "text",
|
||||
Placeholder: RFC2136DefaultPort,
|
||||
Hint: fmt.Sprintf("DNS server port (default: %s)", RFC2136DefaultPort),
|
||||
},
|
||||
{
|
||||
Name: "tsig_algorithm",
|
||||
Label: "TSIG Algorithm",
|
||||
Type: "select",
|
||||
Placeholder: "",
|
||||
Hint: "HMAC algorithm for TSIG authentication (hmac-sha256 recommended)",
|
||||
Options: []dnsprovider.SelectOption{
|
||||
{Value: TSIGAlgorithmHMACSHA256, Label: "HMAC-SHA256 (Recommended)"},
|
||||
{Value: TSIGAlgorithmHMACSHA384, Label: "HMAC-SHA384"},
|
||||
{Value: TSIGAlgorithmHMACSHA512, Label: "HMAC-SHA512"},
|
||||
{Value: TSIGAlgorithmHMACSHA1, Label: "HMAC-SHA1 (Legacy)"},
|
||||
{Value: TSIGAlgorithmHMACMD5, Label: "HMAC-MD5 (Deprecated)"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "zone",
|
||||
Label: "Zone",
|
||||
Type: "text",
|
||||
Placeholder: "example.com",
|
||||
Hint: "DNS zone to update (auto-detected from domain if empty)",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateCredentials checks if the provided credentials are valid.
|
||||
func (p *RFC2136Provider) ValidateCredentials(creds map[string]string) error {
|
||||
// Validate required fields
|
||||
nameserver := strings.TrimSpace(creds["nameserver"])
|
||||
if nameserver == "" {
|
||||
return fmt.Errorf("nameserver is required")
|
||||
}
|
||||
|
||||
tsigKeyName := strings.TrimSpace(creds["tsig_key_name"])
|
||||
if tsigKeyName == "" {
|
||||
return fmt.Errorf("tsig_key_name is required")
|
||||
}
|
||||
|
||||
tsigKeySecret := strings.TrimSpace(creds["tsig_key_secret"])
|
||||
if tsigKeySecret == "" {
|
||||
return fmt.Errorf("tsig_key_secret is required")
|
||||
}
|
||||
|
||||
// Validate base64 encoding of TSIG secret
|
||||
if _, err := base64.StdEncoding.DecodeString(tsigKeySecret); err != nil {
|
||||
return fmt.Errorf("tsig_key_secret must be valid base64: %w", err)
|
||||
}
|
||||
|
||||
// Validate port if provided
|
||||
if portStr := strings.TrimSpace(creds["port"]); portStr != "" {
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("port must be a number: %w", err)
|
||||
}
|
||||
if port < RFC2136MinPort || port > RFC2136MaxPort {
|
||||
return fmt.Errorf("port must be between %d and %d", RFC2136MinPort, RFC2136MaxPort)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate algorithm if provided
|
||||
if algorithm := strings.TrimSpace(creds["tsig_algorithm"]); algorithm != "" {
|
||||
algorithm = strings.ToLower(algorithm)
|
||||
if !ValidTSIGAlgorithms[algorithm] {
|
||||
validAlgorithms := make([]string, 0, len(ValidTSIGAlgorithms))
|
||||
for alg := range ValidTSIGAlgorithms {
|
||||
validAlgorithms = append(validAlgorithms, alg)
|
||||
}
|
||||
return fmt.Errorf("tsig_algorithm must be one of: %s", strings.Join(validAlgorithms, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestCredentials attempts to verify credentials work.
|
||||
// For RFC 2136, we validate the format but cannot test without making actual DNS queries.
|
||||
func (p *RFC2136Provider) TestCredentials(creds map[string]string) error {
|
||||
return p.ValidateCredentials(creds)
|
||||
}
|
||||
|
||||
// SupportsMultiCredential indicates if the provider can handle zone-specific credentials.
|
||||
func (p *RFC2136Provider) SupportsMultiCredential() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// BuildCaddyConfig constructs the Caddy DNS challenge configuration.
|
||||
func (p *RFC2136Provider) BuildCaddyConfig(creds map[string]string) map[string]any {
|
||||
config := map[string]any{
|
||||
"name": "rfc2136",
|
||||
"nameserver": strings.TrimSpace(creds["nameserver"]),
|
||||
"tsig_key_name": strings.TrimSpace(creds["tsig_key_name"]),
|
||||
"tsig_key_secret": strings.TrimSpace(creds["tsig_key_secret"]),
|
||||
}
|
||||
|
||||
// Add port with default
|
||||
port := strings.TrimSpace(creds["port"])
|
||||
if port == "" {
|
||||
port = RFC2136DefaultPort
|
||||
}
|
||||
config["port"] = port
|
||||
|
||||
// Add algorithm with default
|
||||
algorithm := strings.TrimSpace(creds["tsig_algorithm"])
|
||||
if algorithm == "" {
|
||||
algorithm = RFC2136DefaultAlgorithm
|
||||
}
|
||||
config["tsig_algorithm"] = strings.ToLower(algorithm)
|
||||
|
||||
// Add zone if specified (optional - Caddy can auto-detect)
|
||||
if zone := strings.TrimSpace(creds["zone"]); zone != "" {
|
||||
config["zone"] = zone
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// BuildCaddyConfigForZone constructs config for a specific zone.
|
||||
func (p *RFC2136Provider) BuildCaddyConfigForZone(baseDomain string, creds map[string]string) map[string]any {
|
||||
config := p.BuildCaddyConfig(creds)
|
||||
// If zone is not explicitly set, use the base domain
|
||||
if _, hasZone := config["zone"]; !hasZone {
|
||||
config["zone"] = baseDomain
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
// PropagationTimeout returns the recommended DNS propagation wait time.
|
||||
func (p *RFC2136Provider) PropagationTimeout() time.Duration {
|
||||
return p.propagationTimeout
|
||||
}
|
||||
|
||||
// PollingInterval returns the recommended polling interval for DNS verification.
|
||||
func (p *RFC2136Provider) PollingInterval() time.Duration {
|
||||
return p.pollingInterval
|
||||
}
|
||||
|
||||
// GetPort returns the configured port or the default.
|
||||
func (p *RFC2136Provider) GetPort(creds map[string]string) string {
|
||||
if port := strings.TrimSpace(creds["port"]); port != "" {
|
||||
return port
|
||||
}
|
||||
return RFC2136DefaultPort
|
||||
}
|
||||
|
||||
// GetAlgorithm returns the configured algorithm or the default.
|
||||
func (p *RFC2136Provider) GetAlgorithm(creds map[string]string) string {
|
||||
if algorithm := strings.TrimSpace(creds["tsig_algorithm"]); algorithm != "" {
|
||||
return strings.ToLower(algorithm)
|
||||
}
|
||||
return RFC2136DefaultAlgorithm
|
||||
}
|
||||
Reference in New Issue
Block a user