Files
Charon/backend/pkg/dnsprovider/custom/rfc2136_provider.go
2026-01-26 19:22:05 +00:00

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
}