Files

94 lines
2.7 KiB
Go

package godns
import (
"fmt"
"net"
"strings"
)
// TSIG is caller-supplied key material. The library does not generate keys
// or write BIND configuration.
// Name is the key name presented to the nameserver.
// Algorithm is a BIND name such as hmac-sha256 (also hmac-md5, hmac-sha1,
// hmac-sha224, hmac-sha384, and hmac-sha512).
// Secret is the base64 secret.
type TSIG struct {
Name string
Algorithm string
Secret string
}
// Client is one session: one nameserver, one zone, and one TSIG key.
// AXFR and Update both use that key. There is no unsigned mode.
type Client struct {
server string
zone string
tsig TSIG
}
// NewClient starts a session for one nameserver, zone, and TSIG key.
// server is a host or host:port. A missing port becomes 53.
// An IPv6 host must be written in brackets.
// It returns an error when server, zone, or any TSIG field is empty,
// when zone or the key name is not ASCII, or when server is a bare IPv6 address.
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
}