Merge branch 'feature/dnscheck-godns-nc-files'
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
|
||||
# Test / coverage
|
||||
*.test
|
||||
!testdata/bind/db.example.test
|
||||
*.out
|
||||
coverage.html
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ Go library for authoritative DNS operations against BIND (and compatible) namese
|
||||
|
||||
## Status
|
||||
|
||||
Skeleton. Design is in [docs/project-specification.md](docs/project-specification.md). Implementation Issues live in the ExApp Workspace (`docs/issues/`), not in this repository.
|
||||
Library: AXFR over TCP, RFC 2136 UPDATE (UDP with TCP fallback), caller-supplied TSIG, typed RR builders. ASCII names only.
|
||||
|
||||
## Module
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package godns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// TransferResult is a completed AXFR plus SOA serial metadata.
|
||||
type TransferResult struct {
|
||||
Zone Zone
|
||||
Serial uint32
|
||||
}
|
||||
|
||||
// AXFR fetches the full zone over TCP, signed with the session TSIG key.
|
||||
func (c *Client) AXFR(ctx context.Context) (*TransferResult, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
zone := dns.Fqdn(c.zone)
|
||||
msg := new(dns.Msg)
|
||||
msg.SetAxfr(zone)
|
||||
msg.SetTsig(dns.Fqdn(c.tsig.Name), tsigAlgorithm(c.tsig.Algorithm), 300, time.Now().Unix())
|
||||
|
||||
tr := &dns.Transfer{
|
||||
TsigSecret: map[string]string{dns.Fqdn(c.tsig.Name): c.tsig.Secret},
|
||||
DialTimeout: timeoutFrom(ctx),
|
||||
ReadTimeout: timeoutFrom(ctx),
|
||||
}
|
||||
|
||||
envCh, err := tr.In(msg, c.server)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("AXFR to %s: %w", c.server, err)
|
||||
}
|
||||
|
||||
var records []Record
|
||||
var serial uint32
|
||||
sawSOA := false
|
||||
for env := range envCh {
|
||||
if env.Error != nil {
|
||||
return nil, fmt.Errorf("AXFR stream: %w", env.Error)
|
||||
}
|
||||
for _, rr := range env.RR {
|
||||
if soa, ok := rr.(*dns.SOA); ok {
|
||||
if sawSOA {
|
||||
continue // trailing SOA ends the transfer
|
||||
}
|
||||
sawSOA = true
|
||||
serial = soa.Serial
|
||||
}
|
||||
rec, err := fromRR(rr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
records = append(records, rec)
|
||||
}
|
||||
}
|
||||
if !sawSOA {
|
||||
return nil, fmt.Errorf("AXFR returned no SOA for %s", zone)
|
||||
}
|
||||
return &TransferResult{
|
||||
Zone: Zone{
|
||||
Origin: zone,
|
||||
Serial: serial,
|
||||
Records: records,
|
||||
},
|
||||
Serial: serial,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// tsigAlgorithm maps a BIND algorithm name to miekg/dns constants.
|
||||
func tsigAlgorithm(name string) string {
|
||||
trimmed := strings.ToLower(strings.TrimSpace(name))
|
||||
trimmed = strings.TrimSuffix(trimmed, ".")
|
||||
switch trimmed {
|
||||
case "hmac-md5":
|
||||
return dns.HmacMD5
|
||||
case "hmac-sha1":
|
||||
return dns.HmacSHA1
|
||||
case "hmac-sha224":
|
||||
return dns.HmacSHA224
|
||||
case "hmac-sha256":
|
||||
return dns.HmacSHA256
|
||||
case "hmac-sha384":
|
||||
return dns.HmacSHA384
|
||||
case "hmac-sha512":
|
||||
return dns.HmacSHA512
|
||||
default:
|
||||
return dns.Fqdn(trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
// timeoutFrom uses the context deadline, or 30s, with a 1s floor.
|
||||
func timeoutFrom(ctx context.Context) time.Duration {
|
||||
deadline, ok := ctx.Deadline()
|
||||
if !ok {
|
||||
return 30 * time.Second
|
||||
}
|
||||
d := time.Until(deadline)
|
||||
if d < time.Second {
|
||||
return time.Second
|
||||
}
|
||||
return d
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
//go:build integration
|
||||
|
||||
package godns_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
godns "gitea.neitzel.de/konrad/go-dns"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/api/types/network"
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
"github.com/testcontainers/testcontainers-go/wait"
|
||||
)
|
||||
|
||||
func TestAXFRFetchesFullZoneOverTCPWithTSIG(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
server := startBIND(ctx, t)
|
||||
client, err := godns.NewClient(server, "example.test", fixtureTSIG())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
result, err := client.AXFR(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.Serial != 1 {
|
||||
t.Fatalf("serial: %d", result.Serial)
|
||||
}
|
||||
if !hasRecord(result.Zone.Records, "www.example.test.", "A", "192.0.2.10") {
|
||||
t.Fatalf("missing www A record in %#v", result.Zone.Records)
|
||||
}
|
||||
if !hasRecord(result.Zone.Records, "ns.example.test.", "A", "192.0.2.1") {
|
||||
t.Fatalf("missing ns A record in %#v", result.Zone.Records)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateAddAndDeleteAgainstBIND(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
server := startBIND(ctx, t)
|
||||
client, err := godns.NewClient(server, "example.test", fixtureTSIG())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
added, err := godns.A("new.example.test", 300, net.ParseIP("192.0.2.99"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := client.Update(ctx, godns.Add(added)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
afterAdd, err := client.AXFR(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !hasRecord(afterAdd.Zone.Records, "new.example.test.", "A", "192.0.2.99") {
|
||||
t.Fatalf("add did not appear in AXFR: %#v", afterAdd.Zone.Records)
|
||||
}
|
||||
|
||||
if _, err := client.Update(ctx, godns.Delete(added)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
afterDelete, err := client.AXFR(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if hasRecord(afterDelete.Zone.Records, "new.example.test.", "A", "192.0.2.99") {
|
||||
t.Fatal("delete left the record in AXFR")
|
||||
}
|
||||
}
|
||||
|
||||
func startBIND(ctx context.Context, t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
_, thisFile, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
}
|
||||
bindDir := filepath.Join(filepath.Dir(thisFile), "testdata", "bind")
|
||||
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hostPort := listener.Addr().(*net.TCPAddr).Port
|
||||
if err := listener.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tcp53 := network.MustParsePort("53/tcp")
|
||||
udp53 := network.MustParsePort("53/udp")
|
||||
binding := network.PortBinding{
|
||||
HostIP: netip.MustParseAddr("127.0.0.1"),
|
||||
HostPort: strconv.Itoa(hostPort),
|
||||
}
|
||||
|
||||
req := testcontainers.ContainerRequest{
|
||||
FromDockerfile: testcontainers.FromDockerfile{
|
||||
Context: bindDir,
|
||||
Dockerfile: "Dockerfile",
|
||||
},
|
||||
ExposedPorts: []string{"53/tcp", "53/udp"},
|
||||
WaitingFor: wait.ForListeningPort("53/tcp").WithStartupTimeout(2 * time.Minute),
|
||||
HostConfigModifier: func(hc *container.HostConfig) {
|
||||
hc.PortBindings = network.PortMap{
|
||||
tcp53: {binding},
|
||||
udp53: {binding},
|
||||
}
|
||||
},
|
||||
}
|
||||
ctr, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
|
||||
ContainerRequest: req,
|
||||
Started: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("start BIND: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if t.Failed() {
|
||||
reader, logErr := ctr.Logs(context.Background())
|
||||
if logErr == nil {
|
||||
defer reader.Close()
|
||||
data, _ := io.ReadAll(reader)
|
||||
t.Logf("BIND logs:\n%s", data)
|
||||
}
|
||||
}
|
||||
_ = ctr.Terminate(context.Background())
|
||||
})
|
||||
|
||||
return net.JoinHostPort("127.0.0.1", strconv.Itoa(hostPort))
|
||||
}
|
||||
|
||||
func hasRecord(records []godns.Record, owner, typ, rdata string) bool {
|
||||
for _, rec := range records {
|
||||
if rec.Owner == owner && rec.Type == typ && rec.RData == rdata {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if os.Getenv("DOCKER_HOST") == "" {
|
||||
sock := filepath.Join(os.Getenv("HOME"), ".rd", "docker.sock")
|
||||
if _, err := os.Stat(sock); err == nil {
|
||||
_ = os.Setenv("DOCKER_HOST", "unix://"+sock)
|
||||
}
|
||||
}
|
||||
// Ryuk cannot mount Rancher Desktop's user-scoped docker.sock.
|
||||
if os.Getenv("TESTCONTAINERS_RYUK_DISABLED") == "" {
|
||||
_ = os.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true")
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package godns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TSIG is caller-supplied key material. go-dns does not generate keys.
|
||||
type TSIG struct {
|
||||
Name string // key name presented to BIND
|
||||
Algorithm string // BIND algorithm name, e.g. hmac-sha256
|
||||
Secret string // base64 secret from the Key File
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package godns_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
godns "gitea.neitzel.de/konrad/go-dns"
|
||||
)
|
||||
|
||||
func TestNewClientRejectsNonASCIIZone(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := godns.NewClient("127.0.0.1:53", "exämple.test", fixtureTSIG())
|
||||
if err == nil {
|
||||
t.Fatal("expected error for non-ASCII zone name")
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "ascii") {
|
||||
t.Fatalf("error should mention ASCII, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClientRejectsEmptyTSIG(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := godns.NewClient("127.0.0.1:53", "example.test", godns.TSIG{})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for empty TSIG")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClientAcceptsASCIIZone(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, err := godns.NewClient("127.0.0.1", "example.test", fixtureTSIG())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if client == nil {
|
||||
t.Fatal("expected client")
|
||||
}
|
||||
}
|
||||
|
||||
const fixtureSecret = "dGVzdC1maXh0dXJlLWhtYWMtc2hhMjU2LWtleSEhISE="
|
||||
|
||||
func fixtureTSIG() godns.TSIG {
|
||||
return godns.TSIG{
|
||||
Name: "go-dns-test",
|
||||
Algorithm: "hmac-sha256",
|
||||
Secret: fixtureSecret,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -19,7 +19,7 @@ Callers perform updates and full zone reads in Go with typed **zone data structu
|
||||
| Public package | **`godns`** (`package godns` at module root) |
|
||||
| Git remote | `ssh://git@gitea.neitzel.de:2222/konrad/go-dns.git` |
|
||||
|
||||
This is a **Library** in the ExApp Workspace. Workspace Go rules live in `.cursor/rules/` (`go.mdc`, `go-testing.mdc`). Specs and Issues for this Library live in the Workspace under `docs/issues/`, not in this repository.
|
||||
This is a **Library** in the ExApp Workspace. Workspace Go rules live in `.cursor/rules/` (`go.mdc`, `go-testing.mdc`). Specs, Issues, and ADRs for this Library live in the Workspace (`docs/issues/`, `docs/adr/go-dns/`), not in this repository.
|
||||
|
||||
## Target environment
|
||||
|
||||
|
||||
@@ -1,3 +1,62 @@
|
||||
module gitea.neitzel.de/konrad/go-dns
|
||||
|
||||
go 1.26.4
|
||||
|
||||
require (
|
||||
github.com/miekg/dns v1.1.73
|
||||
github.com/moby/moby/api v1.55.0
|
||||
github.com/testcontainers/testcontainers-go v0.44.0
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.7.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/ebitengine/purego v0.10.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.1.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.18.6 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect
|
||||
github.com/magiconair/properties v1.8.10 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/go-archive v0.2.0 // indirect
|
||||
github.com/moby/moby/client v0.5.0 // indirect
|
||||
github.com/moby/patternmatcher v0.6.1 // indirect
|
||||
github.com/moby/sys/sequential v0.7.0 // indirect
|
||||
github.com/moby/sys/user v0.4.0 // indirect
|
||||
github.com/moby/sys/userns v0.1.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.26.6 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||
github.com/stretchr/testify v1.11.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.4.0 // indirect
|
||||
github.com/tklauser/numcpus v0.12.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect
|
||||
go.opentelemetry.io/otel v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
||||
golang.org/x/crypto v0.54.0 // indirect
|
||||
golang.org/x/net v0.57.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
|
||||
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
|
||||
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
|
||||
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
|
||||
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY=
|
||||
github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
|
||||
github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak=
|
||||
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/miekg/dns v1.1.73 h1:uhT8nJxmTrPJYClxVxTCX+CVn6qnzSiybRk72Z6DgrE=
|
||||
github.com/miekg/dns v1.1.73/go.mod h1:RW2Obtfd5NZHvOFe3zYG0W8koWOQtAzyHaLo8vASBuQ=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
|
||||
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
|
||||
github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc=
|
||||
github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
||||
github.com/moby/moby/client v0.5.0 h1:5XhyPk2fuOWf6RlSFa3MkIIgDZkF25xToXW8Q/BH7cc=
|
||||
github.com/moby/moby/client v0.5.0/go.mod h1:rcVpF8ncl9vo5gaIBdol6CnbEtSj1uxMvEV/UrykF/s=
|
||||
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
|
||||
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8=
|
||||
github.com/moby/sys/sequential v0.7.0/go.mod h1:NfSTAp6V3fw4tmkD62PEcOKeZKquXT8VKCkf7aVR79o=
|
||||
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
||||
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
||||
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/shirou/gopsutil/v4 v4.26.6 h1:Mzr/npDtQC/xpeEuQKHZt8Zo9CmPvhTj8nkR8w5TLDs=
|
||||
github.com/shirou/gopsutil/v4 v4.26.6/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/testcontainers/testcontainers-go v0.44.0 h1:/Fwh6HY1mIikhnm9e7HwoxGycx0lzRAE0f5VQpjFxzI=
|
||||
github.com/testcontainers/testcontainers-go v0.44.0/go.mod h1:IcnwQrYTO86xHXu5bvMaBH7ATlbS3Qn1M1QWW3c66rE=
|
||||
github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU=
|
||||
github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI=
|
||||
github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4=
|
||||
github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
|
||||
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
|
||||
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
|
||||
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
||||
@@ -0,0 +1,184 @@
|
||||
package godns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Record is a single resource record with presentation-format rdata.
|
||||
type Record struct {
|
||||
Owner string
|
||||
Class string
|
||||
Type string
|
||||
TTL uint32
|
||||
RData string
|
||||
}
|
||||
|
||||
// RRSet groups records that share owner, class, and type.
|
||||
type RRSet struct {
|
||||
Owner string
|
||||
Class string
|
||||
Type string
|
||||
Records []Record
|
||||
}
|
||||
|
||||
// Zone is a complete in-memory zone from AXFR (or assembled records).
|
||||
type Zone struct {
|
||||
Origin string
|
||||
Serial uint32
|
||||
Records []Record
|
||||
}
|
||||
|
||||
// RRSets groups Zone records by owner/class/type, preserving first-seen order.
|
||||
func (z Zone) RRSets() []RRSet {
|
||||
type key struct{ owner, class, typ string }
|
||||
order := make([]key, 0)
|
||||
grouped := make(map[key][]Record)
|
||||
for _, rec := range z.Records {
|
||||
k := key{rec.Owner, rec.Class, rec.Type}
|
||||
if _, ok := grouped[k]; !ok {
|
||||
order = append(order, k)
|
||||
}
|
||||
grouped[k] = append(grouped[k], rec)
|
||||
}
|
||||
out := make([]RRSet, 0, len(order))
|
||||
for _, k := range order {
|
||||
out = append(out, RRSet{Owner: k.owner, Class: k.class, Type: k.typ, Records: grouped[k]})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// A builds an IPv4 address record.
|
||||
func A(owner string, ttl uint32, ip net.IP) (Record, error) {
|
||||
v4 := ip.To4()
|
||||
if v4 == nil {
|
||||
return Record{}, fmt.Errorf("A record requires an IPv4 address")
|
||||
}
|
||||
return fromRR(&dns.A{Hdr: rrHeader(owner, dns.TypeA, ttl), A: v4})
|
||||
}
|
||||
|
||||
// AAAA builds an IPv6 address record.
|
||||
func AAAA(owner string, ttl uint32, ip net.IP) (Record, error) {
|
||||
if ip == nil || ip.To16() == nil || ip.To4() != nil {
|
||||
return Record{}, fmt.Errorf("AAAA record requires an IPv6 address")
|
||||
}
|
||||
return fromRR(&dns.AAAA{Hdr: rrHeader(owner, dns.TypeAAAA, ttl), AAAA: ip.To16()})
|
||||
}
|
||||
|
||||
// MX builds a mail-exchanger record.
|
||||
func MX(owner string, ttl uint32, pref uint16, target string) (Record, error) {
|
||||
name, err := fqdnASCII(target)
|
||||
if err != nil {
|
||||
return Record{}, err
|
||||
}
|
||||
return fromRR(&dns.MX{Hdr: rrHeader(owner, dns.TypeMX, ttl), Preference: pref, Mx: name})
|
||||
}
|
||||
|
||||
// TXT builds a text record.
|
||||
func TXT(owner string, ttl uint32, text string) (Record, error) {
|
||||
return fromRR(&dns.TXT{Hdr: rrHeader(owner, dns.TypeTXT, ttl), Txt: []string{text}})
|
||||
}
|
||||
|
||||
// CNAME builds a canonical-name record.
|
||||
func CNAME(owner string, ttl uint32, target string) (Record, error) {
|
||||
name, err := fqdnASCII(target)
|
||||
if err != nil {
|
||||
return Record{}, err
|
||||
}
|
||||
return fromRR(&dns.CNAME{Hdr: rrHeader(owner, dns.TypeCNAME, ttl), Target: name})
|
||||
}
|
||||
|
||||
// NS builds a nameserver record.
|
||||
func NS(owner string, ttl uint32, target string) (Record, error) {
|
||||
name, err := fqdnASCII(target)
|
||||
if err != nil {
|
||||
return Record{}, err
|
||||
}
|
||||
return fromRR(&dns.NS{Hdr: rrHeader(owner, dns.TypeNS, ttl), Ns: name})
|
||||
}
|
||||
|
||||
// SRV builds a service locator record.
|
||||
func SRV(owner string, ttl uint32, prio, weight, port uint16, target string) (Record, error) {
|
||||
name, err := fqdnASCII(target)
|
||||
if err != nil {
|
||||
return Record{}, err
|
||||
}
|
||||
return fromRR(&dns.SRV{
|
||||
Hdr: rrHeader(owner, dns.TypeSRV, ttl),
|
||||
Priority: prio,
|
||||
Weight: weight,
|
||||
Port: port,
|
||||
Target: name,
|
||||
})
|
||||
}
|
||||
|
||||
// PTR builds a pointer record.
|
||||
func PTR(owner string, ttl uint32, target string) (Record, error) {
|
||||
name, err := fqdnASCII(target)
|
||||
if err != nil {
|
||||
return Record{}, err
|
||||
}
|
||||
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 {
|
||||
name, err := fqdnASCII(owner)
|
||||
if err != nil {
|
||||
// fromRR is not called on this path; builders call fqdn via fromRR after Header is set.
|
||||
// Validate in fromRR instead so the error is returned to the caller.
|
||||
name = owner
|
||||
}
|
||||
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) {
|
||||
hdr := rr.Header()
|
||||
if err := asciiName("owner", strings.TrimSuffix(hdr.Name, ".")); err != nil {
|
||||
return Record{}, err
|
||||
}
|
||||
if hdr.Name == "" || !isASCII(hdr.Name) {
|
||||
return Record{}, fmt.Errorf("owner must be ASCII, got %q", hdr.Name)
|
||||
}
|
||||
return Record{
|
||||
Owner: dns.Fqdn(hdr.Name),
|
||||
Class: dns.ClassToString[hdr.Class],
|
||||
Type: dns.TypeToString[hdr.Rrtype],
|
||||
TTL: hdr.Ttl,
|
||||
RData: strings.TrimSpace(strings.TrimPrefix(rr.String(), hdr.String())),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// recordToRR encodes a Record as a miekg/dns RR for UPDATE.
|
||||
func recordToRR(rec Record) (dns.RR, error) {
|
||||
if err := asciiName("owner", rec.Owner); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rr, err := dns.NewRR(fmt.Sprintf("%s %d %s %s %s", dns.Fqdn(rec.Owner), rec.TTL, rec.Class, rec.Type, rec.RData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("encode %s %s: %w", rec.Type, rec.Owner, err)
|
||||
}
|
||||
return rr, nil
|
||||
}
|
||||
|
||||
// fqdnASCII validates ASCII and returns a trailing-dot FQDN.
|
||||
func fqdnASCII(name string) (string, error) {
|
||||
if err := asciiName("name", name); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return dns.Fqdn(strings.TrimSpace(name)), nil
|
||||
}
|
||||
|
||||
// isASCII reports whether s contains only bytes <= 127.
|
||||
func isASCII(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] > 127 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
package godns_test
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
godns "gitea.neitzel.de/konrad/go-dns"
|
||||
)
|
||||
|
||||
func TestABuilder(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rec, err := godns.A("www.example.test", 300, net.ParseIP("192.0.2.10"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rec.Owner != "www.example.test." {
|
||||
t.Fatalf("owner: %q", rec.Owner)
|
||||
}
|
||||
if rec.Type != "A" {
|
||||
t.Fatalf("type: %q", rec.Type)
|
||||
}
|
||||
if rec.TTL != 300 {
|
||||
t.Fatalf("ttl: %d", rec.TTL)
|
||||
}
|
||||
if rec.RData != "192.0.2.10" {
|
||||
t.Fatalf("rdata: %q", rec.RData)
|
||||
}
|
||||
if rec.Class != "IN" {
|
||||
t.Fatalf("class: %q", rec.Class)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAAAABuilder(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rec, err := godns.AAAA("www.example.test", 300, net.ParseIP("2001:db8::10"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rec.Type != "AAAA" {
|
||||
t.Fatalf("type: %q", rec.Type)
|
||||
}
|
||||
if rec.RData != "2001:db8::10" {
|
||||
t.Fatalf("rdata: %q", rec.RData)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMXBuilder(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rec, err := godns.MX("example.test", 3600, 10, "mail.example.test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rec.Type != "MX" {
|
||||
t.Fatalf("type: %q", rec.Type)
|
||||
}
|
||||
if rec.RData != "10 mail.example.test." {
|
||||
t.Fatalf("rdata: %q", rec.RData)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTXTBuilder(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rec, err := godns.TXT("example.test", 3600, "v=spf1 -all")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rec.Type != "TXT" {
|
||||
t.Fatalf("type: %q", rec.Type)
|
||||
}
|
||||
if rec.RData != `"v=spf1 -all"` {
|
||||
t.Fatalf("rdata: %q", rec.RData)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCNAMEBuilder(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rec, err := godns.CNAME("www.example.test", 3600, "example.test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rec.Type != "CNAME" {
|
||||
t.Fatalf("type: %q", rec.Type)
|
||||
}
|
||||
if rec.RData != "example.test." {
|
||||
t.Fatalf("rdata: %q", rec.RData)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNSBuilder(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rec, err := godns.NS("example.test", 3600, "ns.example.test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rec.Type != "NS" {
|
||||
t.Fatalf("type: %q", rec.Type)
|
||||
}
|
||||
if rec.RData != "ns.example.test." {
|
||||
t.Fatalf("rdata: %q", rec.RData)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSRVBuilder(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rec, err := godns.SRV("_sip._tcp.example.test", 3600, 10, 20, 5060, "sip.example.test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rec.Type != "SRV" {
|
||||
t.Fatalf("type: %q", rec.Type)
|
||||
}
|
||||
if rec.RData != "10 20 5060 sip.example.test." {
|
||||
t.Fatalf("rdata: %q", rec.RData)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPTRBuilder(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rec, err := godns.PTR("1.2.0.192.in-addr.arpa", 3600, "www.example.test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rec.Type != "PTR" {
|
||||
t.Fatalf("type: %q", rec.Type)
|
||||
}
|
||||
if rec.RData != "www.example.test." {
|
||||
t.Fatalf("rdata: %q", rec.RData)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderRejectsNonASCIIOwner(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := godns.A("wüw.example.test", 300, net.ParseIP("192.0.2.10"))
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "ascii") {
|
||||
t.Fatalf("error should mention ASCII, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZoneGroupsRRSets(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
a1, err := godns.A("www.example.test", 300, net.ParseIP("192.0.2.10"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a2, err := godns.A("www.example.test", 300, net.ParseIP("192.0.2.11"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mx, err := godns.MX("example.test", 3600, 10, "mail.example.test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
zone := godns.Zone{
|
||||
Origin: "example.test.",
|
||||
Serial: 1,
|
||||
Records: []godns.Record{a1, a2, mx},
|
||||
}
|
||||
sets := zone.RRSets()
|
||||
if len(sets) != 2 {
|
||||
t.Fatalf("expected 2 RRsets, got %d", len(sets))
|
||||
}
|
||||
}
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
FROM ubuntu:24.04
|
||||
RUN apt-get update \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends bind9 \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& mkdir -p /run/named /var/cache/bind \
|
||||
&& chown -R bind:bind /run/named /var/cache/bind
|
||||
COPY named.conf /etc/bind/named.conf
|
||||
COPY db.example.test /var/cache/bind/db.example.test
|
||||
RUN chown bind:bind /etc/bind/named.conf /var/cache/bind/db.example.test \
|
||||
&& chmod 644 /etc/bind/named.conf \
|
||||
&& chmod 664 /var/cache/bind/db.example.test
|
||||
EXPOSE 53/tcp 53/udp
|
||||
CMD ["/usr/sbin/named", "-f", "-g", "-u", "bind", "-c", "/etc/bind/named.conf"]
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
$TTL 3600
|
||||
@ IN SOA ns.example.test. hostmaster.example.test. (
|
||||
1
|
||||
3600
|
||||
600
|
||||
86400
|
||||
3600 )
|
||||
IN NS ns.example.test.
|
||||
ns IN A 192.0.2.1
|
||||
www IN A 192.0.2.10
|
||||
Vendored
+33
@@ -0,0 +1,33 @@
|
||||
options {
|
||||
directory "/var/cache/bind";
|
||||
listen-on port 53 { any; };
|
||||
listen-on-v6 { none; };
|
||||
allow-query { any; };
|
||||
recursion no;
|
||||
dnssec-validation no;
|
||||
pid-file none;
|
||||
};
|
||||
|
||||
logging {
|
||||
channel stderr_log {
|
||||
stderr;
|
||||
severity info;
|
||||
print-time yes;
|
||||
print-category yes;
|
||||
};
|
||||
category default { stderr_log; };
|
||||
category update { stderr_log; };
|
||||
category update-security { stderr_log; };
|
||||
};
|
||||
|
||||
key "go-dns-test" {
|
||||
algorithm hmac-sha256;
|
||||
secret "dGVzdC1maXh0dXJlLWhtYWMtc2hhMjU2LWtleSEhISE=";
|
||||
};
|
||||
|
||||
zone "example.test" {
|
||||
type master;
|
||||
file "/var/cache/bind/db.example.test";
|
||||
allow-update { key "go-dns-test"; };
|
||||
allow-transfer { key "go-dns-test"; };
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
package godns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Action is one RFC 2136 UPDATE operation.
|
||||
type Action int
|
||||
|
||||
const (
|
||||
// ActionAdd inserts a record.
|
||||
ActionAdd Action = iota
|
||||
// ActionDelete removes a matching RR (name, type, and rdata).
|
||||
ActionDelete
|
||||
)
|
||||
|
||||
// Change is a typed add or delete of one record.
|
||||
type Change struct {
|
||||
Action Action
|
||||
Record Record
|
||||
}
|
||||
|
||||
// Add builds an insert change.
|
||||
func Add(rec Record) Change {
|
||||
return Change{Action: ActionAdd, Record: rec}
|
||||
}
|
||||
|
||||
// Delete builds a delete-by-rr change (name, type, and rdata).
|
||||
func Delete(rec Record) Change {
|
||||
return Change{Action: ActionDelete, Record: rec}
|
||||
}
|
||||
|
||||
// UpdateResult is the nameserver's response to an UPDATE.
|
||||
type UpdateResult struct {
|
||||
Rcode string
|
||||
}
|
||||
|
||||
// Update applies RFC 2136 changes. It tries UDP first and retries on TCP when
|
||||
// the UDP exchange fails or the response is truncated.
|
||||
func (c *Client) Update(ctx context.Context, changes ...Change) (*UpdateResult, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(changes) == 0 {
|
||||
return &UpdateResult{Rcode: dns.RcodeToString[dns.RcodeSuccess]}, nil
|
||||
}
|
||||
|
||||
msg, err := c.updateMessage(changes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := &dns.Client{
|
||||
Net: "udp",
|
||||
TsigSecret: map[string]string{dns.Fqdn(c.tsig.Name): c.tsig.Secret},
|
||||
Timeout: timeoutFrom(ctx),
|
||||
}
|
||||
udpMsg := msg.Copy()
|
||||
resp, _, err := client.ExchangeContext(ctx, udpMsg, c.server)
|
||||
if err != nil || (resp != nil && resp.Truncated) {
|
||||
client.Net = "tcp"
|
||||
resp, _, err = client.ExchangeContext(ctx, msg.Copy(), c.server)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("UPDATE to %s: %w", c.server, err)
|
||||
}
|
||||
if resp.Rcode != dns.RcodeSuccess {
|
||||
return nil, fmt.Errorf("UPDATE rcode %s", dns.RcodeToString[resp.Rcode])
|
||||
}
|
||||
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) {
|
||||
zone := dns.Fqdn(c.zone)
|
||||
msg := new(dns.Msg)
|
||||
msg.SetUpdate(zone)
|
||||
for _, change := range changes {
|
||||
rr, err := recordToRR(change.Record)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch change.Action {
|
||||
case ActionAdd:
|
||||
msg.Insert([]dns.RR{rr})
|
||||
case ActionDelete:
|
||||
msg.Remove([]dns.RR{rr})
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported update action %d", change.Action)
|
||||
}
|
||||
}
|
||||
msg.SetTsig(dns.Fqdn(c.tsig.Name), tsigAlgorithm(c.tsig.Algorithm), 300, time.Now().Unix())
|
||||
return msg, nil
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
package godns_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
godns "gitea.neitzel.de/konrad/go-dns"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func TestUpdateFallsBackToTCPWhenUDPTruncated(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var udpHits, tcpHits atomic.Int32
|
||||
mux := dns.NewServeMux()
|
||||
mux.HandleFunc("example.test.", func(w dns.ResponseWriter, r *dns.Msg) {
|
||||
if r.IsTsig() == nil {
|
||||
t.Error("UPDATE request missing TSIG")
|
||||
}
|
||||
msg := new(dns.Msg)
|
||||
msg.SetReply(r)
|
||||
if _, ok := w.RemoteAddr().(*net.UDPAddr); ok {
|
||||
udpHits.Add(1)
|
||||
msg.Truncated = true
|
||||
_ = w.WriteMsg(msg)
|
||||
return
|
||||
}
|
||||
tcpHits.Add(1)
|
||||
_ = w.WriteMsg(msg)
|
||||
})
|
||||
|
||||
udp, tcp := startDNSServers(t, mux)
|
||||
client, err := godns.NewClient(udp, "example.test", fixtureTSIG())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = tcp
|
||||
|
||||
rec, err := godns.A("new.example.test", 300, net.ParseIP("192.0.2.99"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
result, err := client.Update(ctx, godns.Add(rec))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.Rcode != "NOERROR" {
|
||||
t.Fatalf("rcode: %s", result.Rcode)
|
||||
}
|
||||
if udpHits.Load() < 1 {
|
||||
t.Fatal("expected a UDP attempt")
|
||||
}
|
||||
if tcpHits.Load() < 1 {
|
||||
t.Fatal("expected TCP fallback")
|
||||
}
|
||||
}
|
||||
|
||||
func startDNSServers(t *testing.T, handler dns.Handler) (udpAddr, tcpAddr string) {
|
||||
t.Helper()
|
||||
|
||||
secrets := map[string]string{"go-dns-test.": fixtureSecret}
|
||||
|
||||
acceptUpdate := func(dh dns.Header) dns.MsgAcceptAction {
|
||||
return dns.MsgAccept
|
||||
}
|
||||
udpSrv := &dns.Server{Addr: "127.0.0.1:0", Net: "udp", Handler: handler, TsigSecret: secrets, MsgAcceptFunc: acceptUpdate}
|
||||
udpReady := make(chan struct{})
|
||||
udpSrv.NotifyStartedFunc = func() { close(udpReady) }
|
||||
go func() { _ = udpSrv.ListenAndServe() }()
|
||||
<-udpReady
|
||||
t.Cleanup(func() { _ = udpSrv.Shutdown() })
|
||||
|
||||
udpAddr = udpSrv.PacketConn.LocalAddr().String()
|
||||
_, port, err := net.SplitHostPort(udpAddr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tcpSrv := &dns.Server{Addr: "127.0.0.1:" + port, Net: "tcp", Handler: handler, TsigSecret: secrets, MsgAcceptFunc: acceptUpdate}
|
||||
tcpReady := make(chan struct{})
|
||||
tcpSrv.NotifyStartedFunc = func() { close(tcpReady) }
|
||||
go func() { _ = tcpSrv.ListenAndServe() }()
|
||||
<-tcpReady
|
||||
t.Cleanup(func() { _ = tcpSrv.Shutdown() })
|
||||
|
||||
return udpAddr, tcpSrv.Listener.Addr().String()
|
||||
}
|
||||
|
||||
func TestAddChangeDescribesInsert(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rec, err := godns.A("www.example.test", 300, net.ParseIP("192.0.2.10"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
change := godns.Add(rec)
|
||||
if change.Action != godns.ActionAdd {
|
||||
t.Fatalf("action: %v", change.Action)
|
||||
}
|
||||
if change.Record.Type != "A" {
|
||||
t.Fatalf("record: %+v", change.Record)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteChangeDescribesRemove(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rec, err := godns.TXT("bad.example.test", 300, "stale")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
change := godns.Delete(rec)
|
||||
if change.Action != godns.ActionDelete {
|
||||
t.Fatalf("action: %v", change.Action)
|
||||
}
|
||||
if !strings.Contains(change.Record.RData, "stale") {
|
||||
t.Fatalf("rdata: %q", change.Record.RData)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user