272 lines
8.5 KiB
Go
272 lines
8.5 KiB
Go
// 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
|
|
}
|