-
Notifications
You must be signed in to change notification settings - Fork 18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
refactor Resolver to support custom per-TLD resolvers #26
Changes from all commits
f9a0619
807a426
95b70c8
cf42e56
1e9c3ec
d965d38
45cdfcf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package madns | ||
|
||
import ( | ||
"context" | ||
"net" | ||
) | ||
|
||
type MockResolver struct { | ||
IP map[string][]net.IPAddr | ||
TXT map[string][]string | ||
} | ||
|
||
var _ BasicResolver = (*MockResolver)(nil) | ||
|
||
func (r *MockResolver) LookupIPAddr(ctx context.Context, name string) ([]net.IPAddr, error) { | ||
results, ok := r.IP[name] | ||
if ok { | ||
return results, nil | ||
} else { | ||
return []net.IPAddr{}, nil | ||
} | ||
} | ||
|
||
func (r *MockResolver) LookupTXT(ctx context.Context, name string) ([]string, error) { | ||
results, ok := r.TXT[name] | ||
if ok { | ||
return results, nil | ||
} else { | ||
return []string{}, nil | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,59 +9,86 @@ import ( | |
) | ||
|
||
var ResolvableProtocols = []ma.Protocol{DnsaddrProtocol, Dns4Protocol, Dns6Protocol, DnsProtocol} | ||
var DefaultResolver = &Resolver{Backend: net.DefaultResolver} | ||
var DefaultResolver = &Resolver{def: net.DefaultResolver} | ||
|
||
const dnsaddrTXTPrefix = "dnsaddr=" | ||
|
||
type Backend interface { | ||
// BasicResolver is a low level interface for DNS resolution | ||
type BasicResolver interface { | ||
LookupIPAddr(context.Context, string) ([]net.IPAddr, error) | ||
LookupTXT(context.Context, string) ([]string, error) | ||
} | ||
|
||
// Resolver is an object capable of resolving dns multiaddrs by using one or more BasicResolvers; | ||
// it supports custom per domain/TLD resolvers. | ||
// It also implements the BasicResolver interface so that it can act as a custom per domain/TLD | ||
// resolver. | ||
type Resolver struct { | ||
Backend Backend | ||
Comment on lines
26
to
-22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @vyzo IIRC this is making a choice to break backwards compatibility here a bit for the sake of future extensibility. Two breakages:
cc @Stebalien in case you have any issues with the breakages. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, conscious choice -- having the fields public was just a bad idea. |
||
def BasicResolver | ||
custom map[string]BasicResolver | ||
} | ||
|
||
var _ Backend = (*MockBackend)(nil) | ||
var _ BasicResolver = (*Resolver)(nil) | ||
|
||
type MockBackend struct { | ||
IP map[string][]net.IPAddr | ||
TXT map[string][]string | ||
// NewResolver creates a new Resolver instance with the specified options | ||
func NewResolver(opts ...Option) (*Resolver, error) { | ||
r := &Resolver{def: net.DefaultResolver} | ||
for _, opt := range opts { | ||
err := opt(r) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
|
||
return r, nil | ||
} | ||
|
||
func (r *MockBackend) LookupIPAddr(ctx context.Context, name string) ([]net.IPAddr, error) { | ||
results, ok := r.IP[name] | ||
if ok { | ||
return results, nil | ||
} else { | ||
return []net.IPAddr{}, nil | ||
type Option func(*Resolver) error | ||
|
||
// WithDefaultResolver is an option that specifies the default basic resolver, | ||
// which resolves any TLD that doesn't have a custom resolver. | ||
// Defaults to net.DefaultResolver | ||
func WithDefaultResolver(def BasicResolver) Option { | ||
return func(r *Resolver) error { | ||
r.def = def | ||
return nil | ||
} | ||
} | ||
|
||
func (r *MockBackend) LookupTXT(ctx context.Context, name string) ([]string, error) { | ||
results, ok := r.TXT[name] | ||
if ok { | ||
return results, nil | ||
} else { | ||
return []string{}, nil | ||
// WithDomainResolver specifies a custom resolver for a domain/TLD. | ||
// Custom resolver selection matches domains left to right, with more specific resolvers | ||
// superseding generic ones. | ||
func WithDomainResolver(domain string, rslv BasicResolver) Option { | ||
return func(r *Resolver) error { | ||
if r.custom == nil { | ||
r.custom = make(map[string]BasicResolver) | ||
} | ||
r.custom[domain] = rslv | ||
return nil | ||
} | ||
} | ||
|
||
func Matches(maddr ma.Multiaddr) (matches bool) { | ||
ma.ForEach(maddr, func(c ma.Component) bool { | ||
switch c.Protocol().Code { | ||
case DnsProtocol.Code, Dns4Protocol.Code, Dns6Protocol.Code, DnsaddrProtocol.Code: | ||
matches = true | ||
func (r *Resolver) getResolver(domain string) BasicResolver { | ||
// we match left-to-right, with more specific resolvers superseding generic ones. | ||
// So for a domain a.b.c, we will try a.b,c, b.c, c, and fallback to the default if | ||
// there is no match | ||
rslv, ok := r.custom[domain] | ||
if ok { | ||
return rslv | ||
} | ||
|
||
for i := strings.Index(domain, "."); i != -1; i = strings.Index(domain, ".") { | ||
domain = domain[i+1:] | ||
rslv, ok = r.custom[domain] | ||
if ok { | ||
return rslv | ||
} | ||
return !matches | ||
}) | ||
return matches | ||
} | ||
} | ||
|
||
func Resolve(ctx context.Context, maddr ma.Multiaddr) ([]ma.Multiaddr, error) { | ||
return DefaultResolver.Resolve(ctx, maddr) | ||
return r.def | ||
} | ||
|
||
// Resolve resolves a DNS multiaddr. | ||
func (r *Resolver) Resolve(ctx context.Context, maddr ma.Multiaddr) ([]ma.Multiaddr, error) { | ||
var results []ma.Multiaddr | ||
for i := 0; maddr != nil; i++ { | ||
|
@@ -99,6 +126,7 @@ func (r *Resolver) Resolve(ctx context.Context, maddr ma.Multiaddr) ([]ma.Multia | |
|
||
proto := resolve.Protocol() | ||
value := resolve.Value() | ||
rslv := r.getResolver(value) | ||
|
||
// resolve the dns component | ||
var resolved []ma.Multiaddr | ||
|
@@ -114,7 +142,7 @@ func (r *Resolver) Resolve(ctx context.Context, maddr ma.Multiaddr) ([]ma.Multia | |
// differentiating between IPv6 and IPv4. A v4-in-v6 | ||
// AAAA record will _look_ like an A record to us and | ||
// there's nothing we can do about that. | ||
records, err := r.Backend.LookupIPAddr(ctx, value) | ||
records, err := rslv.LookupIPAddr(ctx, value) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
@@ -155,7 +183,7 @@ func (r *Resolver) Resolve(ctx context.Context, maddr ma.Multiaddr) ([]ma.Multia | |
// matching the result of step 2. | ||
|
||
// First, lookup the TXT record | ||
records, err := r.Backend.LookupTXT(ctx, "_dnsaddr."+value) | ||
records, err := rslv.LookupTXT(ctx, "_dnsaddr."+value) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
@@ -235,37 +263,10 @@ func (r *Resolver) Resolve(ctx context.Context, maddr ma.Multiaddr) ([]ma.Multia | |
return results, nil | ||
} | ||
|
||
// counts the number of components in the multiaddr | ||
func addrLen(maddr ma.Multiaddr) int { | ||
length := 0 | ||
ma.ForEach(maddr, func(_ ma.Component) bool { | ||
length++ | ||
return true | ||
}) | ||
return length | ||
} | ||
|
||
// trims `offset` components from the beginning of the multiaddr. | ||
func offset(maddr ma.Multiaddr, offset int) ma.Multiaddr { | ||
_, after := ma.SplitFunc(maddr, func(c ma.Component) bool { | ||
if offset == 0 { | ||
return true | ||
} | ||
offset-- | ||
return false | ||
}) | ||
return after | ||
func (r *Resolver) LookupIPAddr(ctx context.Context, domain string) ([]net.IPAddr, error) { | ||
return r.getResolver(domain).LookupIPAddr(ctx, domain) | ||
} | ||
|
||
// takes the cross product of two sets of multiaddrs | ||
// | ||
// assumes `a` is non-empty. | ||
func cross(a, b []ma.Multiaddr) []ma.Multiaddr { | ||
res := make([]ma.Multiaddr, 0, len(a)*len(b)) | ||
for _, x := range a { | ||
for _, y := range b { | ||
res = append(res, x.Encapsulate(y)) | ||
} | ||
} | ||
return res | ||
func (r *Resolver) LookupTXT(ctx context.Context, txt string) ([]string, error) { | ||
return r.getResolver(txt).LookupTXT(ctx, txt) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
package madns | ||
|
||
import ( | ||
"context" | ||
|
||
ma "github.com/multiformats/go-multiaddr" | ||
) | ||
|
||
func Matches(maddr ma.Multiaddr) (matches bool) { | ||
ma.ForEach(maddr, func(c ma.Component) bool { | ||
switch c.Protocol().Code { | ||
case DnsProtocol.Code, Dns4Protocol.Code, Dns6Protocol.Code, DnsaddrProtocol.Code: | ||
matches = true | ||
} | ||
return !matches | ||
}) | ||
return matches | ||
} | ||
|
||
func Resolve(ctx context.Context, maddr ma.Multiaddr) ([]ma.Multiaddr, error) { | ||
return DefaultResolver.Resolve(ctx, maddr) | ||
} | ||
|
||
// counts the number of components in the multiaddr | ||
func addrLen(maddr ma.Multiaddr) int { | ||
length := 0 | ||
ma.ForEach(maddr, func(_ ma.Component) bool { | ||
length++ | ||
return true | ||
}) | ||
return length | ||
} | ||
|
||
// trims `offset` components from the beginning of the multiaddr. | ||
func offset(maddr ma.Multiaddr, offset int) ma.Multiaddr { | ||
_, after := ma.SplitFunc(maddr, func(c ma.Component) bool { | ||
if offset == 0 { | ||
return true | ||
} | ||
offset-- | ||
return false | ||
}) | ||
return after | ||
} | ||
|
||
// takes the cross product of two sets of multiaddrs | ||
// | ||
// assumes `a` is non-empty. | ||
func cross(a, b []ma.Multiaddr) []ma.Multiaddr { | ||
res := make([]ma.Multiaddr, 0, len(a)*len(b)) | ||
for _, x := range a { | ||
for _, y := range b { | ||
res = append(res, x.Encapsulate(y)) | ||
} | ||
} | ||
return res | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if I have enough context here, but doesn't DNS allow us to lookup A, AAAA and TXT records in the same query? This interface suggests that looking up both would cause 2 queries (and potentially 2 roundtrips).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In theory it does, but in practice it doesn't work as most DNS servers only respond to the first one.
There is a caveat about that in the dns library.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, they have different usage, we don't do concurrent A/AAAA and TXT resolution in our usage patterns.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be fair, at some point may have some overlap in our usage patterns.
For example, a request for DNSLink might be accompanied by a lookup for a content routing hint (if we implement some form of ipfs/kubo#6516).
That being said, I don't think we should do any premature optimizations.
When time comes, adding cache for raw DNS records (that respects their TTL) will be enough.