godoc now covers ActionAdd/ActionDelete, internal helpers, and UDP-then-TCP UPDATE behaviour. Co-authored-by: Cursor <cursoragent@cursor.com>
85 lines
2.3 KiB
Go
85 lines
2.3 KiB
Go
package godns
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
)
|
|
|
|
// TSIG is caller-supplied key material. go-dns does not generate keys.
|
|
type TSIG struct {
|
|
Name string // key name presented to BIND
|
|
Algorithm string // BIND algorithm name, e.g. hmac-sha256
|
|
Secret string // base64 secret from the Key File
|
|
}
|
|
|
|
// Client is a session against one nameserver and zone, authenticated with TSIG.
|
|
type Client struct {
|
|
server string
|
|
zone string
|
|
tsig TSIG
|
|
}
|
|
|
|
// NewClient validates ASCII zone names and required TSIG fields.
|
|
// server is host or host:port; port 53 is assumed when omitted.
|
|
func NewClient(server, zone string, key TSIG) (*Client, error) {
|
|
if strings.TrimSpace(server) == "" {
|
|
return nil, fmt.Errorf("server is required")
|
|
}
|
|
if err := asciiName("zone", zone); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := validateTSIG(key); err != nil {
|
|
return nil, err
|
|
}
|
|
addr, err := withDNSPort(server)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Client{server: addr, zone: strings.TrimSpace(zone), tsig: key}, nil
|
|
}
|
|
|
|
// validateTSIG requires name, algorithm, and secret, and an ASCII key name.
|
|
func validateTSIG(key TSIG) error {
|
|
if strings.TrimSpace(key.Name) == "" {
|
|
return fmt.Errorf("TSIG name is required")
|
|
}
|
|
if strings.TrimSpace(key.Algorithm) == "" {
|
|
return fmt.Errorf("TSIG algorithm is required")
|
|
}
|
|
if strings.TrimSpace(key.Secret) == "" {
|
|
return fmt.Errorf("TSIG secret is required")
|
|
}
|
|
if err := asciiName("TSIG name", key.Name); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// withDNSPort appends :53 when server has no port.
|
|
func withDNSPort(server string) (string, error) {
|
|
host := strings.TrimSpace(server)
|
|
if _, _, err := net.SplitHostPort(host); err == nil {
|
|
return host, nil
|
|
}
|
|
// net.SplitHostPort fails for bare IPv6; require brackets or a hostname/IPv4.
|
|
if strings.Count(host, ":") > 1 && !strings.HasPrefix(host, "[") {
|
|
return "", fmt.Errorf("IPv6 server must be [host]:port or wrapped in brackets")
|
|
}
|
|
return net.JoinHostPort(host, "53"), nil
|
|
}
|
|
|
|
// asciiName rejects empty or non-ASCII DNS names.
|
|
func asciiName(label, name string) error {
|
|
trimmed := strings.TrimSpace(name)
|
|
if trimmed == "" {
|
|
return fmt.Errorf("%s is required", label)
|
|
}
|
|
for i := 0; i < len(trimmed); i++ {
|
|
if trimmed[i] > 127 {
|
|
return fmt.Errorf("%s must be ASCII, got %q", label, name)
|
|
}
|
|
}
|
|
return nil
|
|
}
|