diff --git a/core/commands/dns.go b/core/commands/dns.go index a775f09db1f..d51f4f16914 100644 --- a/core/commands/dns.go +++ b/core/commands/dns.go @@ -4,6 +4,7 @@ import ( "fmt" "io" + oldcmds "github.com/ipfs/go-ipfs/commands" ncmd "github.com/ipfs/go-ipfs/core/commands/name" namesys "github.com/ipfs/go-ipfs/namesys" nsopts "github.com/ipfs/interface-go-ipfs-core/options/namesys" @@ -60,9 +61,18 @@ The resolver can recursively resolve: cmds.BoolOption(dnsRecursiveOptionName, "r", "Resolve until the result is not a DNS link.").WithDefault(true), }, Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error { + cctx := env.(*oldcmds.Context) + cfg, err := cctx.GetConfig() + if err != nil { + return err + } + recursive, _ := req.Options[dnsRecursiveOptionName].(bool) name := req.Arguments[0] - resolver := namesys.NewDNSResolver() + resolver, err := namesys.NewDNSResolver(cfg) + if err != nil { + return err + } var routing []nsopts.ResolveOpt if !recursive { diff --git a/core/coreapi/coreapi.go b/core/coreapi/coreapi.go index 5f2201df624..025e2da6182 100644 --- a/core/coreapi/coreapi.go +++ b/core/coreapi/coreapi.go @@ -215,8 +215,11 @@ func (api *CoreAPI) WithOptions(opts ...options.ApiOption) (coreiface.CoreAPI, e } subApi.routing = offlineroute.NewOfflineRouter(subApi.repo.Datastore(), subApi.recordValidator) - subApi.namesys = namesys.NewNameSystem(subApi.routing, subApi.repo.Datastore(), cs) subApi.provider = provider.NewOfflineProvider() + subApi.namesys, err = namesys.NewNameSystem(subApi.routing, subApi.repo.Datastore(), cs, cfg) + if err != nil { + return nil, err + } subApi.peerstore = nil subApi.peerHost = nil diff --git a/core/coreapi/name.go b/core/coreapi/name.go index da38b929b18..76967d82f6a 100644 --- a/core/coreapi/name.go +++ b/core/coreapi/name.go @@ -95,7 +95,14 @@ func (api *NameAPI) Search(ctx context.Context, name string, opts ...caopts.Name var resolver namesys.Resolver = api.namesys if !options.Cache { - resolver = namesys.NewNameSystem(api.routing, api.repo.Datastore(), 0) + cfg, err := api.repo.Config() + if err != nil { + return nil, err + } + resolver, err = namesys.NewNameSystem(api.routing, api.repo.Datastore(), 0, cfg) + if err != nil { + return nil, err + } } if !strings.HasPrefix(name, "/ipns/") { diff --git a/core/node/ipns.go b/core/node/ipns.go index 11769d97f92..2a442c23cc7 100644 --- a/core/node/ipns.go +++ b/core/node/ipns.go @@ -29,7 +29,15 @@ func RecordValidator(ps peerstore.Peerstore) record.Validator { // Namesys creates new name system func Namesys(cacheSize int) func(rt routing.Routing, repo repo.Repo) (namesys.NameSystem, error) { return func(rt routing.Routing, repo repo.Repo) (namesys.NameSystem, error) { - return namesys.NewNameSystem(rt, repo.Datastore(), cacheSize), nil + cfg, err := repo.Config() + if err != nil { + return nil, err + } + ns, err := namesys.NewNameSystem(rt, repo.Datastore(), cacheSize, cfg) + if err != nil { + return nil, err + } + return ns, nil } } diff --git a/go.mod b/go.mod index fca0933ada9..5759c44ef26 100644 --- a/go.mod +++ b/go.mod @@ -87,6 +87,7 @@ require ( github.com/libp2p/go-maddr-filter v0.0.5 github.com/mattn/go-runewidth v0.0.4 // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect + github.com/miekg/dns v1.1.12 github.com/mitchellh/go-homedir v1.1.0 github.com/mr-tron/base58 v1.1.2 github.com/multiformats/go-multiaddr v0.0.4 @@ -95,6 +96,7 @@ require ( github.com/multiformats/go-multibase v0.0.1 github.com/multiformats/go-multihash v0.0.5 github.com/opentracing/opentracing-go v1.1.0 + github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pkg/errors v0.8.1 github.com/prometheus/client_golang v0.9.3 github.com/prometheus/procfs v0.0.0-20190519111021-9935e8e0588d // indirect @@ -109,7 +111,9 @@ require ( go.uber.org/goleak v0.10.0 // indirect go.uber.org/multierr v1.1.0 // indirect go4.org v0.0.0-20190313082347-94abd6928b1d // indirect + golang.org/x/net v0.0.0-20190620200207-3b0461eec859 golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb + google.golang.org/appengine v1.4.0 // indirect gopkg.in/cheggaaa/pb.v1 v1.0.28 gotest.tools/gotestsum v0.3.4 ) diff --git a/namesys/dns.go b/namesys/dns.go index 931edec0019..6d4e41415c7 100644 --- a/namesys/dns.go +++ b/namesys/dns.go @@ -3,26 +3,107 @@ package namesys import ( "context" "errors" + "fmt" "net" + "strconv" "strings" + config "github.com/ipfs/go-ipfs-config" path "github.com/ipfs/go-path" opts "github.com/ipfs/interface-go-ipfs-core/options/namesys" isd "github.com/jbenet/go-is-domain" + ma "github.com/multiformats/go-multiaddr" ) +// LookupTXTFunc is the interface for the lookupTXT property of DNSResolver type LookupTXTFunc func(name string) (txt []string, err error) // DNSResolver implements a Resolver on DNS domains type DNSResolver struct { lookupTXT LookupTXTFunc - // TODO: maybe some sort of caching? - // cache would need a timeout } // NewDNSResolver constructs a name resolver using DNS TXT records. -func NewDNSResolver() *DNSResolver { - return &DNSResolver{lookupTXT: net.LookupTXT} +func NewDNSResolver(cfg *config.Config) (*DNSResolver, error) { + // Check if we're using a custom DNS server + if len(cfg.DNS.Resolver) > 0 { + // Custom DNS resolver + dns := &customDNS{} + + // Parse the multi-address + resolverMaddr, err := ma.NewMultiaddr(cfg.DNS.Resolver) + if err != nil { + return nil, fmt.Errorf("Invalid DNS Resolver address: %q (err: %s)", cfg.DNS.Resolver, err) + } + proto := resolverMaddr.Protocols() + + // Need to have at least 1 component + if len(proto) < 1 { + return nil, fmt.Errorf("Invalid DNS Resolver address: %q (Not enough components)", cfg.DNS.Resolver) + } + + // First component should be the IP or DNS + if proto[0].Code == ma.P_IP4 || proto[0].Code == ma.P_IP6 { + val, err := resolverMaddr.ValueForProtocol(proto[0].Code) + if err != nil { + return nil, fmt.Errorf("Invalid DNS Resolver address: %q (Invalid value for protocol 0)", cfg.DNS.Resolver) + } + dns.Address = val + } else { + return nil, fmt.Errorf("Invalid DNS Resolver address: %q (Invalid type for protocol 0)", cfg.DNS.Resolver) + } + + // Second component is optional and it's the port + // If it's UDP, we stop here, as we're using normal DNS + // If it's TCP, we will need more info + if len(proto) > 1 && proto[1].Code == ma.P_UDP { + val, err := resolverMaddr.ValueForProtocol(proto[1].Code) + if err != nil { + return nil, fmt.Errorf("Invalid DNS Resolver address: %q (Invalid value for protocol 1)", cfg.DNS.Resolver) + } + dns.Protocol = "udp" + if len(val) > 0 && val != "0" { + n, err := strconv.ParseUint(val, 10, 32) + if err != nil { + return nil, err + } + dns.Port = uint(n) + } + } else if len(proto) > 2 && proto[1].Code == ma.P_TCP { + // Require at least 3 components here because we need to know if it's using tls or https + val, err := resolverMaddr.ValueForProtocol(proto[1].Code) + if err != nil { + return nil, fmt.Errorf("Invalid DNS Resolver address: %q (Invalid value for protocol 1)", cfg.DNS.Resolver) + } + if len(val) > 0 && val != "0" { + n, err := strconv.ParseUint(val, 10, 32) + if err != nil { + return nil, err + } + dns.Port = uint(n) + } + + // Need the protocol: "tls" for DNS-over-TLS or "https" for DNS-over-HTTPS + if proto[2].Code == ma.P_HTTPS { + // TODO: Get the host, not yet supported + dns.Protocol = "dns-over-https" + + // This is pending https://github.com/multiformats/multicodec/pull/145 and the addition of the TLS protocol insiode https://github.com/multiformats/go-multiaddr/ + /*} else if proto[2].Code == ma.P_TLS { + dns.Protocol = "dns-over-tls" */ + } else { + return nil, fmt.Errorf("Invalid DNS Resolver address: %q (Invalid format)", cfg.DNS.Resolver) + } + } else if len(proto) > 1 { + return nil, fmt.Errorf("Invalid DNS Resolver address: %q (Invalid format)", cfg.DNS.Resolver) + } + + // Return the resolver + return &DNSResolver{lookupTXT: dns.LookupTXT}, nil + } + + // Use the system's built-in resolver + return &DNSResolver{lookupTXT: net.LookupTXT}, nil } // Resolve implements Resolver. diff --git a/namesys/dns_resolver.go b/namesys/dns_resolver.go new file mode 100644 index 00000000000..afc5543f8f0 --- /dev/null +++ b/namesys/dns_resolver.go @@ -0,0 +1,215 @@ +package namesys + +import ( + "bytes" + "crypto/tls" + "fmt" + "io/ioutil" + "net/http" + "time" + + "github.com/miekg/dns" + cacheLib "github.com/patrickmn/go-cache" + "golang.org/x/net/http2" +) + +// Class managing custom DNS resolvers +type customDNS struct { + // Address is the address of the DNS server (e.g. 1.1.1.1) + Address string + + // Protocol to use: "udp" (or "" - default), "dns-over-https", "dns-over-tls" + // Optional: default is "" (= "udp") + Protocol string + + // DNSoverHTTPSHost is the value for the "Host" header used when making requests via DNS-over-HTTPS + // Optional: use if necessary + DNSoverHTTPSHost string + + // Port is the port the DNS server listens to + // Optional: default value is 53 (udp), 443 (dns-over-https) or 853 (dns-over-tls) depending on the protocol used + Port uint +} + +// Timeout for DNS requests +const reqTimeout = 5 * time.Second + +// Interval between DNS cache purges +const dnsCachePurgeInterval = 5 * time.Minute + +// Maximum TTL allowed for DNS records (to keep cache leaner) +const maxTTL = 6 * time.Hour + +// Initialize the cache +var cache = cacheLib.New(dnsCachePurgeInterval, dnsCachePurgeInterval) + +// LookupTXT looks up a TXT record, using the cache when available +func (d *customDNS) LookupTXT(name string) (txt []string, err error) { + // Check if the record is available in cache + if msgI, found := cache.Get(name); found { + log.Debugf("Responding from cache\n") + msg := msgI.(*dns.Msg) + // We already checked and are sure that this response exists + t := msg.Answer[0].(*dns.TXT) + txt = t.Txt + return + } + + txt, err = d.RequestTXT(name) + return +} + +// RequestTXT performs the request for the TXT record to the server +func (d *customDNS) RequestTXT(name string) (txt []string, err error) { + // Request + m := new(dns.Msg) + m.SetQuestion(dns.Fqdn(name), dns.TypeTXT) + + // Response + var in *dns.Msg + + // dns-over-https has a different path + if d.Protocol == "dns-over-https" { + // Request + in, err = d.ExchangeDoHRequest(m, name) + if err != nil { + return + } + } else { // dns-over-tls and standard + // Request + in, err = d.ExchangeDNSRequest(m, name) + if err != nil { + return + } + } + + // Get the TXT record + if t, ok := in.Answer[0].(*dns.TXT); ok { + txt = t.Txt + + // Check if we have a TTL + if t.Hdr.Ttl > 0 { + // Cache the result + d.cacheResult(name, in) + } + } + + return +} + +// ExchangeDNSRequest sends a request using the DNS UDP protocol or DNS-over-TLS +func (d *customDNS) ExchangeDNSRequest(msg *dns.Msg, name string) (in *dns.Msg, err error) { + // Create the DNS client with the correct transport, then set the port + var c dns.Client + port := d.Port + if d.Protocol == "dns-over-tls" { + c = dns.Client{ + Net: "tcp-tls", + DialTimeout: reqTimeout, + } + + if port == 0 { + port = 853 + } + } else { + c = dns.Client{} + + if port == 0 { + port = 53 + } + } + + // Address + addr := fmt.Sprintf("%s:%d", d.Address, port) + log.Debugf("Resolving TXT for %s using server %s\n", name, addr) + + // Send the request + var rtt time.Duration + in, rtt, err = c.Exchange(msg, addr) + if err != nil { + return + } + log.Debugf("Request took %d\n", rtt) + return +} + +// ExchangeDoHRequest sends a request using the DNS-over-HTTPS protocol +func (d *customDNS) ExchangeDoHRequest(msg *dns.Msg, name string) (in *dns.Msg, err error) { + // Get the port + port := d.Port + if port == 0 { + port = 443 + } + + // Initialize the HTTP client + tlsConf := &tls.Config{} + if len(d.DNSoverHTTPSHost) > 0 { + tlsConf.ServerName = d.DNSoverHTTPSHost + } + client := &http.Client{ + Timeout: reqTimeout, + Transport: &http2.Transport{ + DisableCompression: true, + TLSClientConfig: tlsConf, + }, + } + + // Serialize the message + var body []byte + body, err = msg.Pack() + if err != nil { + return + } + + // Create a POST request + var req *http.Request + req, err = http.NewRequest("POST", fmt.Sprintf("https://%s:%d/dns-query", d.Address, port), bytes.NewBuffer(body)) + if err != nil { + return + } + + // Set headers + if len(d.DNSoverHTTPSHost) > 0 { + req.Host = d.DNSoverHTTPSHost + } + req.Header.Set("Content-Type", "application/dns-message") + + var res *http.Response + res, err = client.Do(req) + if err != nil { + return + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("Invalid response status code: %d", res.StatusCode) + } + + var raw []byte + raw, err = ioutil.ReadAll(res.Body) + if err != nil { + return + } + if len(raw) < 1 { + err = fmt.Errorf("Response is empty") + return + } + + // Parse the response + in = &dns.Msg{} + err = in.Unpack(raw) + if err != nil { + in = nil + return + } + + return +} + +func (d *customDNS) cacheResult(name string, msg *dns.Msg) { + ttl := time.Duration(msg.Answer[0].Header().Ttl) * time.Second + if ttl > maxTTL { + ttl = maxTTL + } + cache.Set(name, msg, ttl) +} diff --git a/namesys/namesys.go b/namesys/namesys.go index cf944001d97..86e3cd4fd82 100644 --- a/namesys/namesys.go +++ b/namesys/namesys.go @@ -7,6 +7,7 @@ import ( lru "github.com/hashicorp/golang-lru" ds "github.com/ipfs/go-datastore" + config "github.com/ipfs/go-ipfs-config" path "github.com/ipfs/go-path" opts "github.com/ipfs/interface-go-ipfs-core/options/namesys" isd "github.com/jbenet/go-is-domain" @@ -33,19 +34,24 @@ type mpns struct { } // NewNameSystem will construct the IPFS naming system based on Routing -func NewNameSystem(r routing.ValueStore, ds ds.Datastore, cachesize int) NameSystem { +func NewNameSystem(r routing.ValueStore, ds ds.Datastore, cachesize int, cfg *config.Config) (NameSystem, error) { var cache *lru.Cache if cachesize > 0 { cache, _ = lru.New(cachesize) } + resolver, err := NewDNSResolver(cfg) + if err != nil { + return nil, err + } + return &mpns{ - dnsResolver: NewDNSResolver(), + dnsResolver: resolver, proquintResolver: new(ProquintResolver), ipnsResolver: NewIpnsResolver(r), ipnsPublisher: NewIpnsPublisher(r, ds), cache: cache, - } + }, nil } const DefaultResolverCacheTTL = time.Minute diff --git a/namesys/namesys_test.go b/namesys/namesys_test.go index d1ecf49e858..a6dab7d2f71 100644 --- a/namesys/namesys_test.go +++ b/namesys/namesys_test.go @@ -107,7 +107,10 @@ func TestPublishWithCache0(t *testing.T) { "pk": record.PublicKeyValidator{}, }) - nsys := NewNameSystem(routing, dst, 0) + nsys, err := NewNameSystem(routing, dst, 0) + if err != nil { + t.Fatal(err) + } p, err := path.ParsePath(unixfs.EmptyDirNode().Cid().String()) if err != nil { t.Fatal(err) diff --git a/namesys/republisher/repub_test.go b/namesys/republisher/repub_test.go index 5fedc3907a2..11fd595b6e3 100644 --- a/namesys/republisher/repub_test.go +++ b/namesys/republisher/repub_test.go @@ -37,7 +37,10 @@ func TestRepublish(t *testing.T) { t.Fatal(err) } - nd.Namesys = namesys.NewNameSystem(nd.Routing, nd.Repo.Datastore(), 0) + nd.Namesys, err = namesys.NewNameSystem(nd.Routing, nd.Repo.Datastore(), 0) + if err != nil { + t.Fatal(err) + } nodes = append(nodes, nd) }