From 3e5abf1e365380157e66a529c1b157d717021411 Mon Sep 17 00:00:00 2001 From: vyzo Date: Tue, 16 Oct 2018 15:14:34 +0300 Subject: [PATCH 1/8] service implementation --- README.md | 24 ++++++ ci/Jenkinsfile | 1 + proto.go | 24 ++++++ svc.go | 215 +++++++++++++++++++++++++++++++++++++++++++++++++ svc_test.go | 132 ++++++++++++++++++++++++++++++ 5 files changed, 396 insertions(+) create mode 100644 README.md create mode 100644 ci/Jenkinsfile create mode 100644 proto.go create mode 100644 svc.go create mode 100644 svc_test.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..58b8f14 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# go-libp2p-discovery + +[![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://ipn.io) +[![](https://img.shields.io/badge/project-libp2p-blue.svg?style=flat-square)](http://libp2p.io/) +[![](https://img.shields.io/badge/freenode-%23ipfs-blue.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23ipfs) +[![standard-readme compliant](https://img.shields.io/badge/standard--readme-OK-green.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme) + +> AutoNAT service implementation + +This package provides an implementation of the AutoNATService; see [autonat](https://github.com/libp2p/go-libp2p-autonat). + +## Documenation + +See https://godoc.org/github.com/libp2p/go-libp2p-autonat-svc. + +## Contribute + +Feel free to join in. All welcome. Open an [issue](https://github.com/libp2p/go-libp2p-discovery/issues)! + +This repository falls under the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). + +## License + +MIT diff --git a/ci/Jenkinsfile b/ci/Jenkinsfile new file mode 100644 index 0000000..b2067e6 --- /dev/null +++ b/ci/Jenkinsfile @@ -0,0 +1 @@ +golang() diff --git a/proto.go b/proto.go new file mode 100644 index 0000000..c0bd44a --- /dev/null +++ b/proto.go @@ -0,0 +1,24 @@ +package autonat + +import ( + pb "github.com/libp2p/go-libp2p-autonat/pb" + + logging "github.com/ipfs/go-log" + ma "github.com/multiformats/go-multiaddr" +) + +var log = logging.Logger("autonat-svc") + +func newDialResponseOK(addr ma.Multiaddr) *pb.Message_DialResponse { + dr := new(pb.Message_DialResponse) + dr.Status = pb.Message_OK.Enum() + dr.Addr = addr.Bytes() + return dr +} + +func newDialResponseError(status pb.Message_ResponseStatus, text string) *pb.Message_DialResponse { + dr := new(pb.Message_DialResponse) + dr.Status = status.Enum() + dr.StatusText = &text + return dr +} diff --git a/svc.go b/svc.go new file mode 100644 index 0000000..e523af6 --- /dev/null +++ b/svc.go @@ -0,0 +1,215 @@ +package autonat + +import ( + "context" + "sync" + "time" + + pb "github.com/libp2p/go-libp2p-autonat/pb" + + ggio "github.com/gogo/protobuf/io" + libp2p "github.com/libp2p/go-libp2p" + autonat "github.com/libp2p/go-libp2p-autonat" + host "github.com/libp2p/go-libp2p-host" + inet "github.com/libp2p/go-libp2p-net" + peer "github.com/libp2p/go-libp2p-peer" + pstore "github.com/libp2p/go-libp2p-peerstore" + ma "github.com/multiformats/go-multiaddr" +) + +const P_CIRCUIT = 290 + +var ( + AutoNATServiceDialTimeout = 42 * time.Second + AutoNATServiceResetInterval = 1 * time.Minute + + AutoNATServiceThrottle = 3 +) + +// AutoNATService provides NAT autodetection services to other peers +type AutoNATService struct { + ctx context.Context + dialer host.Host + + // rate limiter + mx sync.Mutex + reqs map[peer.ID]int +} + +// NewAutoNATService creates a new AutoNATService instance attached to a host +func NewAutoNATService(ctx context.Context, h host.Host, opts ...libp2p.Option) (*AutoNATService, error) { + opts = append(opts, libp2p.NoListenAddrs) + dialer, err := libp2p.New(ctx, opts...) + if err != nil { + return nil, err + } + + as := &AutoNATService{ + ctx: ctx, + dialer: dialer, + reqs: make(map[peer.ID]int), + } + h.SetStreamHandler(autonat.AutoNATProto, as.handleStream) + + go as.resetRateLimiter() + + return as, nil +} + +func (as *AutoNATService) handleStream(s inet.Stream) { + defer s.Close() + + pid := s.Conn().RemotePeer() + log.Debugf("New stream from %s", pid.Pretty()) + + r := ggio.NewDelimitedReader(s, inet.MessageSizeMax) + w := ggio.NewDelimitedWriter(s) + + var req pb.Message + var res pb.Message + + err := r.ReadMsg(&req) + if err != nil { + log.Debugf("Error reading message from %s: %s", pid.Pretty(), err.Error()) + s.Reset() + return + } + + t := req.GetType() + if t != pb.Message_DIAL { + log.Debugf("Unexpected message from %s: %s (%d)", pid.Pretty(), t.String(), t) + s.Reset() + return + } + + dr := as.handleDial(pid, s.Conn().RemoteMultiaddr(), req.GetDial().GetPeer()) + res.Type = pb.Message_DIAL_RESPONSE.Enum() + res.DialResponse = dr + + err = w.WriteMsg(&res) + if err != nil { + log.Debugf("Error writing response to %s: %s", pid.Pretty(), err.Error()) + s.Reset() + return + } +} + +func (as *AutoNATService) handleDial(p peer.ID, obsaddr ma.Multiaddr, mpi *pb.Message_PeerInfo) *pb.Message_DialResponse { + if mpi == nil { + return newDialResponseError(pb.Message_E_BAD_REQUEST, "missing peer info") + } + + mpid := mpi.GetId() + if mpid != nil { + mp, err := peer.IDFromBytes(mpid) + if err != nil { + return newDialResponseError(pb.Message_E_BAD_REQUEST, "bad peer id") + } + + if mp != p { + return newDialResponseError(pb.Message_E_BAD_REQUEST, "peer id mismatch") + } + } + + addrs := make([]ma.Multiaddr, 0) + seen := make(map[string]struct{}) + + // add observed addr to the list of addresses to dial + if !as.skipDial(obsaddr) { + addrs = append(addrs, obsaddr) + seen[obsaddr.String()] = struct{}{} + } + + for _, maddr := range mpi.GetAddrs() { + addr, err := ma.NewMultiaddrBytes(maddr) + if err != nil { + log.Debugf("Error parsing multiaddr: %s", err.Error()) + continue + } + + if as.skipDial(addr) { + continue + } + + str := addr.String() + _, ok := seen[str] + if ok { + continue + } + + addrs = append(addrs, addr) + seen[str] = struct{}{} + } + + if len(addrs) == 0 { + return newDialResponseError(pb.Message_E_DIAL_ERROR, "no dialable addresses") + } + + return as.doDial(pstore.PeerInfo{ID: p, Addrs: addrs}) +} + +func (as *AutoNATService) skipDial(addr ma.Multiaddr) bool { + // skip relay addresses + _, err := addr.ValueForProtocol(P_CIRCUIT) + if err == nil { + return true + } + + // skip private network (unroutable) addresses + if !autonat.IsPublicAddr(addr) { + return true + } + + return false +} + +func (as *AutoNATService) doDial(pi pstore.PeerInfo) *pb.Message_DialResponse { + // rate limit check + as.mx.Lock() + count := as.reqs[pi.ID] + if count >= AutoNATServiceThrottle { + as.mx.Unlock() + return newDialResponseError(pb.Message_E_DIAL_REFUSED, "too many dials") + } + as.reqs[pi.ID] = count + 1 + as.mx.Unlock() + + ctx, cancel := context.WithTimeout(as.ctx, AutoNATServiceDialTimeout) + defer cancel() + + err := as.dialer.Connect(ctx, pi) + if err != nil { + log.Debugf("error dialing %s: %s", pi.ID.Pretty(), err.Error()) + // wait for the context to timeout to avoid leaking timing information + // this renders the service ineffective as a port scanner + <-ctx.Done() + return newDialResponseError(pb.Message_E_DIAL_ERROR, "dial failed") + } + + conns := as.dialer.Network().ConnsToPeer(pi.ID) + if len(conns) == 0 { + log.Errorf("supposedly connected to %s, but no connection to peer", pi.ID.Pretty()) + return newDialResponseError(pb.Message_E_INTERNAL_ERROR, "internal service error") + } + + ra := conns[0].RemoteMultiaddr() + as.dialer.Network().ClosePeer(pi.ID) + return newDialResponseOK(ra) +} + +func (as *AutoNATService) resetRateLimiter() { + ticker := time.NewTicker(AutoNATServiceResetInterval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + as.mx.Lock() + as.reqs = make(map[peer.ID]int) + as.mx.Unlock() + + case <-as.ctx.Done(): + return + } + } +} diff --git a/svc_test.go b/svc_test.go new file mode 100644 index 0000000..65b2a9d --- /dev/null +++ b/svc_test.go @@ -0,0 +1,132 @@ +package autonat + +import ( + "context" + "net" + "testing" + "time" + + libp2p "github.com/libp2p/go-libp2p" + autonat "github.com/libp2p/go-libp2p-autonat" + host "github.com/libp2p/go-libp2p-host" + pstore "github.com/libp2p/go-libp2p-peerstore" +) + +func makeAutoNATService(ctx context.Context, t *testing.T) (host.Host, *AutoNATService) { + h, err := libp2p.New(ctx, libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0")) + if err != nil { + t.Fatal(err) + } + + as, err := NewAutoNATService(ctx, h) + if err != nil { + t.Fatal(err) + } + + return h, as +} + +func makeAutoNATClient(ctx context.Context, t *testing.T) (host.Host, autonat.AutoNATClient) { + h, err := libp2p.New(ctx, libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0")) + if err != nil { + t.Fatal(err) + } + + cli := autonat.NewAutoNATClient(h) + return h, cli +} + +func connect(t *testing.T, a, b host.Host) { + pinfo := pstore.PeerInfo{ID: a.ID(), Addrs: a.Addrs()} + err := b.Connect(context.Background(), pinfo) + if err != nil { + t.Fatal(err) + } +} + +// Note: these tests assume that the host has only private inet addresses! +func TestAutoNATServiceDialError(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + save := AutoNATServiceDialTimeout + AutoNATServiceDialTimeout = 1 * time.Second + + hs, _ := makeAutoNATService(ctx, t) + hc, ac := makeAutoNATClient(ctx, t) + connect(t, hs, hc) + + _, err := ac.DialBack(ctx, hs.ID()) + if err == nil { + t.Fatal("Dial back succeeded unexpectedly!") + } + + if !autonat.IsDialError(err) { + t.Fatal(err) + } + + AutoNATServiceDialTimeout = save +} + +func TestAutoNATServiceDialSuccess(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + save := autonat.Private4 + autonat.Private4 = []*net.IPNet{} + + hs, _ := makeAutoNATService(ctx, t) + hc, ac := makeAutoNATClient(ctx, t) + connect(t, hs, hc) + + _, err := ac.DialBack(ctx, hs.ID()) + if err != nil { + t.Fatalf("Dial back failed: %s", err.Error()) + } + + autonat.Private4 = save +} + +func TestAutoNATServiceDialRateLimiter(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + save1 := AutoNATServiceDialTimeout + AutoNATServiceDialTimeout = 1 * time.Second + save2 := AutoNATServiceResetInterval + AutoNATServiceResetInterval = 1 * time.Second + save3 := AutoNATServiceThrottle + AutoNATServiceThrottle = 1 + save4 := autonat.Private4 + autonat.Private4 = []*net.IPNet{} + + hs, _ := makeAutoNATService(ctx, t) + hc, ac := makeAutoNATClient(ctx, t) + connect(t, hs, hc) + + _, err := ac.DialBack(ctx, hs.ID()) + if err != nil { + t.Fatal(err) + } + + _, err = ac.DialBack(ctx, hs.ID()) + if err == nil { + t.Fatal("Dial back succeeded unexpectedly!") + } + + if !autonat.IsDialRefused(err) { + t.Fatal(err) + } + + time.Sleep(2 * time.Second) + + _, err = ac.DialBack(ctx, hs.ID()) + if err != nil { + t.Fatal(err) + } + + AutoNATServiceDialTimeout = save1 + AutoNATServiceResetInterval = save2 + AutoNATServiceThrottle = save3 + autonat.Private4 = save4 +} From 136d3caeec346d477489de9a17025f202664bb07 Mon Sep 17 00:00:00 2001 From: vyzo Date: Tue, 16 Oct 2018 16:48:46 +0300 Subject: [PATCH 2/8] IsPublicAddr lives in multiaddr-net --- svc.go | 3 ++- svc_test.go | 13 +++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/svc.go b/svc.go index e523af6..b874b7d 100644 --- a/svc.go +++ b/svc.go @@ -15,6 +15,7 @@ import ( peer "github.com/libp2p/go-libp2p-peer" pstore "github.com/libp2p/go-libp2p-peerstore" ma "github.com/multiformats/go-multiaddr" + manet "github.com/multiformats/go-multiaddr-net" ) const P_CIRCUIT = 290 @@ -156,7 +157,7 @@ func (as *AutoNATService) skipDial(addr ma.Multiaddr) bool { } // skip private network (unroutable) addresses - if !autonat.IsPublicAddr(addr) { + if !manet.IsPublicAddr(addr) { return true } diff --git a/svc_test.go b/svc_test.go index 65b2a9d..9c8d969 100644 --- a/svc_test.go +++ b/svc_test.go @@ -10,6 +10,7 @@ import ( autonat "github.com/libp2p/go-libp2p-autonat" host "github.com/libp2p/go-libp2p-host" pstore "github.com/libp2p/go-libp2p-peerstore" + manet "github.com/multiformats/go-multiaddr-net" ) func makeAutoNATService(ctx context.Context, t *testing.T) (host.Host, *AutoNATService) { @@ -72,8 +73,8 @@ func TestAutoNATServiceDialSuccess(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - save := autonat.Private4 - autonat.Private4 = []*net.IPNet{} + save := manet.Private4 + manet.Private4 = []*net.IPNet{} hs, _ := makeAutoNATService(ctx, t) hc, ac := makeAutoNATClient(ctx, t) @@ -84,7 +85,7 @@ func TestAutoNATServiceDialSuccess(t *testing.T) { t.Fatalf("Dial back failed: %s", err.Error()) } - autonat.Private4 = save + manet.Private4 = save } func TestAutoNATServiceDialRateLimiter(t *testing.T) { @@ -97,8 +98,8 @@ func TestAutoNATServiceDialRateLimiter(t *testing.T) { AutoNATServiceResetInterval = 1 * time.Second save3 := AutoNATServiceThrottle AutoNATServiceThrottle = 1 - save4 := autonat.Private4 - autonat.Private4 = []*net.IPNet{} + save4 := manet.Private4 + manet.Private4 = []*net.IPNet{} hs, _ := makeAutoNATService(ctx, t) hc, ac := makeAutoNATClient(ctx, t) @@ -128,5 +129,5 @@ func TestAutoNATServiceDialRateLimiter(t *testing.T) { AutoNATServiceDialTimeout = save1 AutoNATServiceResetInterval = save2 AutoNATServiceThrottle = save3 - autonat.Private4 = save4 + manet.Private4 = save4 } From 61b34a1f716d355af8ce4640b7dc01a32f0c143f Mon Sep 17 00:00:00 2001 From: vyzo Date: Thu, 18 Oct 2018 18:47:20 +0300 Subject: [PATCH 3/8] fix readme bugs --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 58b8f14..6ba5912 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# go-libp2p-discovery +# go-libp2p-autonat-svc [![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://ipn.io) [![](https://img.shields.io/badge/project-libp2p-blue.svg?style=flat-square)](http://libp2p.io/) @@ -9,7 +9,7 @@ This package provides an implementation of the AutoNATService; see [autonat](https://github.com/libp2p/go-libp2p-autonat). -## Documenation +## Documentation See https://godoc.org/github.com/libp2p/go-libp2p-autonat-svc. From 98136870ea70c2517aef86668c536112e1b5ae02 Mon Sep 17 00:00:00 2001 From: vyzo Date: Thu, 18 Oct 2018 18:48:40 +0300 Subject: [PATCH 4/8] gx package --- .gx/lastpubver | 1 + package.json | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 .gx/lastpubver create mode 100644 package.json diff --git a/.gx/lastpubver b/.gx/lastpubver new file mode 100644 index 0000000..57dcde8 --- /dev/null +++ b/.gx/lastpubver @@ -0,0 +1 @@ +1.0.0: QmYeCCWPmNuMC9BwBKVQoR3ABze369kbHXfRt4WvYtEnj8 diff --git a/package.json b/package.json new file mode 100644 index 0000000..6b348a6 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "author": "vyzo", + "bugs": {}, + "gx": { + "dvcsimport": "github.com/libp2p/go-libp2p-autonat-svc" + }, + "gxDependencies": [ + { + "author": "whyrusleeping", + "hash": "QmPL3AKtiaQyYpchZceXBZhZ3MSnoGqJvLZrc7fzDTTQdJ", + "name": "go-libp2p", + "version": "6.0.19" + } + ], + "gxVersion": "0.12.1", + "language": "go", + "license": "", + "name": "go-libp2p-autonat-svc", + "releaseCmd": "git commit -a -m \"gx publish $VERSION\"", + "version": "1.0.0" +} From a87b01c8341707e5e606e30083c491e5775796c9 Mon Sep 17 00:00:00 2001 From: vyzo Date: Thu, 18 Oct 2018 19:01:32 +0300 Subject: [PATCH 5/8] gx import go-libp2p-autonat --- package.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/package.json b/package.json index 6b348a6..9292464 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,12 @@ "hash": "QmPL3AKtiaQyYpchZceXBZhZ3MSnoGqJvLZrc7fzDTTQdJ", "name": "go-libp2p", "version": "6.0.19" + }, + { + "author": "vyzo", + "hash": "QmNtEW59XTrreF9U4NAjYUmixEJ56HnfiJCj1p97bFnah2", + "name": "go-libp2p-autonat", + "version": "1.0.0" } ], "gxVersion": "0.12.1", @@ -19,3 +25,4 @@ "releaseCmd": "git commit -a -m \"gx publish $VERSION\"", "version": "1.0.0" } + From 3b3a6d0f1e8f647199d889156ad7cbec19240d86 Mon Sep 17 00:00:00 2001 From: vyzo Date: Thu, 25 Oct 2018 11:56:27 +0300 Subject: [PATCH 6/8] update test for AutoNATClient constructor change --- svc_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/svc_test.go b/svc_test.go index 9c8d969..c0a676d 100644 --- a/svc_test.go +++ b/svc_test.go @@ -33,7 +33,7 @@ func makeAutoNATClient(ctx context.Context, t *testing.T) (host.Host, autonat.Au t.Fatal(err) } - cli := autonat.NewAutoNATClient(h) + cli := autonat.NewAutoNATClient(h, nil) return h, cli } From 8c74386e6f7a77a8b2add786beb154b333fdf376 Mon Sep 17 00:00:00 2001 From: vyzo Date: Thu, 25 Oct 2018 11:56:37 +0300 Subject: [PATCH 7/8] gx update --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 9292464..ab9f048 100644 --- a/package.json +++ b/package.json @@ -7,15 +7,15 @@ "gxDependencies": [ { "author": "whyrusleeping", - "hash": "QmPL3AKtiaQyYpchZceXBZhZ3MSnoGqJvLZrc7fzDTTQdJ", + "hash": "QmUDTcnDp2WssbmiDLC6aYurUeyt7QeRakHUQMxA2mZ5iB", "name": "go-libp2p", - "version": "6.0.19" + "version": "6.0.23" }, { "author": "vyzo", - "hash": "QmNtEW59XTrreF9U4NAjYUmixEJ56HnfiJCj1p97bFnah2", + "hash": "QmUn8mtaf4tTFwKnFRzkNYYLc8XEo3yz6qBfp5ShVB1HYZ", "name": "go-libp2p-autonat", - "version": "1.0.0" + "version": "1.0.1" } ], "gxVersion": "0.12.1", From 9553d460944df929d8b1c8d6cbd8cbc7a948f1b7 Mon Sep 17 00:00:00 2001 From: vyzo Date: Thu, 25 Oct 2018 11:57:52 +0300 Subject: [PATCH 8/8] gx publish 1.0.0 --- .gx/lastpubver | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gx/lastpubver b/.gx/lastpubver index 57dcde8..4cf0390 100644 --- a/.gx/lastpubver +++ b/.gx/lastpubver @@ -1 +1 @@ -1.0.0: QmYeCCWPmNuMC9BwBKVQoR3ABze369kbHXfRt4WvYtEnj8 +1.0.0: QmeYNWTPm2TdZfSepXp1Su22UXgbsPPN1vTXB8H3nFJVMD