From a507b202ec48a62f601fd55be77815415eeacc67 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Wed, 4 Mar 2026 06:17:27 +0100 Subject: [PATCH] fix: create AAAA DNS record for IPv6 machines" (#257) * chore: update gitignore * fix: create AAAA DNS record for IPv6 machines" * test: randomize execution order to prevent order-based bugs in tests --- .gitignore | 2 + Makefile | 6 +-- pkg/client/dns.go | 67 +++++++++++++++++++++++------ pkg/client/dns_internal_test.go | 74 +++++++++++++++++++++++++++++++++ 4 files changed, 133 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 7e845a69..450fd229 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,5 @@ node_modules/ # VS Code .vscode .devcontainer/ + +.zed/ diff --git a/Makefile b/Makefile index 84cdd85d..bdc7b3f5 100644 --- a/Makefile +++ b/Makefile @@ -73,14 +73,14 @@ mocks: .PHONY: test test: ifeq ($(TEST_NAME),) - go test -count=1 -v ./... + go test -shuffle=on -count=1 -v ./... else - go test -race -count=1 -v -run ^$(TEST_NAME)$$ ./... + go test -shuffle=on -race -count=1 -v -run ^$(TEST_NAME)$$ ./... endif .PHONY: test-e2e test-e2e: - go test -race -count=1 -v ./test/e2e + go test -shuffle=on -race -count=1 -v ./test/e2e .PHONY: test-clean test-clean: diff --git a/pkg/client/dns.go b/pkg/client/dns.go index dc5d133d..8b061ba4 100644 --- a/pkg/client/dns.go +++ b/pkg/client/dns.go @@ -75,24 +75,17 @@ func (cli *Client) CreateIngressRecords(ctx context.Context, serviceID string) ( close(reachableMachines) }() - var ingressIPs []string + var machines []*pb.MachineInfo for m := range reachableMachines { - ip, _ := m.PublicIp.ToAddr() - ingressIPs = append(ingressIPs, ip.String()) + machines = append(machines, m) } - if len(ingressIPs) == 0 { + if len(machines) == 0 { return nil, ErrNoReachableMachines } - req := &pb.CreateDomainRecordsRequest{ - Records: []*pb.DNSRecord{ - { - Name: "*", - Type: pb.DNSRecord_A, - Values: ingressIPs, - }, - // TODO: Add AAAA record with routable IPv6 addresses of machines running Caddy containers. - }, + req, err := getCreateDomainRecordsRequest(machines) + if err != nil { + return nil, fmt.Errorf("create CreateDomainRecordsRequest: %w", err) } resp, err := cli.CreateDomainRecords(ctx, req) if err != nil { @@ -102,6 +95,54 @@ func (cli *Client) CreateIngressRecords(ctx context.Context, serviceID string) ( return resp.Records, nil } +func getCreateDomainRecordsRequest(machines []*pb.MachineInfo) (*pb.CreateDomainRecordsRequest, error) { + if len(machines) == 0 { + return nil, fmt.Errorf("at least one machine must be provided") + } + + var ipv4IngressIPs []string + var ipv6IngressIPs []string + var errs error + for _, m := range machines { + ip, _ := m.PublicIp.ToAddr() + if ip.Is4() { + ipv4IngressIPs = append(ipv4IngressIPs, ip.String()) + } else if ip.Is6() { + ipv6IngressIPs = append(ipv6IngressIPs, ip.String()) + } else { + // This is just a save guard, in case some special case is ever missed. + errs = errors.Join(errs, fmt.Errorf("machine with name %s (and ID: %s) has the public IP address '%s' which is neither IPv4 nor IPv6", m.Name, m.Id, ip.String())) + } + } + if errs != nil { + return nil, errs + } + + records := make([]*pb.DNSRecord, 0, 2) + if len(ipv4IngressIPs) > 0 { + records = append(records, + &pb.DNSRecord{ + Name: "*", + Type: pb.DNSRecord_A, + Values: ipv4IngressIPs, + }, + ) + } + if len(ipv6IngressIPs) > 0 { + records = append(records, + &pb.DNSRecord{ + Name: "*", + Type: pb.DNSRecord_AAAA, + Values: ipv6IngressIPs, + }, + ) + } + + return &pb.CreateDomainRecordsRequest{ + Records: records, + }, nil +} + // verifyCaddyReachable verifies that the Caddy service is reachable on the machine by its public IP. func verifyCaddyReachable(ctx context.Context, m *pb.MachineInfo) error { publicIP, _ := m.PublicIp.ToAddr() diff --git a/pkg/client/dns_internal_test.go b/pkg/client/dns_internal_test.go index c5466b4c..45de6d7b 100644 --- a/pkg/client/dns_internal_test.go +++ b/pkg/client/dns_internal_test.go @@ -1,10 +1,13 @@ package client import ( + "fmt" "net/netip" "testing" + "github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestGetVerifyURL(t *testing.T) { @@ -28,3 +31,74 @@ func TestGetVerifyURL(t *testing.T) { }) } } + +func TestGetCreateDomainRecordsRequest(t *testing.T) { + tests := map[string]struct { + given []*pb.MachineInfo + wantReq *pb.CreateDomainRecordsRequest + wantErr error + }{ + "single IPv4 machine": { + given: []*pb.MachineInfo{ + {Id: "m1", Name: "machine-1", PublicIp: pb.NewIP(netip.MustParseAddr("1.2.3.4"))}, + }, + wantReq: &pb.CreateDomainRecordsRequest{ + Records: []*pb.DNSRecord{ + {Name: "*", Type: pb.DNSRecord_A, Values: []string{"1.2.3.4"}}, + }, + }, + }, + "single IPv6 machine": { + given: []*pb.MachineInfo{ + {Id: "m1", Name: "machine-1", PublicIp: pb.NewIP(netip.MustParseAddr("2001:db8::1"))}, + }, + wantReq: &pb.CreateDomainRecordsRequest{ + Records: []*pb.DNSRecord{ + {Name: "*", Type: pb.DNSRecord_AAAA, Values: []string{"2001:db8::1"}}, + }, + }, + }, + "multiple IPv4 machines": { + given: []*pb.MachineInfo{ + {Id: "m1", Name: "machine-1", PublicIp: pb.NewIP(netip.MustParseAddr("1.2.3.4"))}, + {Id: "m2", Name: "machine-2", PublicIp: pb.NewIP(netip.MustParseAddr("5.6.7.8"))}, + }, + wantReq: &pb.CreateDomainRecordsRequest{ + Records: []*pb.DNSRecord{ + {Name: "*", Type: pb.DNSRecord_A, Values: []string{"1.2.3.4", "5.6.7.8"}}, + }, + }, + }, + "mixed IPv4 and IPv6 machines": { + given: []*pb.MachineInfo{ + {Id: "m1", Name: "machine-1", PublicIp: pb.NewIP(netip.MustParseAddr("1.2.3.4"))}, + {Id: "m2", Name: "machine-2", PublicIp: pb.NewIP(netip.MustParseAddr("2001:db8::1"))}, + {Id: "m3", Name: "machine-3", PublicIp: pb.NewIP(netip.MustParseAddr("10.0.0.1"))}, + {Id: "m4", Name: "machine-4", PublicIp: pb.NewIP(netip.MustParseAddr("2001:db8::2"))}, + }, + wantReq: &pb.CreateDomainRecordsRequest{ + Records: []*pb.DNSRecord{ + {Name: "*", Type: pb.DNSRecord_A, Values: []string{"1.2.3.4", "10.0.0.1"}}, + {Name: "*", Type: pb.DNSRecord_AAAA, Values: []string{"2001:db8::1", "2001:db8::2"}}, + }, + }, + }, + "empty machine list": { + given: nil, + wantErr: fmt.Errorf("at least one machine must be provided"), + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + gotReq, gotErr := getCreateDomainRecordsRequest(tt.given) + if tt.wantErr == nil { + require.NoError(t, gotErr) + assert.Equal(t, tt.wantReq, gotReq) + } else { + assert.Nil(t, gotReq) + assert.Equal(t, tt.wantErr.Error(), gotErr.Error()) + } + }) + } +}