Implement AXFR over TCP and RFC 2136 UPDATE with caller TSIG.
Callers get a session API, typed RR builders, and BIND testcontainer proof so CheckDNS can stay off the DNS wire. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
package godns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TSIG is caller-supplied key material. go-dns does not generate keys.
|
||||
type TSIG struct {
|
||||
Name string
|
||||
Algorithm string
|
||||
Secret string
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user