From b938ab9ab685688e379e9bfbc8c714f3cf5d450f Mon Sep 17 00:00:00 2001 From: Jeromy Date: Sat, 9 Apr 2016 22:40:40 -0700 Subject: [PATCH 1/5] Handle incoming conns in their own goroutines Doing the multistream negotiation in sync causes hanging issues. This commit accepts transport connections and starts the negotiation in a separate goroutine, sending it down a channel when its ready. --- p2p/net/conn/dial_test.go | 52 +++++++++++++++ p2p/net/conn/listen.go | 134 +++++++++++++++++++++++--------------- 2 files changed, 134 insertions(+), 52 deletions(-) diff --git a/p2p/net/conn/dial_test.go b/p2p/net/conn/dial_test.go index 2179cbd3..23a59140 100644 --- a/p2p/net/conn/dial_test.go +++ b/p2p/net/conn/dial_test.go @@ -397,3 +397,55 @@ func TestFailedAccept(t *testing.T) { c.Close() <-done } + +func TestHangingAccept(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + p1 := tu.RandPeerNetParamsOrFatal(t) + + l1, err := Listen(ctx, p1.Addr, p1.ID, p1.PrivKey) + if err != nil { + t.Fatal(err) + } + + p1.Addr = l1.Multiaddr() // Addr has been determined by kernel. + + done := make(chan struct{}) + go func() { + defer close(done) + con, err := net.Dial("tcp", l1.Addr().String()) + if err != nil { + t.Error("first dial failed: ", err) + } + // hang this connection + defer con.Close() + + // ensure that the first conn hits first + time.Sleep(time.Millisecond * 50) + + con2, err := net.Dial("tcp", l1.Addr().String()) + if err != nil { + t.Error("second dial failed: ", err) + } + defer con2.Close() + + err = msmux.SelectProtoOrFail(SecioTag, con2) + if err != nil { + t.Error("msmux select failed: ", err) + } + + _, err = con2.Write([]byte("test")) + if err != nil { + t.Error("con write failed: ", err) + } + }() + + c, err := l1.Accept() + if err != nil { + t.Fatal("connections after a failed accept should still work: ", err) + } + + c.Close() + <-done +} diff --git a/p2p/net/conn/listen.go b/p2p/net/conn/listen.go index 3d81cbaf..51a6dde0 100644 --- a/p2p/net/conn/listen.go +++ b/p2p/net/conn/listen.go @@ -4,6 +4,8 @@ import ( "fmt" "io" "net" + "sync" + "time" ic "github.com/ipfs/go-libp2p/p2p/crypto" filter "github.com/ipfs/go-libp2p/p2p/net/filter" @@ -20,6 +22,26 @@ import ( const SecioTag = "/secio/1.0.0" const NoEncryptionTag = "/plaintext/1.0.0" +const connAcceptBuffer = 32 +const NegotiateReadTimeout = time.Second * 20 + +var catcher = tec.TempErrCatcher{ + IsTemp: func(e error) bool { + // ignore connection breakages up to this point. but log them + if e == io.EOF { + log.Debugf("listener ignoring conn with EOF: %s", e) + return true + } + + te, ok := e.(tec.Temporary) + if ok { + log.Debugf("listener ignoring conn with temporary err: %s", e) + return te.Temporary() + } + return false + }, +} + // ConnWrapper is any function that wraps a raw multiaddr connection type ConnWrapper func(transport.Conn) transport.Conn @@ -37,6 +59,10 @@ type listener struct { proc goprocess.Process mux *msmux.MultistreamMuxer + + incoming chan transport.Conn + + ctx context.Context } func (l *listener) teardown() error { @@ -60,57 +86,8 @@ func (l *listener) SetAddrFilters(fs *filter.Filters) { // Accept waits for and returns the next connection to the listener. // Note that unfortunately this func (l *listener) Accept() (net.Conn, error) { - - // listeners dont have contexts. given changes dont make sense here anymore - // note that the parent of listener will Close, which will interrupt all io. - // Contexts and io don't mix. - ctx := context.Background() - - var catcher tec.TempErrCatcher - - catcher.IsTemp = func(e error) bool { - // ignore connection breakages up to this point. but log them - if e == io.EOF { - log.Debugf("listener ignoring conn with EOF: %s", e) - return true - } - - te, ok := e.(tec.Temporary) - if ok { - log.Debugf("listener ignoring conn with temporary err: %s", e) - return te.Temporary() - } - return false - } - - for { - maconn, err := l.Listener.Accept() - if err != nil { - if catcher.IsTemporary(err) { - continue - } - return nil, err - } - - log.Debugf("listener %s got connection: %s <---> %s", l, maconn.LocalMultiaddr(), maconn.RemoteMultiaddr()) - - if l.filters != nil && l.filters.AddrBlocked(maconn.RemoteMultiaddr()) { - log.Debugf("blocked connection from %s", maconn.RemoteMultiaddr()) - maconn.Close() - continue - } - // If we have a wrapper func, wrap this conn - if l.wrapper != nil { - maconn = l.wrapper(maconn) - } - - _, _, err = l.mux.Negotiate(maconn) - if err != nil { - log.Info("negotiation of crypto protocol failed: ", err) - continue - } - - c, err := newSingleConn(ctx, l.local, "", maconn) + for con := range l.incoming { + c, err := newSingleConn(l.ctx, l.local, "", con) if err != nil { if catcher.IsTemporary(err) { continue @@ -122,13 +99,14 @@ func (l *listener) Accept() (net.Conn, error) { log.Warning("listener %s listening INSECURELY!", l) return c, nil } - sc, err := newSecureConn(ctx, l.privk, c) + sc, err := newSecureConn(l.ctx, l.privk, c) if err != nil { log.Infof("ignoring conn we failed to secure: %s %s", err, c) continue } return sc, nil } + return nil, fmt.Errorf("listener is closed") } func (l *listener) Addr() net.Addr { @@ -157,12 +135,62 @@ func (l *listener) Loggable() map[string]interface{} { } } +func (l *listener) handleIncoming() { + var wg sync.WaitGroup + defer func() { + wg.Wait() + close(l.incoming) + }() + + for { + maconn, err := l.Listener.Accept() + if err != nil { + if catcher.IsTemporary(err) { + continue + } + log.Warningf("listener errored and will close: %s", err) + return + } + + log.Debugf("listener %s got connection: %s <---> %s", l, maconn.LocalMultiaddr(), maconn.RemoteMultiaddr()) + + if l.filters != nil && l.filters.AddrBlocked(maconn.RemoteMultiaddr()) { + log.Debugf("blocked connection from %s", maconn.RemoteMultiaddr()) + maconn.Close() + continue + } + // If we have a wrapper func, wrap this conn + if l.wrapper != nil { + maconn = l.wrapper(maconn) + } + + wg.Add(1) + go func() { + defer wg.Done() + maconn.SetReadDeadline(time.Now().Add(NegotiateReadTimeout)) + _, _, err = l.mux.Negotiate(maconn) + if err != nil { + log.Info("negotiation of crypto protocol failed: ", err) + maconn.Close() + return + } + + // clear read readline + maconn.SetReadDeadline(time.Time{}) + + l.incoming <- maconn + }() + } +} + func WrapTransportListener(ctx context.Context, ml transport.Listener, local peer.ID, sk ic.PrivKey) (Listener, error) { l := &listener{ Listener: ml, local: local, privk: sk, mux: msmux.NewMultistreamMuxer(), + incoming: make(chan transport.Conn, connAcceptBuffer), + ctx: ctx, } l.proc = goprocessctx.WithContextAndTeardown(ctx, l.teardown) @@ -172,6 +200,8 @@ func WrapTransportListener(ctx context.Context, ml transport.Listener, local pee l.mux.AddHandler(NoEncryptionTag, nil) } + go l.handleIncoming() + log.Debugf("Conn Listener on %s", l.Multiaddr()) log.Event(ctx, "swarmListen", l) return l, nil From 9d44390084f63d81b22cd8e2b9aaa5d4970f6236 Mon Sep 17 00:00:00 2001 From: Jeromy Date: Sun, 10 Apr 2016 10:21:25 -0700 Subject: [PATCH 2/5] address feedback from CR --- p2p/net/conn/listen.go | 63 +++++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/p2p/net/conn/listen.go b/p2p/net/conn/listen.go index 51a6dde0..20d7ffd0 100644 --- a/p2p/net/conn/listen.go +++ b/p2p/net/conn/listen.go @@ -25,23 +25,6 @@ const NoEncryptionTag = "/plaintext/1.0.0" const connAcceptBuffer = 32 const NegotiateReadTimeout = time.Second * 20 -var catcher = tec.TempErrCatcher{ - IsTemp: func(e error) bool { - // ignore connection breakages up to this point. but log them - if e == io.EOF { - log.Debugf("listener ignoring conn with EOF: %s", e) - return true - } - - te, ok := e.(tec.Temporary) - if ok { - log.Debugf("listener ignoring conn with temporary err: %s", e) - return te.Temporary() - } - return false - }, -} - // ConnWrapper is any function that wraps a raw multiaddr connection type ConnWrapper func(transport.Conn) transport.Conn @@ -55,12 +38,13 @@ type listener struct { filters *filter.Filters wrapper ConnWrapper + catcher tec.TempErrCatcher proc goprocess.Process mux *msmux.MultistreamMuxer - incoming chan transport.Conn + incoming chan connErr ctx context.Context } @@ -83,13 +67,23 @@ func (l *listener) SetAddrFilters(fs *filter.Filters) { l.filters = fs } +type connErr struct { + conn transport.Conn + err error +} + // Accept waits for and returns the next connection to the listener. // Note that unfortunately this func (l *listener) Accept() (net.Conn, error) { for con := range l.incoming { - c, err := newSingleConn(l.ctx, l.local, "", con) + if con.err != nil { + return nil, con.err + } + + c, err := newSingleConn(l.ctx, l.local, "", con.conn) if err != nil { - if catcher.IsTemporary(err) { + con.conn.Close() + if l.catcher.IsTemporary(err) { continue } return nil, err @@ -101,6 +95,7 @@ func (l *listener) Accept() (net.Conn, error) { } sc, err := newSecureConn(l.ctx, l.privk, c) if err != nil { + con.conn.Close() log.Infof("ignoring conn we failed to secure: %s %s", err, c) continue } @@ -142,13 +137,17 @@ func (l *listener) handleIncoming() { close(l.incoming) }() + wg.Add(1) + defer wg.Done() + for { maconn, err := l.Listener.Accept() if err != nil { - if catcher.IsTemporary(err) { + if l.catcher.IsTemporary(err) { continue } - log.Warningf("listener errored and will close: %s", err) + + l.incoming <- connErr{err: err} return } @@ -170,7 +169,7 @@ func (l *listener) handleIncoming() { maconn.SetReadDeadline(time.Now().Add(NegotiateReadTimeout)) _, _, err = l.mux.Negotiate(maconn) if err != nil { - log.Info("negotiation of crypto protocol failed: ", err) + log.Info("incoming conn: negotiation of crypto protocol failed: ", err) maconn.Close() return } @@ -178,7 +177,7 @@ func (l *listener) handleIncoming() { // clear read readline maconn.SetReadDeadline(time.Time{}) - l.incoming <- maconn + l.incoming <- connErr{conn: maconn} }() } } @@ -189,10 +188,24 @@ func WrapTransportListener(ctx context.Context, ml transport.Listener, local pee local: local, privk: sk, mux: msmux.NewMultistreamMuxer(), - incoming: make(chan transport.Conn, connAcceptBuffer), + incoming: make(chan connErr, connAcceptBuffer), ctx: ctx, } l.proc = goprocessctx.WithContextAndTeardown(ctx, l.teardown) + l.catcher.IsTemp = func(e error) bool { + // ignore connection breakages up to this point. but log them + if e == io.EOF { + log.Debugf("listener ignoring conn with EOF: %s", e) + return true + } + + te, ok := e.(tec.Temporary) + if ok { + log.Debugf("listener ignoring conn with temporary err: %s", e) + return te.Temporary() + } + return false + } if EncryptConnections { l.mux.AddHandler(SecioTag, nil) From d3a90434f1801bf24f08bee52b910f8d1190f93f Mon Sep 17 00:00:00 2001 From: Jeromy Date: Sun, 10 Apr 2016 11:03:53 -0700 Subject: [PATCH 3/5] add test to show concurrency of dials --- p2p/net/conn/dial_test.go | 57 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/p2p/net/conn/dial_test.go b/p2p/net/conn/dial_test.go index 23a59140..e97020c2 100644 --- a/p2p/net/conn/dial_test.go +++ b/p2p/net/conn/dial_test.go @@ -6,6 +6,7 @@ import ( "io" "net" "strings" + "sync" "testing" "time" @@ -449,3 +450,59 @@ func TestHangingAccept(t *testing.T) { c.Close() <-done } + +func TestConcurrentAccept(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + p1 := tu.RandPeerNetParamsOrFatal(t) + + l1, err := Listen(ctx, p1.Addr, p1.ID, p1.PrivKey) + if err != nil { + t.Fatal(err) + } + + n := 300 + delay := time.Millisecond * 20 + + p1.Addr = l1.Multiaddr() // Addr has been determined by kernel. + + var wg sync.WaitGroup + for i := 0; i < n; i++ { + wg.Add(1) + go func() { + defer wg.Done() + con, err := net.Dial("tcp", l1.Addr().String()) + if err != nil { + log.Error(err) + t.Error("first dial failed: ", err) + return + } + // hang this connection + defer con.Close() + + time.Sleep(delay) + err = msmux.SelectProtoOrFail(SecioTag, con) + if err != nil { + t.Error(err) + } + }() + } + + before := time.Now() + for i := 0; i < n; i++ { + c, err := l1.Accept() + if err != nil { + t.Fatal("connections after a failed accept should still work: ", err) + } + + c.Close() + } + + limit := delay * time.Duration(n) + took := time.Now().Sub(before) + if took > limit { + t.Fatal("took too long!") + } + log.Errorf("took: %s (less than %s)", took, limit) +} From e123a79f92877f8365a4e098f2a5136df34fb0ab Mon Sep 17 00:00:00 2001 From: Jeromy Date: Sun, 10 Apr 2016 11:32:17 -0700 Subject: [PATCH 4/5] make test smaller for osx --- p2p/net/conn/dial_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/p2p/net/conn/dial_test.go b/p2p/net/conn/dial_test.go index e97020c2..8700d540 100644 --- a/p2p/net/conn/dial_test.go +++ b/p2p/net/conn/dial_test.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net" + "runtime" "strings" "sync" "testing" @@ -464,6 +465,9 @@ func TestConcurrentAccept(t *testing.T) { n := 300 delay := time.Millisecond * 20 + if runtime.GOOS == "darwin" { + n = 100 + } p1.Addr = l1.Multiaddr() // Addr has been determined by kernel. From 261b113530a2ca6d0418effcc81154ce19d85325 Mon Sep 17 00:00:00 2001 From: Jeromy Date: Sun, 10 Apr 2016 19:19:43 -0700 Subject: [PATCH 5/5] add a test with connections timing out --- p2p/net/conn/dial_test.go | 138 ++++++++++++++++++++++++++++++++++++++ p2p/net/conn/listen.go | 12 ++-- package.json | 74 +++++++++++--------- 3 files changed, 187 insertions(+), 37 deletions(-) diff --git a/p2p/net/conn/dial_test.go b/p2p/net/conn/dial_test.go index 8700d540..14e6c95d 100644 --- a/p2p/net/conn/dial_test.go +++ b/p2p/net/conn/dial_test.go @@ -16,11 +16,16 @@ import ( peer "github.com/ipfs/go-libp2p/p2p/peer" tu "github.com/ipfs/go-libp2p/testutil" + grc "gx/ipfs/QmTd4Jgb4nbJq5uR55KJgGLyHWmM3dovS21D1HcwRneSLu/gorocheck" msmux "gx/ipfs/QmUeEcYJrzAEKdQXjzTxCgNZgc9sRuwharsvzzm5Gd2oGB/go-multistream" context "gx/ipfs/QmZy2y8t9zQH2a1b8q2ZSLKp17ATuJoCNxxyMFG5qFExpt/go-net/context" ma "gx/ipfs/QmcobAGsCjYt5DXoq9et9L8yR8er7o7Cu3DTvpaq12jYSz/go-multiaddr" ) +func goroFilter(r *grc.Goroutine) bool { + return strings.Contains(r.Function, "go-log.") +} + func echoListen(ctx context.Context, listener Listener) { for { c, err := listener.Accept() @@ -452,6 +457,11 @@ func TestHangingAccept(t *testing.T) { <-done } +// This test kicks off N (=300) concurrent dials, which wait d (=20ms) seconds before failing. +// That wait holds up the handshake (multistream AND crypto), which will happen BEFORE +// l1.Accept() returns a connection. This test checks that the handshakes all happen +// concurrently in the listener side, and not sequentially. This ensures that a hanging dial +// will not block the listener from accepting other dials concurrently. func TestConcurrentAccept(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -509,4 +519,132 @@ func TestConcurrentAccept(t *testing.T) { t.Fatal("took too long!") } log.Errorf("took: %s (less than %s)", took, limit) + l1.Close() + wg.Wait() + cancel() + + time.Sleep(time.Millisecond * 100) + + err = grc.CheckForLeaks(goroFilter) + if err != nil { + panic(err) + t.Fatal(err) + } +} + +func TestConnectionTimeouts(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + old := NegotiateReadTimeout + NegotiateReadTimeout = time.Second * 5 + defer func() { NegotiateReadTimeout = old }() + + p1 := tu.RandPeerNetParamsOrFatal(t) + + l1, err := Listen(ctx, p1.Addr, p1.ID, p1.PrivKey) + if err != nil { + t.Fatal(err) + } + + n := 100 + if runtime.GOOS == "darwin" { + n = 50 + } + + p1.Addr = l1.Multiaddr() // Addr has been determined by kernel. + + var wg sync.WaitGroup + for i := 0; i < n; i++ { + wg.Add(1) + go func() { + defer wg.Done() + con, err := net.Dial("tcp", l1.Addr().String()) + if err != nil { + log.Error(err) + t.Error("first dial failed: ", err) + return + } + defer con.Close() + + // hang this connection until timeout + io.ReadFull(con, make([]byte, 1000)) + }() + } + + // wait to make sure the hanging dials have started + time.Sleep(time.Millisecond * 50) + + good_n := 20 + for i := 0; i < good_n; i++ { + wg.Add(1) + go func() { + defer wg.Done() + con, err := net.Dial("tcp", l1.Addr().String()) + if err != nil { + log.Error(err) + t.Error("first dial failed: ", err) + return + } + defer con.Close() + + // dial these ones through + err = msmux.SelectProtoOrFail(SecioTag, con) + if err != nil { + t.Error(err) + } + }() + } + + before := time.Now() + for i := 0; i < good_n; i++ { + c, err := l1.Accept() + if err != nil { + t.Fatal("connections during hung dials should still work: ", err) + } + + c.Close() + } + + took := time.Now().Sub(before) + + if took > time.Second*5 { + t.Fatal("hanging dials shouldnt block good dials") + } + + wg.Wait() + + go func() { + con, err := net.Dial("tcp", l1.Addr().String()) + if err != nil { + log.Error(err) + t.Error("first dial failed: ", err) + return + } + defer con.Close() + + // dial these ones through + err = msmux.SelectProtoOrFail(SecioTag, con) + if err != nil { + t.Error(err) + } + }() + + // make sure we can dial in still after a bunch of timeouts + con, err := l1.Accept() + if err != nil { + t.Fatal(err) + } + + con.Close() + l1.Close() + cancel() + + time.Sleep(time.Millisecond * 100) + + err = grc.CheckForLeaks(goroFilter) + if err != nil { + panic(err) + t.Fatal(err) + } } diff --git a/p2p/net/conn/listen.go b/p2p/net/conn/listen.go index 20d7ffd0..eee1fc23 100644 --- a/p2p/net/conn/listen.go +++ b/p2p/net/conn/listen.go @@ -19,11 +19,15 @@ import ( ma "gx/ipfs/QmcobAGsCjYt5DXoq9et9L8yR8er7o7Cu3DTvpaq12jYSz/go-multiaddr" ) -const SecioTag = "/secio/1.0.0" -const NoEncryptionTag = "/plaintext/1.0.0" +const ( + SecioTag = "/secio/1.0.0" + NoEncryptionTag = "/plaintext/1.0.0" +) -const connAcceptBuffer = 32 -const NegotiateReadTimeout = time.Second * 20 +var ( + connAcceptBuffer = 32 + NegotiateReadTimeout = time.Second * 60 +) // ConnWrapper is any function that wraps a raw multiaddr connection type ConnWrapper func(transport.Conn) transport.Conn diff --git a/package.json b/package.json index 23982952..c0b2f0e1 100644 --- a/package.json +++ b/package.json @@ -1,145 +1,153 @@ { - "name": "go-libp2p", "author": "whyrusleeping", - "version": "1.0.0", + "bugs": {}, + "gx": { + "dvcsimport": "github.com/ipfs/go-libp2p" + }, "gxDependencies": [ { - "name": "go-semver", "hash": "QmcrrEpx3VMUbrbgVroH3YiYyUS5c4YAykzyPJWKspUYLa", + "name": "go-semver", "version": "0.0.0" }, { - "name": "mdns", "hash": "QmSscYPCcE1H3UQr2tnsJ2a9dK9LsHTBGgP71VW6fz67e5", + "name": "mdns", "version": "0.0.0" }, { - "name": "go-msgio", "hash": "QmRQhVisS8dmPbjBUthVkenn81pBxrx1GxE281csJhm2vL", + "name": "go-msgio", "version": "0.0.0" }, { - "name": "go-ipfs-util", "hash": "QmZNVWh8LLjAavuQ2JXuFmuYH3C11xo988vSgp7UQrTRj1", + "name": "go-ipfs-util", "version": "1.0.0" }, { - "name": "go-keyspace", "hash": "QmUusaX99BZoELh7dmPgirqRQ1FAmMnmnBn3oiqDFGBUSc", + "name": "go-keyspace", "version": "1.0.0" }, { - "name": "go-multistream", "hash": "QmUeEcYJrzAEKdQXjzTxCgNZgc9sRuwharsvzzm5Gd2oGB", + "name": "go-multistream", "version": "0.0.0" }, { - "name": "go-nat", "hash": "QmNLvkCDV6ZjUJsEwGNporYBuZdhWT6q7TBVYQwwRv12HT", + "name": "go-nat", "version": "0.0.0" }, { - "name": "go-detect-race", "hash": "QmQHGMVmrsgmqUG8ih3puNXUJneSpi13dkcZpzLKkskUkH", + "name": "go-detect-race", "version": "0.0.0" }, { - "name": "goprocess", "hash": "QmQopLATEYMNg7dVqZRNDfeE2S1yKy8zrRh5xnYiuqeZBn", + "name": "goprocess", "version": "0.0.0" }, { - "name": "go-log", "hash": "Qmazh5oNUVsDZTs2g59rq8aYQqwpss8tcUWQzor5sCCEuH", + "name": "go-log", "version": "0.0.0" }, { - "name": "go-multiaddr-net", "hash": "QmYVqhVfbK4BKvbW88Lhm26b3ud14sTBvcm1H7uWUx1Fkp", + "name": "go-multiaddr-net", "version": "0.0.0" }, { - "name": "go-multihash", "hash": "QmYf7ng2hG5XBtJA3tN34DQ2GUN5HNksEw1rLDkmr6vGku", + "name": "go-multihash", "version": "0.0.0" }, { - "name": "multiaddr-filter", "hash": "QmPwfFAHUmvWDucLHRS9Xz2Kb1TNX2cY4LJ7pQjg9kVcae", + "name": "multiaddr-filter", "version": "1.0.0" }, { - "name": "go-base58", "hash": "QmT8rehPR3F6bmwL6zjUN8XpiDBFFpMP2myPdC6ApsWfJf", + "name": "go-base58", "version": "0.0.0" }, { - "name": "go-crypto", "hash": "Qme1boxspcQWR8FBzMxeppqug2fYgYc15diNWmqgDVnvn2", + "name": "go-crypto", "version": "0.0.0" }, { - "name": "gogo-protobuf", "hash": "QmZ4Qi3GaRbjcx28Sme5eMH7RQjGkt8wHxt2a65oLaeFEV", + "name": "gogo-protobuf", "version": "0.0.0" }, { - "name": "go-multiaddr", "hash": "QmcobAGsCjYt5DXoq9et9L8yR8er7o7Cu3DTvpaq12jYSz", + "name": "go-multiaddr", "version": "0.0.0" }, { - "name": "go-metrics", "hash": "QmeYJHEk8UjVVZ4XCRTZe6dFQrb8pGWD81LYCgeLp8CvMB", + "name": "go-metrics", "version": "0.0.0" }, { - "name": "randbo", "hash": "QmYvsG72GsfLgUeSojXArjnU6L4Wmwk7wuAxtNLuyXcc1T", + "name": "randbo", "version": "0.0.0" }, { - "name": "go-net", "hash": "QmZy2y8t9zQH2a1b8q2ZSLKp17ATuJoCNxxyMFG5qFExpt", + "name": "go-net", "version": "0.0.0" }, { - "name": "go-stream-muxer", "hash": "QmWSJzRkCMJFHYUQZxKwPX8WA7XipaPtfiwMPARP51ymfn", + "name": "go-stream-muxer", "version": "0.0.0" }, { - "name": "go-reuseport", "hash": "QmaaC9QMYTQHCbMq3Ebr3uMaAR2ev4AVqMmsJpgQijAZbJ", + "name": "go-reuseport", "version": "0.0.0" }, { - "name": "go-notifier", "hash": "QmbcS9XrwZkF1rZj8bBwwzoYhVuA2PCnPhFUL1pyWGgt2A", + "name": "go-notifier", "version": "0.0.0" }, { - "name": "go-temp-err-catcher", "hash": "QmWHgLqrghM9zw77nF6gdvT9ExQ2RB9pLxkd8sDHZf1rWb", + "name": "go-temp-err-catcher", "version": "0.0.0" }, { - "name": "go-peerstream", "hash": "QmZK81vcgMhpb2t7GNbozk7qzt6Rj4zFqitpvsWT9mduW8", + "name": "go-peerstream", "version": "0.0.0" }, { "author": "whyrusleeping", - "name": "mafmt", "hash": "QmWLfU4tstw2aNcTykDm44xbSTCYJ9pUJwfhQCKGwckcHx", + "name": "mafmt", + "version": "0.0.0" + }, + { + "author": "whyrusleeping", + "hash": "QmTd4Jgb4nbJq5uR55KJgGLyHWmM3dovS21D1HcwRneSLu", + "name": "gorocheck", "version": "0.0.0" } ], + "gxVersion": "0.4.0", + "gx_version": "0.4.0", + "issues_url": "", "language": "go", "license": "", - "bugs": "", - "gxVersion": "0.4.0", - "gx": { - "dvcsimport": "github.com/ipfs/go-libp2p" - } + "name": "go-libp2p", + "version": "1.0.0" } \ No newline at end of file