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>
169 lines
4.2 KiB
Go
169 lines
4.2 KiB
Go
//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())
|
|
}
|