Skip to content

Commit 02ab795

Browse files
authored
Implement error codes spec (#2927)
1 parent 4957d35 commit 02ab795

40 files changed

+672
-92
lines changed

core/network/conn.go

+50
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,60 @@ package network
22

33
import (
44
"context"
5+
"fmt"
56
"io"
67

78
ic "github.com/libp2p/go-libp2p/core/crypto"
9+
810
"github.com/libp2p/go-libp2p/core/peer"
911
"github.com/libp2p/go-libp2p/core/protocol"
1012

1113
ma "github.com/multiformats/go-multiaddr"
1214
)
1315

16+
type ConnErrorCode uint32
17+
18+
type ConnError struct {
19+
Remote bool
20+
ErrorCode ConnErrorCode
21+
TransportError error
22+
}
23+
24+
func (c *ConnError) Error() string {
25+
side := "local"
26+
if c.Remote {
27+
side = "remote"
28+
}
29+
if c.TransportError != nil {
30+
return fmt.Sprintf("connection closed (%s): code: 0x%x: transport error: %s", side, c.ErrorCode, c.TransportError)
31+
}
32+
return fmt.Sprintf("connection closed (%s): code: 0x%x", side, c.ErrorCode)
33+
}
34+
35+
func (c *ConnError) Is(target error) bool {
36+
if tce, ok := target.(*ConnError); ok {
37+
return tce.ErrorCode == c.ErrorCode && tce.Remote == c.Remote
38+
}
39+
return false
40+
}
41+
42+
func (c *ConnError) Unwrap() []error {
43+
return []error{ErrReset, c.TransportError}
44+
}
45+
46+
const (
47+
ConnNoError ConnErrorCode = 0
48+
ConnProtocolNegotiationFailed ConnErrorCode = 0x1000
49+
ConnResourceLimitExceeded ConnErrorCode = 0x1001
50+
ConnRateLimited ConnErrorCode = 0x1002
51+
ConnProtocolViolation ConnErrorCode = 0x1003
52+
ConnSupplanted ConnErrorCode = 0x1004
53+
ConnGarbageCollected ConnErrorCode = 0x1005
54+
ConnShutdown ConnErrorCode = 0x1006
55+
ConnGated ConnErrorCode = 0x1007
56+
ConnCodeOutOfRange ConnErrorCode = 0x1008
57+
)
58+
1459
// Conn is a connection to a remote peer. It multiplexes streams.
1560
// Usually there is no need to use a Conn directly, but it may
1661
// be useful to get information about the peer on the other side:
@@ -24,6 +69,11 @@ type Conn interface {
2469
ConnStat
2570
ConnScoper
2671

72+
// CloseWithError closes the connection with errCode. The errCode is sent to the
73+
// peer on a best effort basis. For transports that do not support sending error
74+
// codes on connection close, the behavior is identical to calling Close.
75+
CloseWithError(errCode ConnErrorCode) error
76+
2777
// ID returns an identifier that uniquely identifies this Conn within this
2878
// host, during this run. Connection IDs may repeat across restarts.
2979
ID() string

core/network/mux.go

+53
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package network
33
import (
44
"context"
55
"errors"
6+
"fmt"
67
"io"
78
"net"
89
"time"
@@ -11,6 +12,49 @@ import (
1112
// ErrReset is returned when reading or writing on a reset stream.
1213
var ErrReset = errors.New("stream reset")
1314

15+
type StreamErrorCode uint32
16+
17+
type StreamError struct {
18+
ErrorCode StreamErrorCode
19+
Remote bool
20+
TransportError error
21+
}
22+
23+
func (s *StreamError) Error() string {
24+
side := "local"
25+
if s.Remote {
26+
side = "remote"
27+
}
28+
if s.TransportError != nil {
29+
return fmt.Sprintf("stream reset (%s): code: 0x%x: transport error: %s", side, s.ErrorCode, s.TransportError)
30+
}
31+
return fmt.Sprintf("stream reset (%s): code: 0x%x", side, s.ErrorCode)
32+
}
33+
34+
func (s *StreamError) Is(target error) bool {
35+
if tse, ok := target.(*StreamError); ok {
36+
return tse.ErrorCode == s.ErrorCode && tse.Remote == s.Remote
37+
}
38+
return false
39+
}
40+
41+
func (s *StreamError) Unwrap() []error {
42+
return []error{ErrReset, s.TransportError}
43+
}
44+
45+
const (
46+
StreamNoError StreamErrorCode = 0
47+
StreamProtocolNegotiationFailed StreamErrorCode = 0x1001
48+
StreamResourceLimitExceeded StreamErrorCode = 0x1002
49+
StreamRateLimited StreamErrorCode = 0x1003
50+
StreamProtocolViolation StreamErrorCode = 0x1004
51+
StreamSupplanted StreamErrorCode = 0x1005
52+
StreamGarbageCollected StreamErrorCode = 0x1006
53+
StreamShutdown StreamErrorCode = 0x1007
54+
StreamGated StreamErrorCode = 0x1008
55+
StreamCodeOutOfRange StreamErrorCode = 0x1009
56+
)
57+
1458
// MuxedStream is a bidirectional io pipe within a connection.
1559
type MuxedStream interface {
1660
io.Reader
@@ -56,6 +100,11 @@ type MuxedStream interface {
56100
// side to hang up and go away.
57101
Reset() error
58102

103+
// ResetWithError aborts both ends of the stream with `errCode`. `errCode` is sent
104+
// to the peer on a best effort basis. For transports that do not support sending
105+
// error codes to remote peer, the behavior is identical to calling Reset
106+
ResetWithError(errCode StreamErrorCode) error
107+
59108
SetDeadline(time.Time) error
60109
SetReadDeadline(time.Time) error
61110
SetWriteDeadline(time.Time) error
@@ -75,6 +124,10 @@ type MuxedConn interface {
75124
// Close closes the stream muxer and the the underlying net.Conn.
76125
io.Closer
77126

127+
// CloseWithError closes the connection with errCode. The errCode is sent
128+
// to the peer.
129+
CloseWithError(errCode ConnErrorCode) error
130+
78131
// IsClosed returns whether a connection is fully closed, so it can
79132
// be garbage collected.
80133
IsClosed() bool

core/network/stream.go

+4
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,8 @@ type Stream interface {
2727

2828
// Scope returns the user's view of this stream's resource scope
2929
Scope() StreamScope
30+
31+
// ResetWithError closes both ends of the stream with errCode. The errCode is sent
32+
// to the peer.
33+
ResetWithError(errCode StreamErrorCode) error
3034
}

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ require (
3030
github.com/libp2p/go-nat v0.2.0
3131
github.com/libp2p/go-netroute v0.2.2
3232
github.com/libp2p/go-reuseport v0.4.0
33-
github.com/libp2p/go-yamux/v4 v4.0.2
33+
github.com/libp2p/go-yamux/v5 v5.0.0
3434
github.com/libp2p/zeroconf/v2 v2.2.0
3535
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd
3636
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b

go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,8 @@ github.com/libp2p/go-netroute v0.2.2 h1:Dejd8cQ47Qx2kRABg6lPwknU7+nBnFRpko45/fFP
193193
github.com/libp2p/go-netroute v0.2.2/go.mod h1:Rntq6jUAH0l9Gg17w5bFGhcC9a+vk4KNXs6s7IljKYE=
194194
github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s=
195195
github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU=
196-
github.com/libp2p/go-yamux/v4 v4.0.2 h1:nrLh89LN/LEiqcFiqdKDRHjGstN300C1269K/EX0CPU=
197-
github.com/libp2p/go-yamux/v4 v4.0.2/go.mod h1:C808cCRgOs1iBwY4S71T5oxgMxgLmqUw56qh4AeBW2o=
196+
github.com/libp2p/go-yamux/v5 v5.0.0 h1:2djUh96d3Jiac/JpGkKs4TO49YhsfLopAoryfPmf+Po=
197+
github.com/libp2p/go-yamux/v5 v5.0.0/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU=
198198
github.com/libp2p/zeroconf/v2 v2.2.0 h1:Cup06Jv6u81HLhIj1KasuNM/RHHrJ8T7wOTS4+Tv53Q=
199199
github.com/libp2p/zeroconf/v2 v2.2.0/go.mod h1:fuJqLnUwZTshS3U/bMRJ3+ow/v9oid1n0DmyYyNO1Xs=
200200
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=

p2p/host/basic/basic_host.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,7 @@ func (h *BasicHost) newStreamHandler(s network.Stream) {
464464
} else {
465465
log.Debugf("protocol mux failed: %s (took %s, id:%s, remote peer:%s, remote addr:%v)", err, took, s.ID(), s.Conn().RemotePeer(), s.Conn().RemoteMultiaddr())
466466
}
467-
s.Reset()
467+
s.ResetWithError(network.StreamProtocolNegotiationFailed)
468468
return
469469
}
470470

@@ -478,7 +478,7 @@ func (h *BasicHost) newStreamHandler(s network.Stream) {
478478

479479
if err := s.SetProtocol(protoID); err != nil {
480480
log.Debugf("error setting stream protocol: %s", err)
481-
s.Reset()
481+
s.ResetWithError(network.StreamResourceLimitExceeded)
482482
return
483483
}
484484

@@ -717,7 +717,7 @@ func (h *BasicHost) NewStream(ctx context.Context, p peer.ID, pids ...protocol.I
717717
}
718718
defer func() {
719719
if strErr != nil && s != nil {
720-
s.Reset()
720+
s.ResetWithError(network.StreamProtocolNegotiationFailed)
721721
}
722722
}()
723723

@@ -761,13 +761,14 @@ func (h *BasicHost) NewStream(ctx context.Context, p peer.ID, pids ...protocol.I
761761
return nil, fmt.Errorf("failed to negotiate protocol: %w", err)
762762
}
763763
case <-ctx.Done():
764-
s.Reset()
764+
s.ResetWithError(network.StreamProtocolNegotiationFailed)
765765
// wait for `SelectOneOf` to error out because of resetting the stream.
766766
<-errCh
767767
return nil, fmt.Errorf("failed to negotiate protocol: %w", ctx.Err())
768768
}
769769

770770
if err := s.SetProtocol(selected); err != nil {
771+
s.ResetWithError(network.StreamResourceLimitExceeded)
771772
return nil, err
772773
}
773774
_ = h.Peerstore().AddProtocols(p, selected) // adding the protocol to the peerstore isn't critical

p2p/muxer/testsuite/mux.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"context"
66
crand "crypto/rand"
7+
"errors"
78
"fmt"
89
"io"
910
mrand "math/rand"
@@ -462,7 +463,7 @@ func SubtestStreamReset(t *testing.T, tr network.Multiplexer) {
462463
time.Sleep(time.Millisecond * 50)
463464

464465
_, err = s.Write([]byte("foo"))
465-
if err != network.ErrReset {
466+
if !errors.Is(err, network.ErrReset) {
466467
t.Error("should have been stream reset")
467468
}
468469
s.Close()

p2p/muxer/yamux/conn.go

+7-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import (
55

66
"github.com/libp2p/go-libp2p/core/network"
77

8-
"github.com/libp2p/go-yamux/v4"
8+
"github.com/libp2p/go-yamux/v5"
99
)
1010

1111
// conn implements mux.MuxedConn over yamux.Session.
@@ -23,6 +23,10 @@ func (c *conn) Close() error {
2323
return c.yamux().Close()
2424
}
2525

26+
func (c *conn) CloseWithError(errCode network.ConnErrorCode) error {
27+
return c.yamux().CloseWithError(uint32(errCode))
28+
}
29+
2630
// IsClosed checks if yamux.Session is in closed state.
2731
func (c *conn) IsClosed() bool {
2832
return c.yamux().IsClosed()
@@ -32,7 +36,7 @@ func (c *conn) IsClosed() bool {
3236
func (c *conn) OpenStream(ctx context.Context) (network.MuxedStream, error) {
3337
s, err := c.yamux().OpenStream(ctx)
3438
if err != nil {
35-
return nil, err
39+
return nil, parseError(err)
3640
}
3741

3842
return (*stream)(s), nil
@@ -41,7 +45,7 @@ func (c *conn) OpenStream(ctx context.Context) (network.MuxedStream, error) {
4145
// AcceptStream accepts a stream opened by the other side.
4246
func (c *conn) AcceptStream() (network.MuxedStream, error) {
4347
s, err := c.yamux().AcceptStream()
44-
return (*stream)(s), err
48+
return (*stream)(s), parseError(err)
4549
}
4650

4751
func (c *conn) yamux() *yamux.Session {

p2p/muxer/yamux/stream.go

+27-11
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,46 @@
11
package yamux
22

33
import (
4+
"errors"
5+
"fmt"
46
"time"
57

68
"github.com/libp2p/go-libp2p/core/network"
79

8-
"github.com/libp2p/go-yamux/v4"
10+
"github.com/libp2p/go-yamux/v5"
911
)
1012

1113
// stream implements mux.MuxedStream over yamux.Stream.
1214
type stream yamux.Stream
1315

1416
var _ network.MuxedStream = &stream{}
1517

16-
func (s *stream) Read(b []byte) (n int, err error) {
17-
n, err = s.yamux().Read(b)
18-
if err == yamux.ErrStreamReset {
19-
err = network.ErrReset
18+
func parseError(err error) error {
19+
if err == nil {
20+
return err
21+
}
22+
se := &yamux.StreamError{}
23+
if errors.As(err, &se) {
24+
return &network.StreamError{Remote: se.Remote, ErrorCode: network.StreamErrorCode(se.ErrorCode), TransportError: err}
2025
}
26+
ce := &yamux.GoAwayError{}
27+
if errors.As(err, &ce) {
28+
return &network.ConnError{Remote: ce.Remote, ErrorCode: network.ConnErrorCode(ce.ErrorCode), TransportError: err}
29+
}
30+
if errors.Is(err, yamux.ErrStreamReset) {
31+
return fmt.Errorf("%w: %w", network.ErrReset, err)
32+
}
33+
return err
34+
}
2135

22-
return n, err
36+
func (s *stream) Read(b []byte) (n int, err error) {
37+
n, err = s.yamux().Read(b)
38+
return n, parseError(err)
2339
}
2440

2541
func (s *stream) Write(b []byte) (n int, err error) {
2642
n, err = s.yamux().Write(b)
27-
if err == yamux.ErrStreamReset {
28-
err = network.ErrReset
29-
}
30-
31-
return n, err
43+
return n, parseError(err)
3244
}
3345

3446
func (s *stream) Close() error {
@@ -39,6 +51,10 @@ func (s *stream) Reset() error {
3951
return s.yamux().Reset()
4052
}
4153

54+
func (s *stream) ResetWithError(errCode network.StreamErrorCode) error {
55+
return s.yamux().ResetWithError(uint32(errCode))
56+
}
57+
4258
func (s *stream) CloseRead() error {
4359
return s.yamux().CloseRead()
4460
}

p2p/muxer/yamux/transport.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77

88
"github.com/libp2p/go-libp2p/core/network"
99

10-
"github.com/libp2p/go-yamux/v4"
10+
"github.com/libp2p/go-yamux/v5"
1111
)
1212

1313
var DefaultTransport *Transport

p2p/net/connmgr/connmgr.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,8 @@ func (cm *BasicConnMgr) memoryEmergency() {
175175
// Trim connections without paying attention to the silence period.
176176
for _, c := range cm.getConnsToCloseEmergency(target) {
177177
log.Infow("low on memory. closing conn", "peer", c.RemotePeer())
178-
c.Close()
178+
179+
c.CloseWithError(network.ConnGarbageCollected)
179180
}
180181

181182
// finally, update the last trim time.
@@ -388,7 +389,7 @@ func (cm *BasicConnMgr) trim() {
388389
// do the actual trim.
389390
for _, c := range cm.getConnsToClose() {
390391
log.Debugw("closing conn", "peer", c.RemotePeer())
391-
c.Close()
392+
c.CloseWithError(network.ConnGarbageCollected)
392393
}
393394
}
394395

0 commit comments

Comments
 (0)