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>
This commit is contained in:
Konrad Neitzel
2026-08-25 20:57:02 +02:00
co-authored by Cursor
parent f31924448b
commit ad0bb4533e
5 changed files with 21 additions and 4 deletions
+2
View File
@@ -71,6 +71,7 @@ func (c *Client) AXFR(ctx context.Context) (*TransferResult, error) {
}, nil }, nil
} }
// tsigAlgorithm maps a BIND algorithm name to miekg/dns constants.
func tsigAlgorithm(name string) string { func tsigAlgorithm(name string) string {
trimmed := strings.ToLower(strings.TrimSpace(name)) trimmed := strings.ToLower(strings.TrimSpace(name))
trimmed = strings.TrimSuffix(trimmed, ".") trimmed = strings.TrimSuffix(trimmed, ".")
@@ -92,6 +93,7 @@ func tsigAlgorithm(name string) string {
} }
} }
// timeoutFrom uses the context deadline, or 30s, with a 1s floor.
func timeoutFrom(ctx context.Context) time.Duration { func timeoutFrom(ctx context.Context) time.Duration {
deadline, ok := ctx.Deadline() deadline, ok := ctx.Deadline()
if !ok { if !ok {
+6 -3
View File
@@ -8,9 +8,9 @@ import (
// TSIG is caller-supplied key material. go-dns does not generate keys. // TSIG is caller-supplied key material. go-dns does not generate keys.
type TSIG struct { type TSIG struct {
Name string Name string // key name presented to BIND
Algorithm string Algorithm string // BIND algorithm name, e.g. hmac-sha256
Secret string Secret string // base64 secret from the Key File
} }
// Client is a session against one nameserver and zone, authenticated with TSIG. // Client is a session against one nameserver and zone, authenticated with TSIG.
@@ -39,6 +39,7 @@ func NewClient(server, zone string, key TSIG) (*Client, error) {
return &Client{server: addr, zone: strings.TrimSpace(zone), tsig: key}, nil 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 { func validateTSIG(key TSIG) error {
if strings.TrimSpace(key.Name) == "" { if strings.TrimSpace(key.Name) == "" {
return fmt.Errorf("TSIG name is required") return fmt.Errorf("TSIG name is required")
@@ -55,6 +56,7 @@ func validateTSIG(key TSIG) error {
return nil return nil
} }
// withDNSPort appends :53 when server has no port.
func withDNSPort(server string) (string, error) { func withDNSPort(server string) (string, error) {
host := strings.TrimSpace(server) host := strings.TrimSpace(server)
if _, _, err := net.SplitHostPort(host); err == nil { if _, _, err := net.SplitHostPort(host); err == nil {
@@ -67,6 +69,7 @@ func withDNSPort(server string) (string, error) {
return net.JoinHostPort(host, "53"), nil return net.JoinHostPort(host, "53"), nil
} }
// asciiName rejects empty or non-ASCII DNS names.
func asciiName(label, name string) error { func asciiName(label, name string) error {
trimmed := strings.TrimSpace(name) trimmed := strings.TrimSpace(name)
if trimmed == "" { if trimmed == "" {
+4
View File
@@ -0,0 +1,4 @@
// Package godns is a library for authoritative DNS against BIND-compatible
// nameservers: full zone transfer (AXFR) over TCP and RFC 2136 dynamic UPDATE,
// authenticated with caller-supplied TSIG. Domain names are ASCII only in v1.
package godns
+5
View File
@@ -124,6 +124,7 @@ func PTR(owner string, ttl uint32, target string) (Record, error) {
return fromRR(&dns.PTR{Hdr: rrHeader(owner, dns.TypePTR, ttl), Ptr: name}) return fromRR(&dns.PTR{Hdr: rrHeader(owner, dns.TypePTR, ttl), Ptr: name})
} }
// rrHeader builds an INET header. Owner ASCII is checked in fromRR.
func rrHeader(owner string, typ uint16, ttl uint32) dns.RR_Header { func rrHeader(owner string, typ uint16, ttl uint32) dns.RR_Header {
name, err := fqdnASCII(owner) name, err := fqdnASCII(owner)
if err != nil { if err != nil {
@@ -134,6 +135,7 @@ func rrHeader(owner string, typ uint16, ttl uint32) dns.RR_Header {
return dns.RR_Header{Name: name, Rrtype: typ, Class: dns.ClassINET, Ttl: ttl} return dns.RR_Header{Name: name, Rrtype: typ, Class: dns.ClassINET, Ttl: ttl}
} }
// fromRR maps a miekg/dns RR onto the public Record type.
func fromRR(rr dns.RR) (Record, error) { func fromRR(rr dns.RR) (Record, error) {
hdr := rr.Header() hdr := rr.Header()
if err := asciiName("owner", strings.TrimSuffix(hdr.Name, ".")); err != nil { if err := asciiName("owner", strings.TrimSuffix(hdr.Name, ".")); err != nil {
@@ -151,6 +153,7 @@ func fromRR(rr dns.RR) (Record, error) {
}, nil }, nil
} }
// recordToRR encodes a Record as a miekg/dns RR for UPDATE.
func recordToRR(rec Record) (dns.RR, error) { func recordToRR(rec Record) (dns.RR, error) {
if err := asciiName("owner", rec.Owner); err != nil { if err := asciiName("owner", rec.Owner); err != nil {
return nil, err return nil, err
@@ -162,6 +165,7 @@ func recordToRR(rec Record) (dns.RR, error) {
return rr, nil return rr, nil
} }
// fqdnASCII validates ASCII and returns a trailing-dot FQDN.
func fqdnASCII(name string) (string, error) { func fqdnASCII(name string) (string, error) {
if err := asciiName("name", name); err != nil { if err := asciiName("name", name); err != nil {
return "", err return "", err
@@ -169,6 +173,7 @@ func fqdnASCII(name string) (string, error) {
return dns.Fqdn(strings.TrimSpace(name)), nil return dns.Fqdn(strings.TrimSpace(name)), nil
} }
// isASCII reports whether s contains only bytes <= 127.
func isASCII(s string) bool { func isASCII(s string) bool {
for i := 0; i < len(s); i++ { for i := 0; i < len(s); i++ {
if s[i] > 127 { if s[i] > 127 {
+4 -1
View File
@@ -12,7 +12,9 @@ import (
type Action int type Action int
const ( const (
// ActionAdd inserts a record.
ActionAdd Action = iota ActionAdd Action = iota
// ActionDelete removes a matching RR (name, type, and rdata).
ActionDelete ActionDelete
) )
@@ -38,7 +40,7 @@ type UpdateResult struct {
} }
// Update applies RFC 2136 changes. It tries UDP first and retries on TCP when // Update applies RFC 2136 changes. It tries UDP first and retries on TCP when
// the response is truncated. // the UDP exchange fails or the response is truncated.
func (c *Client) Update(ctx context.Context, changes ...Change) (*UpdateResult, error) { func (c *Client) Update(ctx context.Context, changes ...Change) (*UpdateResult, error) {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return nil, err return nil, err
@@ -72,6 +74,7 @@ func (c *Client) Update(ctx context.Context, changes ...Change) (*UpdateResult,
return &UpdateResult{Rcode: dns.RcodeToString[resp.Rcode]}, nil return &UpdateResult{Rcode: dns.RcodeToString[resp.Rcode]}, nil
} }
// updateMessage builds a TSIG-signed RFC 2136 UPDATE message.
func (c *Client) updateMessage(changes []Change) (*dns.Msg, error) { func (c *Client) updateMessage(changes []Change) (*dns.Msg, error) {
zone := dns.Fqdn(c.zone) zone := dns.Fqdn(c.zone)
msg := new(dns.Msg) msg := new(dns.Msg)