Skip to content

Commit 2b95a2f

Browse files
authored
Merge pull request ipfs/go-namesys#10 from ipfs/feat/custom-resolver
make DNS resolver pluggable This commit was moved from ipfs/go-namesys@2454122
2 parents 5cfe615 + be78492 commit 2b95a2f

File tree

7 files changed

+118
-86
lines changed

7 files changed

+118
-86
lines changed

namesys/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
1111
Package namesys defines `Resolver` and `Publisher` interfaces for IPNS paths, that is, paths in the form of `/ipns/<name_to_be_resolved>`. A "resolved" IPNS path becomes an `/ipfs/<cid>` path.
1212

13-
Traditionally, these paths would be in the form of `/ipns/peer_id`, which references an IPNS record in a distributed `ValueStore` (usually the IPFS DHT).
13+
Traditionally, these paths would be in the form of `/ipns/{libp2p-key}`, which references an IPNS record in a distributed `ValueStore` (usually the IPFS DHT).
1414

15-
Additionally, the /ipns/ namespace can also be used with domain names that use DNSLink (/ipns/my.domain.example, see https://dnslink.io) and proquint strings.
15+
Additionally, the `/ipns/` namespace can also be used with domain names that use DNSLink (`/ipns/en.wikipedia-on-ipfs.org`, see https://docs.ipfs.io/concepts/dnslink/).
1616

1717
The package provides implementations for all three resolvers.
1818

namesys/dns.go

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,11 @@ import (
1010

1111
path "github.com/ipfs/go-path"
1212
opts "github.com/ipfs/interface-go-ipfs-core/options/namesys"
13-
isd "github.com/jbenet/go-is-domain"
13+
dns "github.com/miekg/dns"
1414
)
1515

16-
const ethTLD = "eth"
17-
const linkTLD = "domains"
18-
19-
// LookupTXTFunc is a generic type for a function that lookups TXT record values.
20-
type LookupTXTFunc func(name string) (txt []string, err error)
16+
// LookupTXTFunc is a function that lookups TXT record values.
17+
type LookupTXTFunc func(ctx context.Context, name string) (txt []string, err error)
2118

2219
// DNSResolver implements a Resolver on DNS domains
2320
type DNSResolver struct {
@@ -27,8 +24,8 @@ type DNSResolver struct {
2724
}
2825

2926
// NewDNSResolver constructs a name resolver using DNS TXT records.
30-
func NewDNSResolver() *DNSResolver {
31-
return &DNSResolver{lookupTXT: net.LookupTXT}
27+
func NewDNSResolver(lookup LookupTXTFunc) *DNSResolver {
28+
return &DNSResolver{lookupTXT: lookup}
3229
}
3330

3431
// Resolve implements Resolver.
@@ -55,7 +52,7 @@ func (r *DNSResolver) resolveOnceAsync(ctx context.Context, name string, options
5552
segments := strings.SplitN(name, "/", 2)
5653
domain := segments[0]
5754

58-
if !isd.IsDomain(domain) {
55+
if _, ok := dns.IsDomainName(domain); !ok {
5956
out <- onceResult{err: fmt.Errorf("not a valid domain name: %s", domain)}
6057
close(out)
6158
return out
@@ -68,17 +65,11 @@ func (r *DNSResolver) resolveOnceAsync(ctx context.Context, name string, options
6865
fqdn = domain + "."
6966
}
7067

71-
if strings.HasSuffix(fqdn, "."+ethTLD+".") {
72-
// This is an ENS name. As we're resolving via an arbitrary DNS server
73-
// that may not know about .eth we need to add our link domain suffix.
74-
fqdn += linkTLD + "."
75-
}
76-
7768
rootChan := make(chan lookupRes, 1)
78-
go workDomain(r, fqdn, rootChan)
69+
go workDomain(ctx, r, fqdn, rootChan)
7970

8071
subChan := make(chan lookupRes, 1)
81-
go workDomain(r, "_dnslink."+fqdn, subChan)
72+
go workDomain(ctx, r, "_dnslink."+fqdn, subChan)
8273

8374
appendPath := func(p path.Path) (path.Path, error) {
8475
if len(segments) > 1 {
@@ -139,10 +130,10 @@ func (r *DNSResolver) resolveOnceAsync(ctx context.Context, name string, options
139130
return out
140131
}
141132

142-
func workDomain(r *DNSResolver, name string, res chan lookupRes) {
133+
func workDomain(ctx context.Context, r *DNSResolver, name string, res chan lookupRes) {
143134
defer close(res)
144135

145-
txt, err := r.lookupTXT(name)
136+
txt, err := r.lookupTXT(ctx, name)
146137
if err != nil {
147138
if dnsErr, ok := err.(*net.DNSError); ok {
148139
// If no TXT records found, return same error as when no text

namesys/dns_test.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package namesys
22

33
import (
4+
"context"
45
"fmt"
56
"testing"
67

@@ -11,7 +12,7 @@ type mockDNS struct {
1112
entries map[string][]string
1213
}
1314

14-
func (m *mockDNS) lookupTXT(name string) (txt []string, err error) {
15+
func (m *mockDNS) lookupTXT(ctx context.Context, name string) (txt []string, err error) {
1516
txt, ok := m.entries[name]
1617
if !ok {
1718
return nil, fmt.Errorf("no TXT entry for %s", name)
@@ -126,7 +127,16 @@ func newMockDNS() *mockDNS {
126127
"fqdn.example.com.": {
127128
"dnslink=/ipfs/QmYvMB9yrsSf7RKBghkfwmHJkzJhW2ZgVwq3LxBXXPasFr",
128129
},
129-
"www.wealdtech.eth.domains.": {
130+
"en.wikipedia-on-ipfs.org.": {
131+
"dnslink=/ipfs/bafybeiaysi4s6lnjev27ln5icwm6tueaw2vdykrtjkwiphwekaywqhcjze",
132+
},
133+
"custom.non-icann.tldextravaganza.": {
134+
"dnslink=/ipfs/bafybeieto6mcuvqlechv4iadoqvnffondeiwxc2bcfcewhvpsd2odvbmvm",
135+
},
136+
"singlednslabelshouldbeok.": {
137+
"dnslink=/ipfs/bafybeih4a6ylafdki6ailjrdvmr7o4fbbeceeeuty4v3qyyouiz5koqlpi",
138+
},
139+
"www.wealdtech.eth.": {
130140
"dnslink=/ipns/ipfs.example.com",
131141
},
132142
},
@@ -166,7 +176,10 @@ func TestDNSResolution(t *testing.T) {
166176
testResolution(t, r, "double.example.com", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil)
167177
testResolution(t, r, "conflict.example.com", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjE", nil)
168178
testResolution(t, r, "fqdn.example.com.", opts.DefaultDepthLimit, "/ipfs/QmYvMB9yrsSf7RKBghkfwmHJkzJhW2ZgVwq3LxBXXPasFr", nil)
179+
testResolution(t, r, "en.wikipedia-on-ipfs.org", 2, "/ipfs/bafybeiaysi4s6lnjev27ln5icwm6tueaw2vdykrtjkwiphwekaywqhcjze", nil)
180+
testResolution(t, r, "custom.non-icann.tldextravaganza.", 2, "/ipfs/bafybeieto6mcuvqlechv4iadoqvnffondeiwxc2bcfcewhvpsd2odvbmvm", nil)
181+
testResolution(t, r, "singlednslabelshouldbeok", 2, "/ipfs/bafybeih4a6ylafdki6ailjrdvmr7o4fbbeceeeuty4v3qyyouiz5koqlpi", nil)
169182
testResolution(t, r, "www.wealdtech.eth", 1, "/ipns/ipfs.example.com", ErrResolveRecursion)
170183
testResolution(t, r, "www.wealdtech.eth", 2, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil)
171-
testResolution(t, r, "www.wealdtech.eth.domains", 2, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil)
184+
testResolution(t, r, "www.wealdtech.eth", 2, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil)
172185
}

namesys/namesys.go

Lines changed: 72 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@
77
// DHT).
88
//
99
// Additionally, the /ipns/ namespace can also be used with domain names that
10-
// use DNSLink (/ipns/my.domain.example, see https://dnslink.io) and proquint
11-
// strings.
10+
// use DNSLink (/ipns/<dnslink_name>, https://docs.ipfs.io/concepts/dnslink/)
1211
//
1312
// The package provides implementations for all three resolvers.
1413
package namesys
@@ -23,40 +22,73 @@ import (
2322
lru "github.com/hashicorp/golang-lru"
2423
cid "github.com/ipfs/go-cid"
2524
ds "github.com/ipfs/go-datastore"
25+
dssync "github.com/ipfs/go-datastore/sync"
2626
path "github.com/ipfs/go-path"
2727
opts "github.com/ipfs/interface-go-ipfs-core/options/namesys"
28-
isd "github.com/jbenet/go-is-domain"
2928
ci "github.com/libp2p/go-libp2p-core/crypto"
3029
peer "github.com/libp2p/go-libp2p-core/peer"
3130
routing "github.com/libp2p/go-libp2p-core/routing"
31+
dns "github.com/miekg/dns"
32+
madns "github.com/multiformats/go-multiaddr-dns"
3233
)
3334

3435
// mpns (a multi-protocol NameSystem) implements generic IPFS naming.
3536
//
3637
// Uses several Resolvers:
3738
// (a) IPFS routing naming: SFS-like PKI names.
3839
// (b) dns domains: resolves using links in DNS TXT records
39-
// (c) proquints: interprets string as the raw byte data.
4040
//
4141
// It can only publish to: (a) IPFS routing naming.
4242
//
4343
type mpns struct {
44-
dnsResolver, proquintResolver, ipnsResolver resolver
45-
ipnsPublisher Publisher
44+
ds ds.Datastore
45+
46+
dnsResolver, ipnsResolver resolver
47+
ipnsPublisher Publisher
4648

4749
staticMap map[string]path.Path
4850
cache *lru.Cache
4951
}
5052

51-
// NewNameSystem will construct the IPFS naming system based on Routing
52-
func NewNameSystem(r routing.ValueStore, ds ds.Datastore, cachesize int) NameSystem {
53-
var (
54-
cache *lru.Cache
55-
staticMap map[string]path.Path
56-
)
57-
if cachesize > 0 {
58-
cache, _ = lru.New(cachesize)
53+
type Option func(*mpns) error
54+
55+
// WithCache is an option that instructs the name system to use a (LRU) cache of the given size.
56+
func WithCache(size int) Option {
57+
return func(ns *mpns) error {
58+
if size <= 0 {
59+
return fmt.Errorf("invalid cache size %d; must be > 0", size)
60+
}
61+
62+
cache, err := lru.New(size)
63+
if err != nil {
64+
return err
65+
}
66+
67+
ns.cache = cache
68+
return nil
5969
}
70+
}
71+
72+
// WithDNSResolver is an option that supplies a custom DNS resolver to use instead of the system
73+
// default.
74+
func WithDNSResolver(rslv madns.BasicResolver) Option {
75+
return func(ns *mpns) error {
76+
ns.dnsResolver = NewDNSResolver(rslv.LookupTXT)
77+
return nil
78+
}
79+
}
80+
81+
// WithDatastore is an option that supplies a datastore to use instead of an in-memory map datastore. The datastore is used to store published IPNS records and make them available for querying.
82+
func WithDatastore(ds ds.Datastore) Option {
83+
return func(ns *mpns) error {
84+
ns.ds = ds
85+
return nil
86+
}
87+
}
88+
89+
// NewNameSystem will construct the IPFS naming system based on Routing
90+
func NewNameSystem(r routing.ValueStore, opts ...Option) (NameSystem, error) {
91+
var staticMap map[string]path.Path
6092

6193
// Prewarm namesys cache with static records for deterministic tests and debugging.
6294
// Useful for testing things like DNSLink without real DNS lookup.
@@ -72,14 +104,29 @@ func NewNameSystem(r routing.ValueStore, ds ds.Datastore, cachesize int) NameSys
72104
}
73105
}
74106

75-
return &mpns{
76-
dnsResolver: NewDNSResolver(),
77-
proquintResolver: new(ProquintResolver),
78-
ipnsResolver: NewIpnsResolver(r),
79-
ipnsPublisher: NewIpnsPublisher(r, ds),
80-
staticMap: staticMap,
81-
cache: cache,
107+
ns := &mpns{
108+
staticMap: staticMap,
109+
}
110+
111+
for _, opt := range opts {
112+
err := opt(ns)
113+
if err != nil {
114+
return nil, err
115+
}
116+
}
117+
118+
if ns.ds == nil {
119+
ns.ds = dssync.MutexWrap(ds.NewMapDatastore())
82120
}
121+
122+
if ns.dnsResolver == nil {
123+
ns.dnsResolver = NewDNSResolver(madns.DefaultResolver.LookupTXT)
124+
}
125+
126+
ns.ipnsResolver = NewIpnsResolver(r)
127+
ns.ipnsPublisher = NewIpnsPublisher(r, ns.ds)
128+
129+
return ns, nil
83130
}
84131

85132
// DefaultResolverCacheTTL defines max ttl of a record placed in namesys cache.
@@ -138,7 +185,6 @@ func (ns *mpns) resolveOnceAsync(ctx context.Context, name string, options opts.
138185
// Resolver selection:
139186
// 1. if it is a PeerID/CID/multihash resolve through "ipns".
140187
// 2. if it is a domain name, resolve through "dns"
141-
// 3. otherwise resolve through the "proquint" resolver
142188

143189
var res resolver
144190
ipnsKey, err := peer.Decode(key)
@@ -175,10 +221,12 @@ func (ns *mpns) resolveOnceAsync(ctx context.Context, name string, options opts.
175221

176222
if err == nil {
177223
res = ns.ipnsResolver
178-
} else if isd.IsDomain(key) {
224+
} else if _, ok := dns.IsDomainName(key); ok {
179225
res = ns.dnsResolver
180226
} else {
181-
res = ns.proquintResolver
227+
out <- onceResult{err: fmt.Errorf("invalid IPNS root: %q", key)}
228+
close(out)
229+
return out
182230
}
183231

184232
resCh := res.resolveOnceAsync(ctx, key, options)

namesys/namesys_test.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,11 @@ func TestPublishWithCache0(t *testing.T) {
109109
"pk": record.PublicKeyValidator{},
110110
})
111111

112-
nsys := NewNameSystem(routing, dst, 0)
112+
nsys, err := NewNameSystem(routing, WithDatastore(dst))
113+
if err != nil {
114+
t.Fatal(err)
115+
}
116+
113117
// CID is arbitrary.
114118
p, err := path.ParsePath("QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn")
115119
if err != nil {
@@ -142,7 +146,11 @@ func TestPublishWithTTL(t *testing.T) {
142146
"pk": record.PublicKeyValidator{},
143147
})
144148

145-
nsys := NewNameSystem(routing, dst, 128)
149+
nsys, err := NewNameSystem(routing, WithDatastore(dst), WithCache(128))
150+
if err != nil {
151+
t.Fatal(err)
152+
}
153+
146154
// CID is arbitrary.
147155
p, err := path.ParsePath("QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn")
148156
if err != nil {

namesys/proquint.go

Lines changed: 0 additions & 34 deletions
This file was deleted.

namesys/republisher/repub_test.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,10 @@ func TestRepublish(t *testing.T) {
7474
var nodes []*mockNode
7575
for i := 0; i < 10; i++ {
7676
n := getMockNode(t, ctx)
77-
ns := namesys.NewNameSystem(n.dht, n.store, 0)
77+
ns, err := namesys.NewNameSystem(n.dht, namesys.WithDatastore(n.store))
78+
if err != nil {
79+
t.Fatal(err)
80+
}
7881

7982
nsystems = append(nsystems, ns)
8083
nodes = append(nodes, n)
@@ -153,7 +156,10 @@ func TestLongEOLRepublish(t *testing.T) {
153156
var nodes []*mockNode
154157
for i := 0; i < 10; i++ {
155158
n := getMockNode(t, ctx)
156-
ns := namesys.NewNameSystem(n.dht, n.store, 0)
159+
ns, err := namesys.NewNameSystem(n.dht, namesys.WithDatastore(n.store))
160+
if err != nil {
161+
t.Fatal(err)
162+
}
157163

158164
nsystems = append(nsystems, ns)
159165
nodes = append(nodes, n)

0 commit comments

Comments
 (0)