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:
@@ -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())
|
||||
}
|
||||
Reference in New Issue
Block a user