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:
Konrad Neitzel
2026-08-25 20:44:44 +02:00
co-authored by Cursor
parent afbe268cb5
commit 7055eac0ae
14 changed files with 1229 additions and 2 deletions
+81
View File
@@ -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
}