Files
go-dns/client.go
T
Konrad NeitzelandCursor ad0bb4533e Document the package, exported constants, and remaining helpers.
godoc now covers ActionAdd/ActionDelete, internal helpers, and UDP-then-TCP UPDATE behaviour.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 20:57:02 +02:00

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
}