Skip to content

Use fabric dns client for dns resolves#2069

Merged
lionello merged 6 commits into
mainfrom
edw/server-dns-resolver-impl
May 10, 2026
Merged

Use fabric dns client for dns resolves#2069
lionello merged 6 commits into
mainfrom
edw/server-dns-resolver-impl

Conversation

@edwardrf

@edwardrf edwardrf commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Description

Use fabric dns client for dns resolves

Linked Issues

Fixes: #1955

Checklist

  • I have performed a self-review of my code
  • I have added appropriate tests
  • I have updated the Defang CLI docs and/or README to reflect my changes, if necessary

Summary by CodeRabbit

  • New Features

    • Fabric-backed DNS resolution now supports IP, CNAME, NS and TXT lookups; resolver selection is configurable and safe for concurrent use.
    • CLI BYOC flow and certificate issuance now use an injected, fabric-aware resolver for DNS verification and delegation.
  • Tests

    • Added tests covering resolver behavior (including TXT and empty/no-record cases) and refactored tests to use injected resolvers instead of global state.

@coderabbitai

coderabbitai Bot commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cfe3bc1b-11f8-4d53-a8fd-48b559e86dec

📥 Commits

Reviewing files that changed from the base of the PR and between a75aa5f and fb8e2b5.

📒 Files selected for processing (2)
  • src/pkg/dns/check.go
  • src/pkg/dns/resolver.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/pkg/dns/check.go
  • src/pkg/dns/resolver.go

📝 Walkthrough

Walkthrough

Adds fabric-backed DNS resolution: new FabricClient Resolve* RPCs, GrpcClient wrappers and mock stubs, a FabricResolver implementation, resolver-factory injection into BYOC and cert flows, CheckDomainDNSReady/resolver refactors, and tests updated to inject resolvers.

Changes

Fabric DNS integration

Layer / File(s) Summary
Public contracts
src/pkg/cli/client/client.go, src/pkg/dns/resolver.go
Adds FabricClient Resolve* methods and FabricResolverClient interface; imports updated for defangv1 and CheckTLSCert now accepts a dns.Resolver.
Resolver core & RootResolver refactor
src/pkg/dns/resolver.go
Implements FabricResolver routing lookups through fabric RPCs; makes RootResolver configurable, updates FindNSServers/DirectResolverAt, and removes global ResolverAt.
gRPC wrappers & mocks
src/pkg/cli/client/grpc.go, src/pkg/cli/client/mock.go
Adds GrpcClient passthrough methods for ResolveIPAddr/ResolveCNAME/ResolveNS/ResolveTXT and stub implementations on MockFabricClient.
BYOC provider & connect wiring
src/pkg/cli/client/byoc/aws/byoc.go, src/pkg/cli/connect.go, src/cmd/cli/command/cd.go
Threads fabricClient into NewByocProvider, stores it on ByocAws, and uses a dns.FabricResolver-backed resolver-at for domain delegation; call sites updated.
Cert flow & HTTP client injection
src/pkg/cli/cert.go, src/pkg/cert/check.go
Threads dns.Resolver through cert generation and TLS checks; adds newCertHTTPClient that uses resolver-aware dialing; getWithRetries accepts an injected HTTPClient.
DNS readiness checks
src/pkg/dns/check.go
CheckDomainDNSReady now accepts resolverAt func(string) Resolver; getCNAMEInSync/getIPInSync accept and use the injected resolver factory.
Tests / FabricResolver tests
src/pkg/dns/fabric_test.go, src/pkg/dns/resolver_test.go, src/pkg/dns/check_test.go, src/pkg/cli/cert_test.go, src/pkg/cli/client/byoc/aws/byoc_test.go
Adds FabricResolver unit tests (IP/CNAME/NS/TXT) and refactors tests to inject per-test resolvers and HTTP clients instead of mutating package-level globals.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as CLI
    participant ResolverAt as ResolverFactory
    participant FabricRes as FabricResolver
    participant Grpc as GrpcClient
    participant Service as Fabric gRPC Service

    CLI->>ResolverAt: resolver := ResolverAt(nsServer)
    ResolverAt->>FabricRes: use FabricResolver(Client)
    CLI->>FabricRes: LookupIPAddr(ctx, domain)
    FabricRes->>Grpc: ResolveIPAddr(ctx, ResolveIPAddrRequest)
    Grpc->>Service: ResolveIPAddr RPC
    Service-->>Grpc: ResolveIPAddrResponse
    Grpc-->>FabricRes: response / error
    FabricRes->>CLI: []*net.IPAddr or ErrNoSuchHost / error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • DefangLabs/defang#2057: Adds corresponding server-side FabricController Resolve* gRPC methods and generated bindings that the new client wrappers invoke.
  • DefangLabs/defang#2100: Modifies FabricController DNS RPC surface and client wiring; overlaps on ResolveTXT additions.
  • DefangLabs/defang#2101: Adds TXT lookup support in the resolver layer; related to Fabric-backed TXT plumbing.

Suggested reviewers

  • lionello
  • jordanstephens

Poem

🐰
I hopped through gRPC tunnels, quick and bright,
Turned scattered UDP into a guided flight.
CNAMEs, IPs, NS, and TXT in line,
Fabric answers tidy every time.
Hooray — steady hops for DNS by design! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Use fabric dns client for dns resolves' clearly summarizes the main change: replacing direct DNS resolution with fabric DNS client calls throughout the codebase.
Linked Issues check ✅ Passed The PR implements the proposed solution from #1955 by using fabric's server-side DNS client instead of direct UDP queries, eliminating local network interference and DNS proxy interception issues.
Out of Scope Changes check ✅ Passed All changes are scoped to DNS resolution: extending FabricClient interface, implementing FabricResolver, injecting resolver dependencies throughout the code, and removing global ResolverAt state—all directly addressing issue #1955.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch edw/server-dns-resolver-impl

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.1)

level=warning msg="The linter 'gomodguard' is deprecated (since v2.12.0) due to: new major version. Replaced by gomodguard_v2."
level=warning msg="Suggested new configuration:\nlinters:\n enable:\n - gomodguard_v2\n"
level=warning msg="[linters_context] running gomodguard failed: unable to read module file go.mod: current working directory must have a go.mod file: if you are not using go modules it is suggested to disable this linter"
level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/pkg/dns/fabric_test.go (2)

75-95: Consider adding error propagation test for CNAME.

TestFabricResolverLookupIPAddr includes a subtest for RPC error propagation, but TestFabricResolverLookupCNAME does not. For consistency and completeness:

🧪 Optional: Add error propagation test
t.Run("propagates RPC error", func(t *testing.T) {
	boom := errors.New("rpc boom")
	m := &mockFabricClient{cnameErr: boom}
	r := FabricResolver{Client: m}
	if _, err := r.LookupCNAME(t.Context(), "example.com"); err != boom {
		t.Errorf("expected rpc error, got %v", err)
	}
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pkg/dns/fabric_test.go` around lines 75 - 95, Add a subtest to
TestFabricResolverLookupCNAME that verifies RPC errors from the mock are
propagated: create an error (e.g., boom := errors.New("rpc boom")), set
mockFabricClient{cnameErr: boom}, construct FabricResolver{Client: m}, call
LookupCNAME with a context and domain, and assert the returned error is the same
boom (not wrapped or swallowed). This mirrors the RPC error propagation test
pattern used in TestFabricResolverLookupIPAddr and ensures LookupCNAME correctly
returns the mock's cnameErr.

97-107: Consider using subtests for TestFabricResolverLookupNS.

The other tests use t.Run subtests for different scenarios. For consistency and to test error propagation:

🧪 Optional: Refactor to subtests
func TestFabricResolverLookupNS(t *testing.T) {
	t.Run("returns NS records", func(t *testing.T) {
		m := &mockFabricClient{nsResp: &defangv1.ResolveNSResponse{Hosts: []string{"ns1.example.com.", "ns2.example.com."}}}
		r := FabricResolver{Client: m}
		ns, err := r.LookupNS(t.Context(), "example.com")
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if len(ns) != 2 || ns[0].Host != "ns1.example.com." {
			t.Errorf("unexpected NS result: %+v", ns)
		}
	})

	t.Run("propagates RPC error", func(t *testing.T) {
		boom := errors.New("rpc boom")
		m := &mockFabricClient{nsErr: boom}
		r := FabricResolver{Client: m}
		if _, err := r.LookupNS(t.Context(), "example.com"); err != boom {
			t.Errorf("expected rpc error, got %v", err)
		}
	})
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pkg/dns/fabric_test.go` around lines 97 - 107, Convert
TestFabricResolverLookupNS into a table-driven or t.Run style with at least two
subtests: one "returns NS records" that uses mockFabricClient with nsResp (hosts
"ns1.example.com.", "ns2.example.com.") and asserts successful LookupNS via
FabricResolver, and one "propagates RPC error" that uses mockFabricClient with
nsErr set to a sentinel error and asserts LookupNS returns that error; update
assertions to run inside the respective t.Run closures and reference
mockFabricClient, nsResp, nsErr and FabricResolver.LookupNS to locate the code
to change.
src/pkg/dns/resolver.go (1)

98-111: Consider returning ErrNoSuchHost for empty NS response.

LookupIPAddr and LookupCNAME return ErrNoSuchHost when the response is empty, but LookupNS returns an empty slice. For consistency, consider the same treatment:

♻️ Optional consistency fix
 func (r FabricResolver) LookupNS(ctx context.Context, domain string) ([]*net.NS, error) {
 	resp, err := r.Client.ResolveNS(ctx, &defangv1.ResolveNSRequest{
 		Domain:   domain,
 		NsServer: r.NSServer,
 	})
 	if err != nil {
 		return nil, err
 	}
+	if len(resp.Hosts) == 0 {
+		return nil, ErrNoSuchHost
+	}
 	nss := make([]*net.NS, 0, len(resp.Hosts))
 	for _, h := range resp.Hosts {
 		nss = append(nss, &net.NS{Host: h})
 	}
 	return nss, nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pkg/dns/resolver.go` around lines 98 - 111, LookupNS currently returns an
empty slice when resp.Hosts is empty, which is inconsistent with LookupIPAddr
and LookupCNAME that return ErrNoSuchHost; update FabricResolver.LookupNS to
check len(resp.Hosts)==0 and return nil, ErrNoSuchHost (the same error used by
LookupIPAddr/LookupCNAME) instead of an empty slice, so callers receive a
consistent "no such host" error for missing NS records.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/pkg/cli/connect.go`:
- Around line 21-23: The global ResolverAt is being written by
dns.UseFabricResolver(grpcClient) in Connect while reads occur unprotected
(e.g., getCNAMEInSync, getIPInSync, resolver.go uses ResolverAt), causing a data
race; fix by making ResolverAt access concurrency-safe: either (A) protect reads
with fabricMu.RLock()/RUnlock() via a helper like getResolverAt() analogous to
getFabricClient(), and wrap dns.UseFabricResolver(grpcClient) with
fabricMu.Lock()/Unlock() as already done, or (B) replace ResolverAt with an
atomic.Value holding the resolver function and update it in UseFabricResolver
while readers load it atomically; pick one approach, update all reads
(getCNAMEInSync, getIPInSync, resolver.go usages) to use the safe helper or
atomic load, and ensure Connect no longer performs unsynchronized writes to
ResolverAt during active lookups.

---

Nitpick comments:
In `@src/pkg/dns/fabric_test.go`:
- Around line 75-95: Add a subtest to TestFabricResolverLookupCNAME that
verifies RPC errors from the mock are propagated: create an error (e.g., boom :=
errors.New("rpc boom")), set mockFabricClient{cnameErr: boom}, construct
FabricResolver{Client: m}, call LookupCNAME with a context and domain, and
assert the returned error is the same boom (not wrapped or swallowed). This
mirrors the RPC error propagation test pattern used in
TestFabricResolverLookupIPAddr and ensures LookupCNAME correctly returns the
mock's cnameErr.
- Around line 97-107: Convert TestFabricResolverLookupNS into a table-driven or
t.Run style with at least two subtests: one "returns NS records" that uses
mockFabricClient with nsResp (hosts "ns1.example.com.", "ns2.example.com.") and
asserts successful LookupNS via FabricResolver, and one "propagates RPC error"
that uses mockFabricClient with nsErr set to a sentinel error and asserts
LookupNS returns that error; update assertions to run inside the respective
t.Run closures and reference mockFabricClient, nsResp, nsErr and
FabricResolver.LookupNS to locate the code to change.

In `@src/pkg/dns/resolver.go`:
- Around line 98-111: LookupNS currently returns an empty slice when resp.Hosts
is empty, which is inconsistent with LookupIPAddr and LookupCNAME that return
ErrNoSuchHost; update FabricResolver.LookupNS to check len(resp.Hosts)==0 and
return nil, ErrNoSuchHost (the same error used by LookupIPAddr/LookupCNAME)
instead of an empty slice, so callers receive a consistent "no such host" error
for missing NS records.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d7b6b16b-82f2-433a-9256-2a5fae19f893

📥 Commits

Reviewing files that changed from the base of the PR and between ed4b6ff and 81db18f.

📒 Files selected for processing (5)
  • src/pkg/cli/client/client.go
  • src/pkg/cli/client/grpc.go
  • src/pkg/cli/connect.go
  • src/pkg/dns/fabric_test.go
  • src/pkg/dns/resolver.go

Comment thread src/pkg/cli/connect.go Outdated
@edwardrf edwardrf marked this pull request as draft April 23, 2026 17:05
@edwardrf edwardrf marked this pull request as ready for review April 23, 2026 20:10
@edwardrf edwardrf force-pushed the edw/server-dns-resolver-impl branch from 0d09460 to b4ce078 Compare April 23, 2026 20:10

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/pkg/dns/resolver.go (2)

71-80: Consider logging or handling invalid IP addresses from the server.

If net.ParseIP(s) returns nil, the invalid IP is silently dropped. While returning ErrNoSuchHost when all IPs are invalid is correct, consider logging a warning for debugging when individual IPs fail to parse—this could help diagnose server-side issues.

♻️ Optional: log invalid IPs
+import "log/slog"
+
 func (r FabricResolver) LookupIPAddr(ctx context.Context, domain string) ([]net.IPAddr, error) {
 	resp, err := r.Client.ResolveIPAddr(ctx, &defangv1.ResolveIPAddrRequest{
 		Domain:   domain,
 		NsServer: r.NSServer,
 	})
 	if err != nil {
 		return nil, err
 	}
 	ips := make([]net.IPAddr, 0, len(resp.IpAddrs))
 	for _, s := range resp.IpAddrs {
 		if ip := net.ParseIP(s); ip != nil {
 			ips = append(ips, net.IPAddr{IP: ip})
+		} else {
+			slog.Debug("invalid IP address from fabric resolver", "ip", s, "domain", domain)
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pkg/dns/resolver.go` around lines 71 - 80, When building the ips slice
from resp.IpAddrs in resolver.go, add a warning/log entry whenever
net.ParseIP(s) returns nil so invalid IP strings are visible in logs; keep
appending valid addresses to ips as before, and preserve the ErrNoSuchHost
return when ips ends up empty. Update the block that iterates over resp.IpAddrs
(the loop building ips) to emit a concise warning including the invalid string
(and context like the host or response ID if available) using the package logger
used elsewhere in this file, without changing the final behavior of returning
ips or ErrNoSuchHost.

97-110: Inconsistent empty-result handling: LookupNS returns empty slice instead of ErrNoSuchHost.

LookupIPAddr and LookupCNAME return ErrNoSuchHost when no records are found, but LookupNS returns an empty slice. If this is intentional (e.g., empty NS is semantically valid), consider adding a brief comment to document the design decision.

📝 Option A: Match other methods (return error)
 func (r FabricResolver) LookupNS(ctx context.Context, domain string) ([]*net.NS, error) {
 	resp, err := r.Client.ResolveNS(ctx, &defangv1.ResolveNSRequest{
 		Domain:   domain,
 		NsServer: r.NSServer,
 	})
 	if err != nil {
 		return nil, err
 	}
+	if len(resp.Hosts) == 0 {
+		return nil, ErrNoSuchHost
+	}
 	nss := make([]*net.NS, 0, len(resp.Hosts))
📝 Option B: Document the intentional difference
 func (r FabricResolver) LookupNS(ctx context.Context, domain string) ([]*net.NS, error) {
+	// Note: Unlike LookupIPAddr/LookupCNAME, an empty result is valid for NS
+	// lookups and does not return ErrNoSuchHost.
 	resp, err := r.Client.ResolveNS(ctx, &defangv1.ResolveNSRequest{
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pkg/dns/resolver.go` around lines 97 - 110, LookupNS currently returns an
empty slice when resp.Hosts is empty, which is inconsistent with LookupIPAddr
and LookupCNAME; update the LookupNS method to check if len(resp.Hosts) == 0 and
return nil, net.ErrNoSuchHost (or add a brief comment if the empty-slice
behavior is intentional) so behavior matches the other resolvers; locate the
LookupNS function and add the empty-result check before building nss,
referencing net.ErrNoSuchHost to signal no records found.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/pkg/dns/resolver.go`:
- Around line 71-80: When building the ips slice from resp.IpAddrs in
resolver.go, add a warning/log entry whenever net.ParseIP(s) returns nil so
invalid IP strings are visible in logs; keep appending valid addresses to ips as
before, and preserve the ErrNoSuchHost return when ips ends up empty. Update the
block that iterates over resp.IpAddrs (the loop building ips) to emit a concise
warning including the invalid string (and context like the host or response ID
if available) using the package logger used elsewhere in this file, without
changing the final behavior of returning ips or ErrNoSuchHost.
- Around line 97-110: LookupNS currently returns an empty slice when resp.Hosts
is empty, which is inconsistent with LookupIPAddr and LookupCNAME; update the
LookupNS method to check if len(resp.Hosts) == 0 and return nil,
net.ErrNoSuchHost (or add a brief comment if the empty-slice behavior is
intentional) so behavior matches the other resolvers; locate the LookupNS
function and add the empty-result check before building nss, referencing
net.ErrNoSuchHost to signal no records found.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 51dc1001-7b9f-4b2f-a4fb-8603a743fade

📥 Commits

Reviewing files that changed from the base of the PR and between 81db18f and b4ce078.

📒 Files selected for processing (7)
  • src/pkg/cli/client/client.go
  • src/pkg/cli/client/grpc.go
  • src/pkg/cli/connect.go
  • src/pkg/dns/check_test.go
  • src/pkg/dns/fabric_test.go
  • src/pkg/dns/resolver.go
  • src/pkg/dns/resolver_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/pkg/cli/connect.go
  • src/pkg/cli/client/grpc.go
  • src/pkg/dns/fabric_test.go

Comment thread src/pkg/dns/resolver.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/pkg/dns/fabric_test.go (1)

39-107: ⚡ Quick win

Prefer a table-driven harness for the new FabricResolver suite.

These cases repeat the same setup/assertion pattern across IP, CNAME, and NS lookups. Converting the new file to a table-driven structure will make it much easier to extend the resolver contract with more record/error combinations.

As per coding guidelines "Use table-driven tests for comprehensive test coverage in unit tests".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pkg/dns/fabric_test.go` around lines 39 - 107, Tests for FabricResolver
repeat similar setup/assertions; convert them into table-driven tests by
defining a slice of test cases containing fields like name, method
("LookupIPAddr"/"LookupCNAME"/"LookupNS"), mockFabricClient fields
(ipResp/ipErr, cnameResp, nsResp), input domain, expected result or expected
error (e.g., ErrNoSuchHost) and any expected request checks; then iterate with
t.Run and for each case construct FabricResolver{Client: &mockFabricClient{...},
NSServer: ... if needed}, call the appropriate method
(LookupIPAddr/LookupCNAME/LookupNS) based on the method field, and perform
unified assertions for returned value, error handling, and
mockFabricClient.last* request contents; keep existing behavior for parsing IPs
(valid/invalid), error propagation (ipErr), and empty-result => ErrNoSuchHost.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/pkg/dns/check.go`:
- Around line 27-31: The function CheckDomainDNSReady currently dereferences
resolverAt and will panic if callers pass nil; change it to default a nil
resolverAt to DirectResolverAt at the start (e.g., if resolverAt == nil {
resolverAt = DirectResolverAt }) before any calls to getCNAMEInSync or other
lookups, and apply the same nil-to-DirectResolverAt defaulting in the other
function(s) in the same file that accept resolverAt (the block referenced at
lines 44-68) so all resolverAt usages mirror RootResolver.resolverFn() behavior.

In `@src/pkg/dns/resolver.go`:
- Around line 86-99: The final authoritative lookup still uses DirectResolver
instead of honoring the injected ResolverAt; update the lookup logic in
functions that perform the terminal A/CNAME/NS query to call
RootResolver.resolverFn() (or r.resolverFn()) rather than directly
constructing/using DirectResolver so the injected resolver is used for both NS
discovery and the final authoritative query; locate the code paths referenced
around resolverFn(), resolverAt, and the final lookup (the block that currently
instantiates or calls DirectResolver for the terminal query) and replace that
usage with r.resolverFn()(host) (or equivalent) so the injected resolver is
consistently applied.

---

Nitpick comments:
In `@src/pkg/dns/fabric_test.go`:
- Around line 39-107: Tests for FabricResolver repeat similar setup/assertions;
convert them into table-driven tests by defining a slice of test cases
containing fields like name, method ("LookupIPAddr"/"LookupCNAME"/"LookupNS"),
mockFabricClient fields (ipResp/ipErr, cnameResp, nsResp), input domain,
expected result or expected error (e.g., ErrNoSuchHost) and any expected request
checks; then iterate with t.Run and for each case construct
FabricResolver{Client: &mockFabricClient{...}, NSServer: ... if needed}, call
the appropriate method (LookupIPAddr/LookupCNAME/LookupNS) based on the method
field, and perform unified assertions for returned value, error handling, and
mockFabricClient.last* request contents; keep existing behavior for parsing IPs
(valid/invalid), error propagation (ipErr), and empty-result => ErrNoSuchHost.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f1f88201-8314-4203-96a7-dbac6bdb417f

📥 Commits

Reviewing files that changed from the base of the PR and between b4ce078 and b6b6d9a.

📒 Files selected for processing (13)
  • src/cmd/cli/command/cd.go
  • src/pkg/cert/check.go
  • src/pkg/cli/cert.go
  • src/pkg/cli/cert_test.go
  • src/pkg/cli/client/byoc/aws/byoc.go
  • src/pkg/cli/client/byoc/aws/byoc_test.go
  • src/pkg/cli/client/mock.go
  • src/pkg/cli/connect.go
  • src/pkg/dns/check.go
  • src/pkg/dns/check_test.go
  • src/pkg/dns/fabric_test.go
  • src/pkg/dns/resolver.go
  • src/pkg/dns/resolver_test.go
✅ Files skipped from review due to trivial changes (2)
  • src/pkg/cli/client/byoc/aws/byoc_test.go
  • src/pkg/dns/check_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/pkg/dns/resolver_test.go

Comment thread src/pkg/dns/check.go
Comment thread src/pkg/dns/resolver.go
lionello pushed a commit that referenced this pull request May 7, 2026
Mirrors the existing ResolveIPAddr / ResolveCNAME / ResolveNS shape:
domain + optional ns_server in the request, repeated strings in the
response. NO_SIDE_EFFECTS idempotency.

Server-side handler will land in defang-mvp on top of this. CLI client
wiring (FabricClient / GrpcClient / MockFabricClient / FabricResolver)
will follow in the in-flight #2069 branch once it picks up these
regenerated bindings.

Co-authored-by: Edward J <edw@defang.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
edwardrf added a commit that referenced this pull request May 7, 2026
* dns: add LookupTXT to Resolver interface

Mirrors LookupIPAddr / LookupCNAME / LookupNS:

- Resolver interface gains LookupTXT(ctx, domain) ([]string, error)
- DirectResolver.LookupTXT issues a TypeTXT query and joins each
  record's wire-split strings into one entry per RR
- RootResolver.LookupTXT delegates to its per-domain DirectResolver
- MockResolver.LookupTXT reads from Records[{Type:"TXT"}]
- Test mocks (cli.MockResolver, aws.CallbackMockResolver) add stubs
  to keep satisfying the interface

Prereq for the defang-mvp ResolveTXT server handler (which calls
LookupTXT on the resolver) and for the in-flight FabricResolver work
in #2069 (which adds a fabric-backed Resolver impl that will need a
LookupTXT method too).

Standalone from the cert-validation flow on edw/azure-cert-gen — that
branch's cert.go integration uses these primitives but isn't part of
this change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* dns: follow CNAME chains in LookupTXT

A TXT query against a hostname that's actually a CNAME returns only
the CNAME RR — no TXT records — so RootResolver.LookupTXT must follow
the alias, matching how LookupIPAddr handles A/AAAA over CNAME.

DirectResolver.LookupTXT now captures any CNAME from the answer
section and returns ErrCNAMEFound when the result has no TXT records;
RootResolver.LookupTXT loops up to 10 times following each CNAME.

Without this, _dnsauth.<host> -> CNAME -> Azure-managed validation
record would silently 404, since the upstream wouldn't see the TXT
sitting at the CNAME target.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Edward J <edw@defang.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
edw-defang and others added 5 commits May 8, 2026 16:13
…ons (#2078)

* refactor(aws): carry FabricResolverClient in ByocAws for explicit DNS routing

PrepareDomainDelegation now builds a FabricResolver directly from the
stored fabricClient instead of reading the global dns.ResolverAt. This
makes the NS conflict check's DNS path explicit and traceable without
relying on UseFabricResolver having been called first.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(dns): thread resolverAt explicitly through check.go and FindNSServers

CheckDomainDNSReady, getCNAMEInSync, getIPInSync, and FindNSServers now
accept an explicit resolverAt parameter instead of reading the global.
cert.go's waitForCNAME builds a FabricResolver-backed factory from its
FabricClient argument and passes it through. MockFabricClient gains stub
implementations for the three Resolve RPCs to prevent nil-interface
panics when used in tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(dns): remove global resolver state; add ResolverAt field to RootResolver

UseFabricResolver, getFabricClient, fabricMu, fabricClient, and the
package-level resolverAt var are all deleted. RootResolver gains an
explicit ResolverAt field (nil falls back to DirectResolverAt), and
FindNSServers takes a resolverAt parameter so callers control which
resolver is used when walking the NS delegation chain. connect.go no
longer calls UseFabricResolver. fabric_test.go drops the tests for the
now-deleted functions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(dns): restore fabric DNS routing for all lookup paths

ALB/CNAME IP lookups in CheckDomainDNSReady, the HTTP client used
for cert generation, and CheckTLSCert were still using direct DNS
after the DI refactor. Thread the fabric resolver through all three
paths so no lookup silently falls back to direct UDP DNS.

- DirectResolverAt("") now returns RootResolver{} as the "root"
  fallback, enabling resolverAt("") as a uniform way to request
  recursive resolution regardless of whether fabric or direct DNS
  is in use
- CheckDomainDNSReady uses resolverAt("") for ALB/CNAME IP lookups
  instead of a package-level RootResolver{}
- cert.CheckTLSCert accepts a dns.Resolver parameter
- cli/cert: replace global resolver/httpClient vars with
  newCertHTTPClient(r dns.Resolver); thread resolver through
  generateCert → triggerCertGeneration / waitForTLS / CheckTLSCert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
ResolveTXT (added in #2100, server-side handled in defang-mvp#2960) is
now part of the FabricResolverClient interface, with a matching
FabricResolver.LookupTXT implementation that mirrors LookupIPAddr's
shape: empty Txts -> ErrNoSuchHost, RPC errors propagate.

FabricClient gains ResolveTXT; GrpcClient implements it via the
generated connect client; MockFabricClient returns an empty response.

Tests follow the existing pattern in fabric_test.go:
- returns_txts_and_forwards_NSServer
- empty_Txts_returns_ErrNoSuchHost
- propagates_RPC_error

Cert-validation TXT polling (Azure managed cert flow) now goes through
the fabric resolver chain instead of net.DefaultResolver, sidestepping
upstream NXDOMAIN caches that bit us during apex-domain testing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@edwardrf edwardrf force-pushed the edw/server-dns-resolver-impl branch from b6b6d9a to a75aa5f Compare May 8, 2026 23:18
Two CodeRabbit findings on #2069:

1. CheckDomainDNSReady panicked if a caller passed nil for resolverAt
   (the func dereferences it on the first lookup). Mirror
   RootResolver.resolverFn() and default nil to DirectResolverAt.

2. RootResolver.getResolver only used the injected resolverFn for NS
   discovery, then hard-coded DirectResolver{NSServer: ...} for the
   actual A/CNAME/NS/TXT query. That meant a FabricResolver-injected
   caller still went over raw UDP for the lookup, defeating the point
   of injection. Use resolverFn() for the auth lookup too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@edwardrf

edwardrf commented May 8, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai both findings applied in fb8e2b52:

  1. CheckDomainDNSReady nil-resolverAt guard — added if resolverAt == nil { resolverAt = DirectResolverAt } at the top, mirroring RootResolver.resolverFn().

  2. RootResolver.getResolver honoring the injected resolverAt — was returning DirectResolver{NSServer: ...} for the auth lookup, which silently bypassed the injection. Now uses resolverAt(ns[...].Host) so a FabricResolver-injected caller routes the actual A/CNAME/NS/TXT query through fabric, not raw UDP.

Build + short tests still green.

@coderabbitai

coderabbitai Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@edwardrf Both fixes look correct:

  1. nil-guard in CheckDomainDNSReady — the if resolverAt == nil { resolverAt = DirectResolverAt } sentinel is a clean, consistent pattern matching the RootResolver.resolverFn() fallback. Callers passing nil will get the original direct-DNS behaviour transparently.

  2. RootResolver.getResolver using the injected resolver — this is the more impactful fix. The previous hard-coded DirectResolver{NSServer: ...} path would have silently broken the entire point of the fabric injection (queries would still go over raw UDP), so routing through resolverAt(ns[...].Host) is the right correction.

Glad the build and tests are green. Thanks for the thorough follow-up!

( ˶ˆᗜˆ˵ )

@lionello lionello merged commit ed35ec6 into main May 10, 2026
14 checks passed
@lionello lionello deleted the edw/server-dns-resolver-impl branch May 10, 2026 14:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DNS delegation set conflict check fails when transparent DNS proxy is active (VPN, ISP interception)

4 participants