94 lines
2.7 KiB
Go
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
|
|
}
|