From 501abdd1303a10c42ca38f7619e03c44938fe282 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rau=CC=81l=20Kripalani?= Date: Sat, 11 Aug 2018 13:29:57 +0100 Subject: [PATCH 1/5] WIP DNS resolution for API endpoint on ipfs bin (#5249) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit License: MIT Signed-off-by: Raúl Kripalani --- cmd/ipfs/main.go | 33 +++++++++++++++++++++++++++------ package.json | 6 ++++++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/cmd/ipfs/main.go b/cmd/ipfs/main.go index c39e94041b9..1f8b8263773 100644 --- a/cmd/ipfs/main.go +++ b/cmd/ipfs/main.go @@ -34,6 +34,7 @@ import ( osh "gx/ipfs/QmXuBJ7DR6k3rmUEKtvVMhwjmXDuJgXXPUt4LQXKBMsU93/go-os-helper" ma "gx/ipfs/QmYmsdtJ3HsodkePE3eU3TsCaP2YvPZJ4LoXnNkDE5Tpt7/go-multiaddr" loggables "gx/ipfs/QmZ4zF1mBrt8C2mSCM4ZYE4aAnv78f7GvrzufJC4G5tecK/go-libp2p-loggables" + mdns "gx/ipfs/QmfXU2MhWoegxHoeMd3A2ytL2P6CY4FfqGWc23LTNWBwZt/go-multiaddr-dns" ) // log is the command logger @@ -235,7 +236,7 @@ func commandShouldRunOnDaemon(details cmdDetails, req *cmds.Request, cctx *oldcm // did user specify an api to use for this command? apiAddrStr, _ := req.Options[corecmds.ApiOption].(string) - client, err := getApiClient(cctx.ConfigRoot, apiAddrStr) + client, err := getApiClient(req.Context, cctx.ConfigRoot, apiAddrStr) if err == repo.ErrApiNotRunning { if apiAddrStr != "" && req.Command != daemonCmd { // if user SPECIFIED an api, and this cmd is not daemon @@ -406,7 +407,7 @@ var checkIPFSWinFmt = "Otherwise check:\n\ttasklist | findstr ipfs" // getApiClient checks the repo, and the given options, checking for // a running API service. if there is one, it returns a client. // otherwise, it returns errApiNotRunning, or another error. -func getApiClient(repoPath, apiAddrStr string) (http.Client, error) { +func getApiClient(ctx context.Context, repoPath, apiAddrStr string) (http.Client, error) { var apiErrorFmt string switch { case osh.IsUnix(): @@ -440,14 +441,34 @@ func getApiClient(repoPath, apiAddrStr string) (http.Client, error) { if len(addr.Protocols()) == 0 { return nil, fmt.Errorf(apiErrorFmt, repoPath, "multiaddr doesn't provide any protocols") } - return apiClientForAddr(addr) + return apiClientForAddr(ctx, addr) } -func apiClientForAddr(addr ma.Multiaddr) (http.Client, error) { - _, host, err := manet.DialArgs(addr) +func apiClientForAddr(ctx context.Context, addr ma.Multiaddr) (http.Client, error) { + addrs, err := mdns.Resolve(ctx, addr) if err != nil { return nil, err } - return http.NewClient(host, http.ClientWithAPIPrefix(corehttp.APIPath)), nil + dialer := &manet.Dialer{} + for _, addr := range addrs { + ctx, cancelFunc := context.WithTimeout(ctx, 5*time.Second) + defer cancelFunc() + + conn, err := dialer.DialContext(ctx, addr) + if err != nil { + log.Errorf("connection to %s failed, error: %s", addr, err) + continue + } + conn.Close() + + _, host, err := manet.DialArgs(addr) + if err != nil { + continue + } + + return http.NewClient(host, http.ClientWithAPIPrefix(corehttp.APIPath)), nil + } + + return nil, errors.New("non-resolvable API endpoint") } diff --git a/package.json b/package.json index 3968c405dea..2d227c72662 100644 --- a/package.json +++ b/package.json @@ -539,6 +539,12 @@ "hash": "QmPyxJ2QS7L5FhGkNYkNcXHGjDhvGHueJ4auqAstFHYxy5", "name": "go-cidutil", "version": "0.0.2" + }, + { + "author": "lgierth", + "hash": "QmfXU2MhWoegxHoeMd3A2ytL2P6CY4FfqGWc23LTNWBwZt", + "name": "go-multiaddr-dns", + "version": "0.2.4" } ], "gxVersion": "0.10.0", From 0ddaf58336af04c9fe822262b2e2e000fecfd4ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rau=CC=81l=20Kripalani?= Date: Sat, 11 Aug 2018 23:26:16 +0100 Subject: [PATCH 2/5] main: make --api option resolve hostnames via dns (#5249) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves #5249. Calls multiaddr-dns, and picks the first result. Uses a fixed timeout of 10 seconds. Adds test cases for one, multiple, and no DNS results. License: MIT Signed-off-by: Raúl Kripalani --- cmd/ipfs/dnsresolve_test.go | 72 +++++++++++++++++++++++++++++++++++++ cmd/ipfs/main.go | 40 +++++++++++---------- 2 files changed, 94 insertions(+), 18 deletions(-) create mode 100644 cmd/ipfs/dnsresolve_test.go diff --git a/cmd/ipfs/dnsresolve_test.go b/cmd/ipfs/dnsresolve_test.go new file mode 100644 index 00000000000..9f43be12d4d --- /dev/null +++ b/cmd/ipfs/dnsresolve_test.go @@ -0,0 +1,72 @@ +package main + +import ( + "context" + "fmt" + "net" + "strings" + "testing" + + ma "gx/ipfs/QmYmsdtJ3HsodkePE3eU3TsCaP2YvPZJ4LoXnNkDE5Tpt7/go-multiaddr" + madns "gx/ipfs/QmfXU2MhWoegxHoeMd3A2ytL2P6CY4FfqGWc23LTNWBwZt/go-multiaddr-dns" +) + +var ( + ctx = context.Background() + testAddr, _ = ma.NewMultiaddr("/dns4/example.com/tcp/5001") +) + +func makeResolver(n uint8) *madns.Resolver { + results := make([]net.IPAddr, n) + for i := uint8(0); i < n; i++ { + results[i] = net.IPAddr{IP: net.ParseIP(fmt.Sprintf("192.0.2.%d", i))} + } + + backend := &madns.MockBackend{ + IP: map[string][]net.IPAddr{ + "example.com": results, + }} + + return &madns.Resolver{ + Backend: backend, + } +} + +func TestApiEndpointResolveDNSOneResult(t *testing.T) { + dnsResolver = makeResolver(1) + + addr, err := resolveAddr(ctx, testAddr) + if err != nil { + t.Error(err) + } + + if ref, _ := ma.NewMultiaddr("/ip4/192.0.2.0/tcp/5001"); !addr.Equal(ref) { + t.Errorf("resolved address was different than expected") + } +} + +func TestApiEndpointResolveDNSMultipleResults(t *testing.T) { + dnsResolver = makeResolver(4) + + addr, err := resolveAddr(ctx, testAddr) + if err != nil { + t.Error(err) + } + + if ref, _ := ma.NewMultiaddr("/ip4/192.0.2.0/tcp/5001"); !addr.Equal(ref) { + t.Errorf("resolved address was different than expected") + } +} + +func TestApiEndpointResolveDNSNoResults(t *testing.T) { + dnsResolver = makeResolver(0) + + addr, err := resolveAddr(ctx, testAddr) + if addr != nil || err == nil { + t.Error("expected test address not to resolve, and to throw an error") + } + + if !strings.HasPrefix(err.Error(), "non-resolvable API endpoint") { + t.Errorf("expected error not thrown; actual: %v", err) + } +} diff --git a/cmd/ipfs/main.go b/cmd/ipfs/main.go index 1f8b8263773..b0288931060 100644 --- a/cmd/ipfs/main.go +++ b/cmd/ipfs/main.go @@ -34,7 +34,7 @@ import ( osh "gx/ipfs/QmXuBJ7DR6k3rmUEKtvVMhwjmXDuJgXXPUt4LQXKBMsU93/go-os-helper" ma "gx/ipfs/QmYmsdtJ3HsodkePE3eU3TsCaP2YvPZJ4LoXnNkDE5Tpt7/go-multiaddr" loggables "gx/ipfs/QmZ4zF1mBrt8C2mSCM4ZYE4aAnv78f7GvrzufJC4G5tecK/go-libp2p-loggables" - mdns "gx/ipfs/QmfXU2MhWoegxHoeMd3A2ytL2P6CY4FfqGWc23LTNWBwZt/go-multiaddr-dns" + madns "gx/ipfs/QmfXU2MhWoegxHoeMd3A2ytL2P6CY4FfqGWc23LTNWBwZt/go-multiaddr-dns" ) // log is the command logger @@ -42,6 +42,9 @@ var log = logging.Logger("cmd/ipfs") var errRequestCanceled = errors.New("request canceled") +// declared as a var for testing purposes +var dnsResolver = madns.DefaultResolver + const ( EnvEnableProfiling = "IPFS_PROF" cpuProfile = "ipfs.cpuprof" @@ -445,30 +448,31 @@ func getApiClient(ctx context.Context, repoPath, apiAddrStr string) (http.Client } func apiClientForAddr(ctx context.Context, addr ma.Multiaddr) (http.Client, error) { - addrs, err := mdns.Resolve(ctx, addr) + addr, err := resolveAddr(ctx, addr) if err != nil { return nil, err } - dialer := &manet.Dialer{} - for _, addr := range addrs { - ctx, cancelFunc := context.WithTimeout(ctx, 5*time.Second) - defer cancelFunc() + _, host, err := manet.DialArgs(addr) + if err != nil { + return nil, err + } - conn, err := dialer.DialContext(ctx, addr) - if err != nil { - log.Errorf("connection to %s failed, error: %s", addr, err) - continue - } - conn.Close() + return http.NewClient(host, http.ClientWithAPIPrefix(corehttp.APIPath)), nil +} - _, host, err := manet.DialArgs(addr) - if err != nil { - continue - } +func resolveAddr(ctx context.Context, addr ma.Multiaddr) (ma.Multiaddr, error) { + ctx, cancelFunc := context.WithTimeout(ctx, 10*time.Second) + defer cancelFunc() + + addrs, err := dnsResolver.Resolve(ctx, addr) + if err != nil { + return nil, err + } - return http.NewClient(host, http.ClientWithAPIPrefix(corehttp.APIPath)), nil + if len(addrs) == 0 { + return nil, errors.New("non-resolvable API endpoint") } - return nil, errors.New("non-resolvable API endpoint") + return addrs[0], nil } From 5fe05e9a16a7ccf81317bacc95a0c7c6f102bb95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rau=CC=81l=20Kripalani?= Date: Sat, 11 Aug 2018 23:49:01 +0100 Subject: [PATCH 3/5] fix linting issue (getApiClient => getAPIClient). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit License: MIT Signed-off-by: Raúl Kripalani --- cmd/ipfs/main.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/ipfs/main.go b/cmd/ipfs/main.go index b0288931060..7ddfab45bf1 100644 --- a/cmd/ipfs/main.go +++ b/cmd/ipfs/main.go @@ -239,7 +239,7 @@ func commandShouldRunOnDaemon(details cmdDetails, req *cmds.Request, cctx *oldcm // did user specify an api to use for this command? apiAddrStr, _ := req.Options[corecmds.ApiOption].(string) - client, err := getApiClient(req.Context, cctx.ConfigRoot, apiAddrStr) + client, err := getAPIClient(req.Context, cctx.ConfigRoot, apiAddrStr) if err == repo.ErrApiNotRunning { if apiAddrStr != "" && req.Command != daemonCmd { // if user SPECIFIED an api, and this cmd is not daemon @@ -407,10 +407,10 @@ If you're sure go-ipfs isn't running, you can just delete it. var checkIPFSUnixFmt = "Otherwise check:\n\tps aux | grep ipfs" var checkIPFSWinFmt = "Otherwise check:\n\ttasklist | findstr ipfs" -// getApiClient checks the repo, and the given options, checking for +// getAPIClient checks the repo, and the given options, checking for // a running API service. if there is one, it returns a client. // otherwise, it returns errApiNotRunning, or another error. -func getApiClient(ctx context.Context, repoPath, apiAddrStr string) (http.Client, error) { +func getAPIClient(ctx context.Context, repoPath, apiAddrStr string) (http.Client, error) { var apiErrorFmt string switch { case osh.IsUnix(): From 9a579499b2c2a96893eb4354f9140ee0b0de080b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rau=CC=81l=20Kripalani?= Date: Fri, 17 Aug 2018 18:30:29 +0100 Subject: [PATCH 4/5] add sharness test for DNS resolution on API CLI flag. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit License: MIT Signed-off-by: Raúl Kripalani --- test/sharness/t0236-cli-api-dns-resolve.sh | 27 ++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100755 test/sharness/t0236-cli-api-dns-resolve.sh diff --git a/test/sharness/t0236-cli-api-dns-resolve.sh b/test/sharness/t0236-cli-api-dns-resolve.sh new file mode 100755 index 00000000000..ef217d12768 --- /dev/null +++ b/test/sharness/t0236-cli-api-dns-resolve.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2015 Jeromy Johnson +# MIT Licensed; see the LICENSE file in this repository. +# + +test_description="test dns resolution of api endpoint by cli" + +. lib/test-lib.sh + +test_init_ipfs + +# this test uses the localtest.me domain which resolves to 127.0.0.1 +# see http://readme.localtest.me/ +# in case if failure, check A record of that domain +test_expect_success "can make http request against dns resolved nc server" ' + nc -ld 5005 > nc_out & + NCPID=$! + go-sleep 0.5s && kill "$NCPID" & + ipfs cat /ipfs/Qmabcdef --api /dns4/localtest.me/tcp/5005 || true +' + +test_expect_success "request was received by local nc server" ' + grep "POST /api/v0/cat" nc_out +' + +test_done From c89102a01c6cc0c8fef0317062770e5075a5fc7e Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Thu, 23 Aug 2018 01:13:41 +0000 Subject: [PATCH 5/5] dns test: switch to localhost License: MIT Signed-off-by: Steven Allen --- test/sharness/t0236-cli-api-dns-resolve.sh | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/sharness/t0236-cli-api-dns-resolve.sh b/test/sharness/t0236-cli-api-dns-resolve.sh index ef217d12768..d53ce6a017c 100755 --- a/test/sharness/t0236-cli-api-dns-resolve.sh +++ b/test/sharness/t0236-cli-api-dns-resolve.sh @@ -10,14 +10,11 @@ test_description="test dns resolution of api endpoint by cli" test_init_ipfs -# this test uses the localtest.me domain which resolves to 127.0.0.1 -# see http://readme.localtest.me/ -# in case if failure, check A record of that domain test_expect_success "can make http request against dns resolved nc server" ' nc -ld 5005 > nc_out & NCPID=$! go-sleep 0.5s && kill "$NCPID" & - ipfs cat /ipfs/Qmabcdef --api /dns4/localtest.me/tcp/5005 || true + ipfs cat /ipfs/Qmabcdef --api /dns4/localhost/tcp/5005 || true ' test_expect_success "request was received by local nc server" '