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
+168
View File
@@ -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())
}