Unverified Commit 2454122e authored by Steven Allen's avatar Steven Allen Committed by GitHub

Merge pull request #10 from ipfs/feat/custom-resolver

make DNS resolver pluggable
parents 22432d19 df97fc25
Pipeline #217 failed with stages
in 0 seconds
......@@ -10,9 +10,9 @@
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.
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).
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).
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.
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/).
The package provides implementations for all three resolvers.
......
......@@ -10,14 +10,11 @@ import (
path "github.com/ipfs/go-path"
opts "github.com/ipfs/interface-go-ipfs-core/options/namesys"
isd "github.com/jbenet/go-is-domain"
dns "github.com/miekg/dns"
)
const ethTLD = "eth"
const linkTLD = "domains"
// LookupTXTFunc is a generic type for a function that lookups TXT record values.
type LookupTXTFunc func(name string) (txt []string, err error)
// LookupTXTFunc is a function that lookups TXT record values.
type LookupTXTFunc func(ctx context.Context, name string) (txt []string, err error)
// DNSResolver implements a Resolver on DNS domains
type DNSResolver struct {
......@@ -27,8 +24,8 @@ type DNSResolver struct {
}
// NewDNSResolver constructs a name resolver using DNS TXT records.
func NewDNSResolver() *DNSResolver {
return &DNSResolver{lookupTXT: net.LookupTXT}
func NewDNSResolver(lookup LookupTXTFunc) *DNSResolver {
return &DNSResolver{lookupTXT: lookup}
}
// Resolve implements Resolver.
......@@ -55,7 +52,7 @@ func (r *DNSResolver) resolveOnceAsync(ctx context.Context, name string, options
segments := strings.SplitN(name, "/", 2)
domain := segments[0]
if !isd.IsDomain(domain) {
if _, ok := dns.IsDomainName(domain); !ok {
out <- onceResult{err: fmt.Errorf("not a valid domain name: %s", domain)}
close(out)
return out
......@@ -68,17 +65,11 @@ func (r *DNSResolver) resolveOnceAsync(ctx context.Context, name string, options
fqdn = domain + "."
}
if strings.HasSuffix(fqdn, "."+ethTLD+".") {
// This is an ENS name. As we're resolving via an arbitrary DNS server
// that may not know about .eth we need to add our link domain suffix.
fqdn += linkTLD + "."
}
rootChan := make(chan lookupRes, 1)
go workDomain(r, fqdn, rootChan)
go workDomain(ctx, r, fqdn, rootChan)
subChan := make(chan lookupRes, 1)
go workDomain(r, "_dnslink."+fqdn, subChan)
go workDomain(ctx, r, "_dnslink."+fqdn, subChan)
appendPath := func(p path.Path) (path.Path, error) {
if len(segments) > 1 {
......@@ -139,10 +130,10 @@ func (r *DNSResolver) resolveOnceAsync(ctx context.Context, name string, options
return out
}
func workDomain(r *DNSResolver, name string, res chan lookupRes) {
func workDomain(ctx context.Context, r *DNSResolver, name string, res chan lookupRes) {
defer close(res)
txt, err := r.lookupTXT(name)
txt, err := r.lookupTXT(ctx, name)
if err != nil {
if dnsErr, ok := err.(*net.DNSError); ok {
// If no TXT records found, return same error as when no text
......
package namesys
import (
"context"
"fmt"
"testing"
......@@ -11,7 +12,7 @@ type mockDNS struct {
entries map[string][]string
}
func (m *mockDNS) lookupTXT(name string) (txt []string, err error) {
func (m *mockDNS) lookupTXT(ctx context.Context, name string) (txt []string, err error) {
txt, ok := m.entries[name]
if !ok {
return nil, fmt.Errorf("no TXT entry for %s", name)
......@@ -126,7 +127,16 @@ func newMockDNS() *mockDNS {
"fqdn.example.com.": {
"dnslink=/ipfs/QmYvMB9yrsSf7RKBghkfwmHJkzJhW2ZgVwq3LxBXXPasFr",
},
"www.wealdtech.eth.domains.": {
"en.wikipedia-on-ipfs.org.": {
"dnslink=/ipfs/bafybeiaysi4s6lnjev27ln5icwm6tueaw2vdykrtjkwiphwekaywqhcjze",
},
"custom.non-icann.tldextravaganza.": {
"dnslink=/ipfs/bafybeieto6mcuvqlechv4iadoqvnffondeiwxc2bcfcewhvpsd2odvbmvm",
},
"singlednslabelshouldbeok.": {
"dnslink=/ipfs/bafybeih4a6ylafdki6ailjrdvmr7o4fbbeceeeuty4v3qyyouiz5koqlpi",
},
"www.wealdtech.eth.": {
"dnslink=/ipns/ipfs.example.com",
},
},
......@@ -166,7 +176,10 @@ func TestDNSResolution(t *testing.T) {
testResolution(t, r, "double.example.com", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil)
testResolution(t, r, "conflict.example.com", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjE", nil)
testResolution(t, r, "fqdn.example.com.", opts.DefaultDepthLimit, "/ipfs/QmYvMB9yrsSf7RKBghkfwmHJkzJhW2ZgVwq3LxBXXPasFr", nil)
testResolution(t, r, "en.wikipedia-on-ipfs.org", 2, "/ipfs/bafybeiaysi4s6lnjev27ln5icwm6tueaw2vdykrtjkwiphwekaywqhcjze", nil)
testResolution(t, r, "custom.non-icann.tldextravaganza.", 2, "/ipfs/bafybeieto6mcuvqlechv4iadoqvnffondeiwxc2bcfcewhvpsd2odvbmvm", nil)
testResolution(t, r, "singlednslabelshouldbeok", 2, "/ipfs/bafybeih4a6ylafdki6ailjrdvmr7o4fbbeceeeuty4v3qyyouiz5koqlpi", nil)
testResolution(t, r, "www.wealdtech.eth", 1, "/ipns/ipfs.example.com", ErrResolveRecursion)
testResolution(t, r, "www.wealdtech.eth", 2, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil)
testResolution(t, r, "www.wealdtech.eth.domains", 2, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil)
testResolution(t, r, "www.wealdtech.eth", 2, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil)
}
This diff is collapsed.
......@@ -7,8 +7,7 @@
// DHT).
//
// 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.
// use DNSLink (/ipns/<dnslink_name>, https://docs.ipfs.io/concepts/dnslink/)
//
// The package provides implementations for all three resolvers.
package namesys
......@@ -23,12 +22,14 @@ import (
lru "github.com/hashicorp/golang-lru"
cid "github.com/ipfs/go-cid"
ds "github.com/ipfs/go-datastore"
dssync "github.com/ipfs/go-datastore/sync"
path "github.com/ipfs/go-path"
opts "github.com/ipfs/interface-go-ipfs-core/options/namesys"
isd "github.com/jbenet/go-is-domain"
ci "github.com/libp2p/go-libp2p-core/crypto"
peer "github.com/libp2p/go-libp2p-core/peer"
routing "github.com/libp2p/go-libp2p-core/routing"
dns "github.com/miekg/dns"
madns "github.com/multiformats/go-multiaddr-dns"
)
// mpns (a multi-protocol NameSystem) implements generic IPFS naming.
......@@ -36,27 +37,58 @@ import (
// Uses several Resolvers:
// (a) IPFS routing naming: SFS-like PKI names.
// (b) dns domains: resolves using links in DNS TXT records
// (c) proquints: interprets string as the raw byte data.
//
// It can only publish to: (a) IPFS routing naming.
//
type mpns struct {
dnsResolver, proquintResolver, ipnsResolver resolver
ipnsPublisher Publisher
ds ds.Datastore
dnsResolver, ipnsResolver resolver
ipnsPublisher Publisher
staticMap map[string]path.Path
cache *lru.Cache
}
// NewNameSystem will construct the IPFS naming system based on Routing
func NewNameSystem(r routing.ValueStore, ds ds.Datastore, cachesize int) NameSystem {
var (
cache *lru.Cache
staticMap map[string]path.Path
)
if cachesize > 0 {
cache, _ = lru.New(cachesize)
type Option func(*mpns) error
// WithCache is an option that instructs the name system to use a (LRU) cache of the given size.
func WithCache(size int) Option {
return func(ns *mpns) error {
if size <= 0 {
return fmt.Errorf("invalid cache size %d; must be > 0", size)
}
cache, err := lru.New(size)
if err != nil {
return err
}
ns.cache = cache
return nil
}
}
// WithDNSResolver is an option that supplies a custom DNS resolver to use instead of the system
// default.
func WithDNSResolver(rslv madns.BasicResolver) Option {
return func(ns *mpns) error {
ns.dnsResolver = NewDNSResolver(rslv.LookupTXT)
return nil
}
}
// 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.
func WithDatastore(ds ds.Datastore) Option {
return func(ns *mpns) error {
ns.ds = ds
return nil
}
}
// NewNameSystem will construct the IPFS naming system based on Routing
func NewNameSystem(r routing.ValueStore, opts ...Option) (NameSystem, error) {
var staticMap map[string]path.Path
// Prewarm namesys cache with static records for deterministic tests and debugging.
// 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
}
}
return &mpns{
dnsResolver: NewDNSResolver(),
proquintResolver: new(ProquintResolver),
ipnsResolver: NewIpnsResolver(r),
ipnsPublisher: NewIpnsPublisher(r, ds),
staticMap: staticMap,
cache: cache,
ns := &mpns{
staticMap: staticMap,
}
for _, opt := range opts {
err := opt(ns)
if err != nil {
return nil, err
}
}
if ns.ds == nil {
ns.ds = dssync.MutexWrap(ds.NewMapDatastore())
}
if ns.dnsResolver == nil {
ns.dnsResolver = NewDNSResolver(madns.DefaultResolver.LookupTXT)
}
ns.ipnsResolver = NewIpnsResolver(r)
ns.ipnsPublisher = NewIpnsPublisher(r, ns.ds)
return ns, nil
}
// 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.
// Resolver selection:
// 1. if it is a PeerID/CID/multihash resolve through "ipns".
// 2. if it is a domain name, resolve through "dns"
// 3. otherwise resolve through the "proquint" resolver
var res resolver
ipnsKey, err := peer.Decode(key)
......@@ -175,10 +221,12 @@ func (ns *mpns) resolveOnceAsync(ctx context.Context, name string, options opts.
if err == nil {
res = ns.ipnsResolver
} else if isd.IsDomain(key) {
} else if _, ok := dns.IsDomainName(key); ok {
res = ns.dnsResolver
} else {
res = ns.proquintResolver
out <- onceResult{err: fmt.Errorf("invalid IPNS root: %q", key)}
close(out)
return out
}
resCh := res.resolveOnceAsync(ctx, key, options)
......
......@@ -109,7 +109,11 @@ func TestPublishWithCache0(t *testing.T) {
"pk": record.PublicKeyValidator{},
})
nsys := NewNameSystem(routing, dst, 0)
nsys, err := NewNameSystem(routing, WithDatastore(dst))
if err != nil {
t.Fatal(err)
}
// CID is arbitrary.
p, err := path.ParsePath("QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn")
if err != nil {
......@@ -142,7 +146,11 @@ func TestPublishWithTTL(t *testing.T) {
"pk": record.PublicKeyValidator{},
})
nsys := NewNameSystem(routing, dst, 128)
nsys, err := NewNameSystem(routing, WithDatastore(dst), WithCache(128))
if err != nil {
t.Fatal(err)
}
// CID is arbitrary.
p, err := path.ParsePath("QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn")
if err != nil {
......
package namesys
import (
"context"
"errors"
proquint "github.com/bren2010/proquint"
path "github.com/ipfs/go-path"
opts "github.com/ipfs/interface-go-ipfs-core/options/namesys"
)
// ProquintResolver implements the Resolver interface for proquint identifiers
// (see http://arxiv.org/html/0901.4016).
type ProquintResolver struct{}
// Resolve implements Resolver.
func (r *ProquintResolver) Resolve(ctx context.Context, name string, options ...opts.ResolveOpt) (path.Path, error) {
return resolve(ctx, r, name, opts.ProcessOpts(options))
}
// resolveOnce implements resolver. Decodes the proquint string.
func (r *ProquintResolver) resolveOnceAsync(ctx context.Context, name string, options opts.ResolveOpts) <-chan onceResult {
out := make(chan onceResult, 1)
defer close(out)
ok, err := proquint.IsProquint(name)
if err != nil || !ok {
out <- onceResult{err: errors.New("not a valid proquint string")}
return out
}
// Return a 0 TTL as caching this result is pointless.
out <- onceResult{value: path.FromString(string(proquint.Decode(name)))}
return out
}
......@@ -74,7 +74,10 @@ func TestRepublish(t *testing.T) {
var nodes []*mockNode
for i := 0; i < 10; i++ {
n := getMockNode(t, ctx)
ns := namesys.NewNameSystem(n.dht, n.store, 0)
ns, err := namesys.NewNameSystem(n.dht, namesys.WithDatastore(n.store))
if err != nil {
t.Fatal(err)
}
nsystems = append(nsystems, ns)
nodes = append(nodes, n)
......@@ -153,7 +156,10 @@ func TestLongEOLRepublish(t *testing.T) {
var nodes []*mockNode
for i := 0; i < 10; i++ {
n := getMockNode(t, ctx)
ns := namesys.NewNameSystem(n.dht, n.store, 0)
ns, err := namesys.NewNameSystem(n.dht, namesys.WithDatastore(n.store))
if err != nil {
t.Fatal(err)
}
nsystems = append(nsystems, ns)
nodes = append(nodes, n)
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment