Skip to content

Commit a02e5b3

Browse files
edw-defangedwardrf
authored andcommitted
Use fabric dns client for dns resolves
1 parent f4039ae commit a02e5b3

5 files changed

Lines changed: 239 additions & 1 deletion

File tree

src/pkg/cli/client/client.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ type FabricClient interface {
3434
Preview(context.Context, *defangv1.PreviewRequest) (*defangv1.PreviewResponse, error)
3535
PutDeployment(context.Context, *defangv1.PutDeploymentRequest) error
3636
PutStack(context.Context, *defangv1.PutStackRequest) error
37+
ResolveCNAME(context.Context, *defangv1.ResolveCNAMERequest) (*defangv1.ResolveCNAMEResponse, error)
38+
ResolveIPAddr(context.Context, *defangv1.ResolveIPAddrRequest) (*defangv1.ResolveIPAddrResponse, error)
39+
ResolveNS(context.Context, *defangv1.ResolveNSRequest) (*defangv1.ResolveNSResponse, error)
3740
RevokeToken(context.Context) error
3841
SetOptions(context.Context, *defangv1.SetOptionsRequest) error
3942
Token(context.Context, *defangv1.TokenRequest) (*defangv1.TokenResponse, error)

src/pkg/cli/client/grpc.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,15 @@ func (g GrpcClient) GenerateCompose(ctx context.Context, req *defangv1.GenerateC
206206
func (g GrpcClient) GetDefaultStack(ctx context.Context, req *defangv1.GetDefaultStackRequest) (*defangv1.GetStackResponse, error) {
207207
return getMsg(g.client.GetDefaultStack(ctx, connect.NewRequest(req)))
208208
}
209+
210+
func (g GrpcClient) ResolveIPAddr(ctx context.Context, req *defangv1.ResolveIPAddrRequest) (*defangv1.ResolveIPAddrResponse, error) {
211+
return getMsg(g.client.ResolveIPAddr(ctx, connect.NewRequest(req)))
212+
}
213+
214+
func (g GrpcClient) ResolveCNAME(ctx context.Context, req *defangv1.ResolveCNAMERequest) (*defangv1.ResolveCNAMEResponse, error) {
215+
return getMsg(g.client.ResolveCNAME(ctx, connect.NewRequest(req)))
216+
}
217+
218+
func (g GrpcClient) ResolveNS(ctx context.Context, req *defangv1.ResolveNSRequest) (*defangv1.ResolveNSResponse, error) {
219+
return getMsg(g.client.ResolveNS(ctx, connect.NewRequest(req)))
220+
}

src/pkg/cli/connect.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/DefangLabs/defang/src/pkg/cli/client/byoc/aws"
88
"github.com/DefangLabs/defang/src/pkg/cli/client/byoc/do"
99
"github.com/DefangLabs/defang/src/pkg/cli/client/byoc/gcp"
10+
"github.com/DefangLabs/defang/src/pkg/dns"
1011
"github.com/DefangLabs/defang/src/pkg/term"
1112
"github.com/DefangLabs/defang/src/pkg/types"
1213
)
@@ -17,7 +18,9 @@ func Connect(fabricAddr string, requestedTenant types.TenantNameOrID) *client.Gr
1718
term.Debugf("Using tenant %q for cluster %q", requestedTenant, host)
1819

1920
accessToken := client.GetExistingToken(host)
20-
return client.NewGrpcClient(host, accessToken, requestedTenant)
21+
grpcClient := client.NewGrpcClient(host, accessToken, requestedTenant)
22+
dns.UseFabricResolver(grpcClient)
23+
return grpcClient
2124
}
2225

2326
func ConnectWithTenant(ctx context.Context, fabricAddr string, requestedTenant types.TenantNameOrID) (*client.GrpcClient, error) {

src/pkg/dns/fabric_test.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package dns
2+
3+
import (
4+
"context"
5+
"errors"
6+
"testing"
7+
8+
defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1"
9+
)
10+
11+
type mockFabricClient struct {
12+
ipResp *defangv1.ResolveIPAddrResponse
13+
ipErr error
14+
cnameResp *defangv1.ResolveCNAMEResponse
15+
cnameErr error
16+
nsResp *defangv1.ResolveNSResponse
17+
nsErr error
18+
19+
lastIPReq *defangv1.ResolveIPAddrRequest
20+
lastCNAMEReq *defangv1.ResolveCNAMERequest
21+
lastNSReq *defangv1.ResolveNSRequest
22+
}
23+
24+
func (m *mockFabricClient) ResolveIPAddr(_ context.Context, req *defangv1.ResolveIPAddrRequest) (*defangv1.ResolveIPAddrResponse, error) {
25+
m.lastIPReq = req
26+
return m.ipResp, m.ipErr
27+
}
28+
29+
func (m *mockFabricClient) ResolveCNAME(_ context.Context, req *defangv1.ResolveCNAMERequest) (*defangv1.ResolveCNAMEResponse, error) {
30+
m.lastCNAMEReq = req
31+
return m.cnameResp, m.cnameErr
32+
}
33+
34+
func (m *mockFabricClient) ResolveNS(_ context.Context, req *defangv1.ResolveNSRequest) (*defangv1.ResolveNSResponse, error) {
35+
m.lastNSReq = req
36+
return m.nsResp, m.nsErr
37+
}
38+
39+
func TestFabricResolverLookupIPAddr(t *testing.T) {
40+
t.Run("returns parsed IPs and forwards NSServer", func(t *testing.T) {
41+
m := &mockFabricClient{
42+
ipResp: &defangv1.ResolveIPAddrResponse{IpAddrs: []string{"1.2.3.4", "::1", "not-an-ip"}},
43+
}
44+
r := FabricResolver{Client: m, NSServer: "ns.example.com"}
45+
ips, err := r.LookupIPAddr(t.Context(), "example.com")
46+
if err != nil {
47+
t.Fatalf("unexpected error: %v", err)
48+
}
49+
if len(ips) != 2 {
50+
t.Fatalf("expected 2 valid IPs, got %v", ips)
51+
}
52+
if m.lastIPReq.Domain != "example.com" || m.lastIPReq.NsServer != "ns.example.com" {
53+
t.Errorf("request mismatch: %+v", m.lastIPReq)
54+
}
55+
})
56+
57+
t.Run("empty IPs returns ErrNoSuchHost", func(t *testing.T) {
58+
m := &mockFabricClient{ipResp: &defangv1.ResolveIPAddrResponse{}}
59+
r := FabricResolver{Client: m}
60+
if _, err := r.LookupIPAddr(t.Context(), "nx.example.com"); !errors.Is(err, ErrNoSuchHost) {
61+
t.Errorf("expected ErrNoSuchHost, got %v", err)
62+
}
63+
})
64+
65+
t.Run("propagates RPC error", func(t *testing.T) {
66+
boom := errors.New("rpc boom")
67+
m := &mockFabricClient{ipErr: boom}
68+
r := FabricResolver{Client: m}
69+
if _, err := r.LookupIPAddr(t.Context(), "example.com"); err != boom {
70+
t.Errorf("expected rpc error, got %v", err)
71+
}
72+
})
73+
}
74+
75+
func TestFabricResolverLookupCNAME(t *testing.T) {
76+
t.Run("returns cname", func(t *testing.T) {
77+
m := &mockFabricClient{cnameResp: &defangv1.ResolveCNAMEResponse{Cname: "alb.example.com"}}
78+
r := FabricResolver{Client: m}
79+
cname, err := r.LookupCNAME(t.Context(), "api.example.com")
80+
if err != nil {
81+
t.Fatalf("unexpected error: %v", err)
82+
}
83+
if cname != "alb.example.com" {
84+
t.Errorf("got %q", cname)
85+
}
86+
})
87+
88+
t.Run("empty cname returns ErrNoSuchHost", func(t *testing.T) {
89+
m := &mockFabricClient{cnameResp: &defangv1.ResolveCNAMEResponse{}}
90+
r := FabricResolver{Client: m}
91+
if _, err := r.LookupCNAME(t.Context(), "api.example.com"); !errors.Is(err, ErrNoSuchHost) {
92+
t.Errorf("expected ErrNoSuchHost, got %v", err)
93+
}
94+
})
95+
}
96+
97+
func TestFabricResolverLookupNS(t *testing.T) {
98+
m := &mockFabricClient{nsResp: &defangv1.ResolveNSResponse{Hosts: []string{"ns1.example.com.", "ns2.example.com."}}}
99+
r := FabricResolver{Client: m}
100+
ns, err := r.LookupNS(t.Context(), "example.com")
101+
if err != nil {
102+
t.Fatalf("unexpected error: %v", err)
103+
}
104+
if len(ns) != 2 || ns[0].Host != "ns1.example.com." {
105+
t.Errorf("unexpected NS result: %+v", ns)
106+
}
107+
}
108+
109+
func TestUseFabricResolver(t *testing.T) {
110+
t.Cleanup(func() {
111+
fabricClient = nil
112+
ResolverAt = DirectResolverAt
113+
})
114+
115+
m := &mockFabricClient{ipResp: &defangv1.ResolveIPAddrResponse{IpAddrs: []string{"9.9.9.9"}}}
116+
UseFabricResolver(m)
117+
118+
// RootResolver should now delegate to FabricResolver.
119+
ips, err := RootResolver{}.LookupIPAddr(t.Context(), "example.com")
120+
if err != nil {
121+
t.Fatalf("RootResolver.LookupIPAddr: %v", err)
122+
}
123+
if len(ips) != 1 || ips[0].IP.String() != "9.9.9.9" {
124+
t.Errorf("unexpected IPs: %v", ips)
125+
}
126+
127+
// ResolverAt should return a FabricResolver bound to the NS.
128+
r := ResolverAt("ns1.example.com")
129+
if fr, ok := r.(FabricResolver); !ok || fr.NSServer != "ns1.example.com" {
130+
t.Errorf("ResolverAt did not return FabricResolver: %T %+v", r, r)
131+
}
132+
}

src/pkg/dns/resolver.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"sort"
1010

1111
"github.com/DefangLabs/defang/src/pkg"
12+
defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1"
1213
"github.com/miekg/dns"
1314
)
1415

@@ -18,6 +19,84 @@ type Resolver interface {
1819
LookupNS(ctx context.Context, domain string) ([]*net.NS, error)
1920
}
2021

22+
// FabricResolverClient is the subset of the fabric gRPC API used to resolve DNS
23+
// records remotely.
24+
type FabricResolverClient interface {
25+
ResolveIPAddr(context.Context, *defangv1.ResolveIPAddrRequest) (*defangv1.ResolveIPAddrResponse, error)
26+
ResolveCNAME(context.Context, *defangv1.ResolveCNAMERequest) (*defangv1.ResolveCNAMEResponse, error)
27+
ResolveNS(context.Context, *defangv1.ResolveNSRequest) (*defangv1.ResolveNSResponse, error)
28+
}
29+
30+
// fabricClient is set by UseFabricResolver. When non-nil, RootResolver and
31+
// ResolverAt route DNS lookups through the fabric gRPC API.
32+
var fabricClient FabricResolverClient
33+
34+
// UseFabricResolver wires DNS lookups through the fabric gRPC API. After it is
35+
// called, RootResolver{} and ResolverAt(nsServer) both issue remote RPCs
36+
// instead of performing direct UDP DNS queries.
37+
func UseFabricResolver(c FabricResolverClient) {
38+
fabricClient = c
39+
ResolverAt = func(nsServer string) Resolver {
40+
return FabricResolver{Client: c, NSServer: nsServer}
41+
}
42+
}
43+
44+
// FabricResolver performs DNS lookups via the fabric gRPC API. An empty
45+
// NSServer lets the server perform recursive resolution from the root.
46+
type FabricResolver struct {
47+
Client FabricResolverClient
48+
NSServer string
49+
}
50+
51+
func (r FabricResolver) LookupIPAddr(ctx context.Context, domain string) ([]net.IPAddr, error) {
52+
resp, err := r.Client.ResolveIPAddr(ctx, &defangv1.ResolveIPAddrRequest{
53+
Domain: domain,
54+
NsServer: r.NSServer,
55+
})
56+
if err != nil {
57+
return nil, err
58+
}
59+
ips := make([]net.IPAddr, 0, len(resp.IpAddrs))
60+
for _, s := range resp.IpAddrs {
61+
if ip := net.ParseIP(s); ip != nil {
62+
ips = append(ips, net.IPAddr{IP: ip})
63+
}
64+
}
65+
if len(ips) == 0 {
66+
return nil, ErrNoSuchHost
67+
}
68+
return ips, nil
69+
}
70+
71+
func (r FabricResolver) LookupCNAME(ctx context.Context, domain string) (string, error) {
72+
resp, err := r.Client.ResolveCNAME(ctx, &defangv1.ResolveCNAMERequest{
73+
Domain: domain,
74+
NsServer: r.NSServer,
75+
})
76+
if err != nil {
77+
return "", err
78+
}
79+
if resp.Cname == "" {
80+
return "", ErrNoSuchHost
81+
}
82+
return resp.Cname, nil
83+
}
84+
85+
func (r FabricResolver) LookupNS(ctx context.Context, domain string) ([]*net.NS, error) {
86+
resp, err := r.Client.ResolveNS(ctx, &defangv1.ResolveNSRequest{
87+
Domain: domain,
88+
NsServer: r.NSServer,
89+
})
90+
if err != nil {
91+
return nil, err
92+
}
93+
nss := make([]*net.NS, 0, len(resp.Hosts))
94+
for _, h := range resp.Hosts {
95+
nss = append(nss, &net.NS{Host: h})
96+
}
97+
return nss, nil
98+
}
99+
21100
type RootResolver struct{}
22101

23102
// https://en.wikipedia.org/wiki/Root_name_server
@@ -38,6 +117,9 @@ var rootServers = []*net.NS{
38117
}
39118

40119
func (r RootResolver) LookupIPAddr(ctx context.Context, domain string) ([]net.IPAddr, error) {
120+
if fabricClient != nil {
121+
return FabricResolver{Client: fabricClient}.LookupIPAddr(ctx, domain)
122+
}
41123
for range 10 {
42124
ips, err := r.getResolver(ctx, domain).LookupIPAddr(ctx, domain)
43125
if err != nil {
@@ -54,10 +136,16 @@ func (r RootResolver) LookupIPAddr(ctx context.Context, domain string) ([]net.IP
54136
}
55137

56138
func (r RootResolver) LookupCNAME(ctx context.Context, domain string) (string, error) {
139+
if fabricClient != nil {
140+
return FabricResolver{Client: fabricClient}.LookupCNAME(ctx, domain)
141+
}
57142
return r.getResolver(ctx, domain).LookupCNAME(ctx, domain)
58143
}
59144

60145
func (r RootResolver) LookupNS(ctx context.Context, domain string) ([]*net.NS, error) {
146+
if fabricClient != nil {
147+
return FabricResolver{Client: fabricClient}.LookupNS(ctx, domain)
148+
}
61149
return r.getResolver(ctx, domain).LookupNS(ctx, domain)
62150
}
63151

0 commit comments

Comments
 (0)