- Created a comprehensive documentation file for DNS provider types, including RFC 2136, Webhook, and Script providers, detailing their use cases, configurations, and security notes. - Updated the DNSProviderForm component to handle new field types including select and textarea for better user input management. - Enhanced the DNS provider schemas to include new fields for script execution, webhook authentication, and RFC 2136 configurations, improving flexibility and usability.
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
|
|
}
|