From 4a9cd1b556f089fec212e6915efa5d67a09f2845 Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Thu, 18 Jan 2018 22:05:52 -0800 Subject: [PATCH 01/29] initial commit --- p2p/net/reuseport/dial.go | 102 ++++++++++++++++++++++++++++ p2p/net/reuseport/listen.go | 72 ++++++++++++++++++++ p2p/net/reuseport/multidialer.go | 88 ++++++++++++++++++++++++ p2p/net/reuseport/reuseport.go | 63 +++++++++++++++++ p2p/net/reuseport/reuseport_test.go | 48 +++++++++++++ p2p/net/reuseport/singledialer.go | 16 +++++ p2p/net/reuseport/transport.go | 25 +++++++ 7 files changed, 414 insertions(+) create mode 100644 p2p/net/reuseport/dial.go create mode 100644 p2p/net/reuseport/listen.go create mode 100644 p2p/net/reuseport/multidialer.go create mode 100644 p2p/net/reuseport/reuseport.go create mode 100644 p2p/net/reuseport/reuseport_test.go create mode 100644 p2p/net/reuseport/singledialer.go create mode 100644 p2p/net/reuseport/transport.go diff --git a/p2p/net/reuseport/dial.go b/p2p/net/reuseport/dial.go new file mode 100644 index 00000000..c5996675 --- /dev/null +++ b/p2p/net/reuseport/dial.go @@ -0,0 +1,102 @@ +package tcpreuse + +import ( + "context" + "net" + + reuseport "github.com/libp2p/go-reuseport" + ma "github.com/multiformats/go-multiaddr" + manet "github.com/multiformats/go-multiaddr-net" +) + +type dialer interface { + Dial(network, addr string) (net.Conn, error) + DialContext(ctx context.Context, network, addr string) (net.Conn, error) +} + +func (t *Transport) Dial(raddr ma.Multiaddr) (manet.Conn, error) { + return t.DialContext(context.Background(), raddr) +} + +func (t *Transport) DialContext(ctx context.Context, raddr ma.Multiaddr) (manet.Conn, error) { + network, addr, err := manet.DialArgs(raddr) + if err != nil { + return nil, err + } + var d dialer + switch network { + case "tcp4": + d = t.v4.getDialer(network) + case "tcp6": + d = t.v6.getDialer(network) + default: + return nil, ErrWrongProto + } + conn, err := d.DialContext(ctx, network, addr) + if err != nil { + return nil, err + } + maconn, err := manet.WrapNetConn(conn) + if err != nil { + conn.Close() + return nil, err + } + return maconn, nil +} + +func (n *network) getDialer(network string) dialer { + n.mu.RLock() + d := n.dialer + n.mu.RUnlock() + if d == nil { + n.mu.Lock() + defer n.mu.Unlock() + + if n.dialer == nil { + n.dialer = n.makeDialer(network) + } + d = n.dialer + } + return d +} + +func (n *network) makeDialer(network string) dialer { + if !reuseport.Available() { + log.Debug("reuseport not available") + return &net.Dialer{} + } + + var unspec net.IP + switch network { + case "tcp4": + unspec = net.IPv4zero + case "tcp6": + unspec = net.IPv6unspecified + default: + panic("invalid network: must be either tcp4 or tcp6") + } + + // How many ports are we listening on. + var port = 0 + for l := range n.listeners { + if port == 0 { + port = l.Addr().(*net.TCPAddr).Port + } else { + // > 1 + return newMultiDialer(unspec, n.listeners) + } + } + + // None. + if port == 0 { + return &net.Dialer{} + } + + // One. Always dial from the single port we're listening on. + laddr := &net.TCPAddr{ + IP: unspec, + Port: port, + } + + return (*singleDialer)(laddr) +} diff --git a/p2p/net/reuseport/listen.go b/p2p/net/reuseport/listen.go new file mode 100644 index 00000000..483b33ee --- /dev/null +++ b/p2p/net/reuseport/listen.go @@ -0,0 +1,72 @@ +package tcpreuse + +import ( + "net" + + reuseport "github.com/libp2p/go-reuseport" + ma "github.com/multiformats/go-multiaddr" + manet "github.com/multiformats/go-multiaddr-net" +) + +type listener struct { + manet.Listener + network *network +} + +func (l *listener) Close() error { + l.network.mu.Lock() + delete(l.network.listeners, l) + l.network.mu.Unlock() + return l.Listener.Close() +} + +func (t *Transport) Listen(laddr ma.Multiaddr) (manet.Listener, error) { + nw, naddr, err := manet.DialArgs(laddr) + if err != nil { + return nil, err + } + var n *network + switch nw { + case "tcp4": + n = &t.v4 + case "tcp6": + n = &t.v6 + default: + return nil, ErrWrongProto + } + + if !reuseport.Available() { + return manet.Listen(laddr) + } + nl, err := reuseport.Listen(nw, naddr) + if err != nil { + return manet.Listen(laddr) + } + + if _, ok := nl.Addr().(*net.TCPAddr); !ok { + nl.Close() + return nil, ErrWrongProto + } + + malist, err := manet.WrapNetListener(nl) + if err != nil { + nl.Close() + return nil, err + } + + list := &listener{ + Listener: malist, + network: n, + } + + n.mu.Lock() + defer n.mu.Unlock() + + if n.listeners == nil { + n.listeners = make(map[*listener]struct{}) + } + n.listeners[list] = struct{}{} + n.dialer = nil + + return list, nil +} diff --git a/p2p/net/reuseport/multidialer.go b/p2p/net/reuseport/multidialer.go new file mode 100644 index 00000000..b8f4129f --- /dev/null +++ b/p2p/net/reuseport/multidialer.go @@ -0,0 +1,88 @@ +package tcpreuse + +import ( + "context" + "fmt" + "math/rand" + "net" +) + +type multiDialer struct { + loopback []*net.TCPAddr + unspecified []*net.TCPAddr + global *net.TCPAddr +} + +func (d *multiDialer) Dial(network, addr string) (net.Conn, error) { + return d.DialContext(context.Background(), network, addr) +} + +func randAddr(addrs []*net.TCPAddr) *net.TCPAddr { + if len(addrs) > 0 { + return addrs[rand.Intn(len(addrs))] + } + return nil +} + +func (d *multiDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) { + tcpAddr, err := net.ResolveTCPAddr(network, addr) + if err != nil { + return nil, err + } + + ip := tcpAddr.IP + source := d.global + switch { + case ip.IsLoopback(): + switch { + case len(d.loopback) > 0: + source = randAddr(d.loopback) + case len(d.unspecified) > 0: + source = randAddr(d.unspecified) + } + case ip.IsGlobalUnicast(): + switch { + case len(d.unspecified) > 0: + source = randAddr(d.unspecified) + } + default: + return nil, fmt.Errorf("undialable IP: %s", tcpAddr.IP) + } + return reuseDial(ctx, source, network, addr) +} + +func newMultiDialer(unspec net.IP, listeners map[*listener]struct{}) dialer { + m := new(multiDialer) + for l := range listeners { + laddr := l.Addr().(*net.TCPAddr) + switch { + case laddr.IP.IsLoopback(): + m.loopback = append(m.loopback, laddr) + case laddr.IP.IsGlobalUnicast(): + // Different global ports? Crap. + // + // The *proper* way to deal with this is to, e.g., use + // netlink to figure out which source address we would + // normally use to dial a destination address and then + // pick one of the ports we're listening on on that + // source address. However, this is a pain in the ass. + // + // Instead, we're just going to always dial from the + // unspecified address with the first global port we + // find. + // + // TODO: Port priority? Addr priority? + if m.global != nil { + m.global = &net.TCPAddr{ + IP: unspec, + Port: laddr.Port, + } + } else { + log.Warning("listening on external interfaces on multiple ports, will dial from %d, not %s", m.global, laddr) + } + case laddr.IP.IsUnspecified(): + m.unspecified = append(m.unspecified, laddr) + } + } + return m +} diff --git a/p2p/net/reuseport/reuseport.go b/p2p/net/reuseport/reuseport.go new file mode 100644 index 00000000..c85f1180 --- /dev/null +++ b/p2p/net/reuseport/reuseport.go @@ -0,0 +1,63 @@ +package tcpreuse + +import ( + "context" + "net" + "syscall" + + reuseport "github.com/libp2p/go-reuseport" +) + +// ReuseErrShouldRetry diagnoses whether to retry after a reuse error. +// if we failed to bind, we should retry. if bind worked and this is a +// real dial error (remote end didnt answer) then we should not retry. +func ReuseErrShouldRetry(err error) bool { + if err == nil { + return false // hey, it worked! no need to retry. + } + + // if it's a network timeout error, it's a legitimate failure. + if nerr, ok := err.(net.Error); ok && nerr.Timeout() { + return false + } + + errno, ok := err.(syscall.Errno) + if !ok { // not an errno? who knows what this is. retry. + return true + } + + switch errno { + case syscall.EADDRINUSE, syscall.EADDRNOTAVAIL: + return true // failure to bind. retry. + case syscall.ECONNREFUSED: + return false // real dial error + default: + return true // optimistically default to retry. + } +} + +// Dials using reusport and then redials normally if that fails. +func reuseDial(ctx context.Context, laddr *net.TCPAddr, network, raddr string) (net.Conn, error) { + if laddr == nil { + var d net.Dialer + return d.DialContext(ctx, network, raddr) + } + + d := reuseport.Dialer{ + D: net.Dialer{ + LocalAddr: laddr, + }, + } + + con, err := d.DialContext(ctx, network, raddr) + if err != nil { + return con, err + } + + if ReuseErrShouldRetry(err) && ctx.Err() == nil { + log.Debug("failed to reuse port, dialing with a random port") + var d net.Dialer + con, err = d.DialContext(ctx, network, raddr) + } + return con, err +} diff --git a/p2p/net/reuseport/reuseport_test.go b/p2p/net/reuseport/reuseport_test.go new file mode 100644 index 00000000..a4d8b1e2 --- /dev/null +++ b/p2p/net/reuseport/reuseport_test.go @@ -0,0 +1,48 @@ +package tcpreuse + +import ( + "net" + "syscall" + "testing" +) + +type netTimeoutErr struct { + timeout bool +} + +func (e netTimeoutErr) Error() string { + return "" +} + +func (e netTimeoutErr) Timeout() bool { + return e.timeout +} + +func (e netTimeoutErr) Temporary() bool { + panic("not checked") +} + +func TestReuseError(t *testing.T) { + var nte1 net.Error = &netTimeoutErr{true} + var nte2 net.Error = &netTimeoutErr{false} + + cases := map[error]bool{ + nil: false, + syscall.EADDRINUSE: true, + syscall.EADDRNOTAVAIL: true, + syscall.ECONNREFUSED: false, + + nte1: false, + nte2: true, // this ones a little weird... we should check neterror.Temporary() too + + // test 'default' to true + syscall.EBUSY: true, + } + + for k, v := range cases { + if ReuseErrShouldRetry(k) != v { + t.Fatalf("expected %t for %#v", v, k) + } + } + +} diff --git a/p2p/net/reuseport/singledialer.go b/p2p/net/reuseport/singledialer.go new file mode 100644 index 00000000..efb96eb1 --- /dev/null +++ b/p2p/net/reuseport/singledialer.go @@ -0,0 +1,16 @@ +package tcpreuse + +import ( + "context" + "net" +) + +type singleDialer net.TCPAddr + +func (d *singleDialer) Dial(network, address string) (net.Conn, error) { + return d.DialContext(context.Background(), network, address) +} + +func (d *singleDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { + return reuseDial(ctx, (*net.TCPAddr)(d), network, address) +} diff --git a/p2p/net/reuseport/transport.go b/p2p/net/reuseport/transport.go new file mode 100644 index 00000000..5f094d1a --- /dev/null +++ b/p2p/net/reuseport/transport.go @@ -0,0 +1,25 @@ +package tcpreuse + +import ( + "errors" + "sync" + + logging "github.com/ipfs/go-log" +) + +var log = logging.Logger("reuseport-transport") + +// ErrWrongProto is returned when dialing a protocol other than tcp. +var ErrWrongProto = errors.New("can only dial TCP over IPv4 or IPv6") + +// Transport is a TCP reuse transport that reuses listener ports. +type Transport struct { + v4 network + v6 network +} + +type network struct { + mu sync.RWMutex + listeners map[*listener]struct{} + dialer dialer +} From ad8c7eec539ccf0776e5d573dcd267936ee9e876 Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Fri, 19 Jan 2018 14:45:26 -0800 Subject: [PATCH 02/29] add some documentation --- p2p/net/reuseport/dial.go | 8 ++++++++ p2p/net/reuseport/listen.go | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/p2p/net/reuseport/dial.go b/p2p/net/reuseport/dial.go index c5996675..0fa6e99b 100644 --- a/p2p/net/reuseport/dial.go +++ b/p2p/net/reuseport/dial.go @@ -14,10 +14,18 @@ type dialer interface { DialContext(ctx context.Context, network, addr string) (net.Conn, error) } +// Dial dials the given multiaddr, reusing ports we're currently listening on if +// possible. +// +// Dial attempts to be smart about choosing the source port. For example, If +// we're dialing a loopback address and we're listening on one or more loopback +// ports, Dial will randomly choose one of the loopback ports and addresses and +// reuse it. func (t *Transport) Dial(raddr ma.Multiaddr) (manet.Conn, error) { return t.DialContext(context.Background(), raddr) } +// DialContext is like Dial but takes a context. func (t *Transport) DialContext(ctx context.Context, raddr ma.Multiaddr) (manet.Conn, error) { network, addr, err := manet.DialArgs(raddr) if err != nil { diff --git a/p2p/net/reuseport/listen.go b/p2p/net/reuseport/listen.go index 483b33ee..04bd01b5 100644 --- a/p2p/net/reuseport/listen.go +++ b/p2p/net/reuseport/listen.go @@ -20,6 +20,13 @@ func (l *listener) Close() error { return l.Listener.Close() } +// Listen listens on the given multiaddr. +// +// If reuseport is supported, it will be enabled for this listener and future +// dials from this transport may reuse the port. +// +// Note: You can listen on the same multiaddr as many times as you want +// (although only *one* listener will end up handling the inbound connection). func (t *Transport) Listen(laddr ma.Multiaddr) (manet.Listener, error) { nw, naddr, err := manet.DialArgs(laddr) if err != nil { From 154ae5c47c25c1e2ebea8ba4aa2e3d10bee044f6 Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Fri, 19 Jan 2018 14:45:33 -0800 Subject: [PATCH 03/29] add minimal test --- p2p/net/reuseport/transport_test.go | 40 +++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 p2p/net/reuseport/transport_test.go diff --git a/p2p/net/reuseport/transport_test.go b/p2p/net/reuseport/transport_test.go new file mode 100644 index 00000000..1b3941f4 --- /dev/null +++ b/p2p/net/reuseport/transport_test.go @@ -0,0 +1,40 @@ +package tcpreuse + +import ( + "testing" + + ma "github.com/multiformats/go-multiaddr" +) + +func TestAll(t *testing.T) { + var trA Transport + var trB Transport + laddr, _ := ma.NewMultiaddr("/ip4/127.0.0.1/tcp/0") + listenerA, err := trA.Listen(laddr) + if err != nil { + t.Fatal(err) + } + defer listenerA.Close() + listenerB, err := trB.Listen(laddr) + if err != nil { + t.Fatal(err) + } + defer listenerB.Close() + + done := make(chan struct{}) + go func() { + defer close(done) + c, err := listenerA.Accept() + if err != nil { + t.Fatal(err) + } + c.Close() + }() + + c, err := trB.Dial(listenerA.Multiaddr()) + if err != nil { + t.Fatal(err) + } + <-done + c.Close() +} From fc057b07c10eabe7425c2293c23602d33ec6d735 Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Mon, 22 Jan 2018 10:54:31 -0800 Subject: [PATCH 04/29] handle all possible port configurations when creating dialers --- p2p/net/reuseport/dial.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/p2p/net/reuseport/dial.go b/p2p/net/reuseport/dial.go index 0fa6e99b..a16b5b2b 100644 --- a/p2p/net/reuseport/dial.go +++ b/p2p/net/reuseport/dial.go @@ -87,10 +87,13 @@ func (n *network) makeDialer(network string) dialer { // How many ports are we listening on. var port = 0 for l := range n.listeners { - if port == 0 { - port = l.Addr().(*net.TCPAddr).Port - } else { - // > 1 + newPort := l.Addr().(*net.TCPAddr).Port + switch { + case newPort == 0: // Any port, ignore (really, we shouldn't get this case...). + case port == 0: // Haven't selected a port yet, choose this one. + port = newPort + case newPort == port: // Same as the selected port, continue... + default: // Multiple ports, use the multi dialer return newMultiDialer(unspec, n.listeners) } } From 1ad436a818c4046024325f9e7ea5ac3f0e0a437c Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Mon, 22 Jan 2018 10:55:09 -0800 Subject: [PATCH 05/29] fix incorrect condition in reuseDial --- p2p/net/reuseport/reuseport.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/p2p/net/reuseport/reuseport.go b/p2p/net/reuseport/reuseport.go index c85f1180..307f4ae4 100644 --- a/p2p/net/reuseport/reuseport.go +++ b/p2p/net/reuseport/reuseport.go @@ -50,12 +50,12 @@ func reuseDial(ctx context.Context, laddr *net.TCPAddr, network, raddr string) ( } con, err := d.DialContext(ctx, network, raddr) - if err != nil { - return con, err + if err == nil { + return con, nil } if ReuseErrShouldRetry(err) && ctx.Err() == nil { - log.Debug("failed to reuse port, dialing with a random port") + log.Debugf("failed to reuse port, dialing with a random port: %s", err) var d net.Dialer con, err = d.DialContext(ctx, network, raddr) } From a88cd13c3f7334722acadefdd53904c4156d85fc Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Fri, 9 Mar 2018 22:11:41 -0800 Subject: [PATCH 06/29] unexport ReuseErrShouldRetry There's no reason a consumer of this package would use this error as *we* retry internally. Exporting it will just confuse users. --- p2p/net/reuseport/reuseport.go | 6 +++--- p2p/net/reuseport/reuseport_test.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/p2p/net/reuseport/reuseport.go b/p2p/net/reuseport/reuseport.go index 307f4ae4..1d9602e4 100644 --- a/p2p/net/reuseport/reuseport.go +++ b/p2p/net/reuseport/reuseport.go @@ -8,10 +8,10 @@ import ( reuseport "github.com/libp2p/go-reuseport" ) -// ReuseErrShouldRetry diagnoses whether to retry after a reuse error. +// reuseErrShouldRetry diagnoses whether to retry after a reuse error. // if we failed to bind, we should retry. if bind worked and this is a // real dial error (remote end didnt answer) then we should not retry. -func ReuseErrShouldRetry(err error) bool { +func reuseErrShouldRetry(err error) bool { if err == nil { return false // hey, it worked! no need to retry. } @@ -54,7 +54,7 @@ func reuseDial(ctx context.Context, laddr *net.TCPAddr, network, raddr string) ( return con, nil } - if ReuseErrShouldRetry(err) && ctx.Err() == nil { + if reuseErrShouldRetry(err) && ctx.Err() == nil { log.Debugf("failed to reuse port, dialing with a random port: %s", err) var d net.Dialer con, err = d.DialContext(ctx, network, raddr) diff --git a/p2p/net/reuseport/reuseport_test.go b/p2p/net/reuseport/reuseport_test.go index a4d8b1e2..b0ee4de6 100644 --- a/p2p/net/reuseport/reuseport_test.go +++ b/p2p/net/reuseport/reuseport_test.go @@ -40,7 +40,7 @@ func TestReuseError(t *testing.T) { } for k, v := range cases { - if ReuseErrShouldRetry(k) != v { + if reuseErrShouldRetry(k) != v { t.Fatalf("expected %t for %#v", v, k) } } From 1af0f4224ccb650592e96fe2f5ee188f0ab0c30d Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Tue, 27 Mar 2018 13:07:48 -0700 Subject: [PATCH 07/29] document source port choosing algorithm --- p2p/net/reuseport/multidialer.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/p2p/net/reuseport/multidialer.go b/p2p/net/reuseport/multidialer.go index b8f4129f..7098637f 100644 --- a/p2p/net/reuseport/multidialer.go +++ b/p2p/net/reuseport/multidialer.go @@ -30,6 +30,28 @@ func (d *multiDialer) DialContext(ctx context.Context, network, addr string) (ne return nil, err } + // We pick the source *port* based on the following algorithm. + // + // 1. If we're dialing loopback, choose a source-port in order of + // preference: + // 1. A port in-use by an explicit loopback listener. + // 2. A port in-use by a listener on an unspecified address (must + // also be listening on localhost). + // 3. A port in-use by a listener on a global address. We don't have + // any other better options (other than picking a random port). + // 2. If we're dialing a global address, choose a source-port in order + // of preference: + // 1. A port in-use by a listener on an unspecified address (the most + // general case). + // 2. A port in-use by a listener on the global address. + // 3. Fail on link-local dials (go-ipfs currently forbids this and I + // figured we could try lifting this restriction later). + // + // + // Note: We *always* dial from the unspecified address (regardless of + // the port we pick). In the future, we could use netlink (on Linux) to + // figure out the right source address but we're going to punt on that. + ip := tcpAddr.IP source := d.global switch { From 0b6d56b5e46bb74c8a8c63e9f36f295c3474ddcf Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Tue, 27 Mar 2018 13:07:56 -0700 Subject: [PATCH 08/29] define a global fallbackDialer indirectly addresses: https://github.com/libp2p/go-reuseport-transport/issues/1#issuecomment-376625179 --- p2p/net/reuseport/reuseport.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/p2p/net/reuseport/reuseport.go b/p2p/net/reuseport/reuseport.go index 1d9602e4..47ceac22 100644 --- a/p2p/net/reuseport/reuseport.go +++ b/p2p/net/reuseport/reuseport.go @@ -8,6 +8,8 @@ import ( reuseport "github.com/libp2p/go-reuseport" ) +var fallbackDialer net.Dialer + // reuseErrShouldRetry diagnoses whether to retry after a reuse error. // if we failed to bind, we should retry. if bind worked and this is a // real dial error (remote end didnt answer) then we should not retry. @@ -39,8 +41,7 @@ func reuseErrShouldRetry(err error) bool { // Dials using reusport and then redials normally if that fails. func reuseDial(ctx context.Context, laddr *net.TCPAddr, network, raddr string) (net.Conn, error) { if laddr == nil { - var d net.Dialer - return d.DialContext(ctx, network, raddr) + return fallbackDialer.DialContext(ctx, network, raddr) } d := reuseport.Dialer{ @@ -56,8 +57,7 @@ func reuseDial(ctx context.Context, laddr *net.TCPAddr, network, raddr string) ( if reuseErrShouldRetry(err) && ctx.Err() == nil { log.Debugf("failed to reuse port, dialing with a random port: %s", err) - var d net.Dialer - con, err = d.DialContext(ctx, network, raddr) + con, err = fallbackDialer.DialContext(ctx, network, raddr) } return con, err } From 2f5a5fe89afb20d6334d948d61802dabab8f559e Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Thu, 29 Mar 2018 00:08:11 -0700 Subject: [PATCH 09/29] add another test case --- p2p/net/reuseport/transport_test.go | 48 ++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/p2p/net/reuseport/transport_test.go b/p2p/net/reuseport/transport_test.go index 1b3941f4..2564595f 100644 --- a/p2p/net/reuseport/transport_test.go +++ b/p2p/net/reuseport/transport_test.go @@ -1,12 +1,13 @@ package tcpreuse import ( + "net" "testing" ma "github.com/multiformats/go-multiaddr" ) -func TestAll(t *testing.T) { +func TestSingle(t *testing.T) { var trA Transport var trB Transport laddr, _ := ma.NewMultiaddr("/ip4/127.0.0.1/tcp/0") @@ -38,3 +39,48 @@ func TestAll(t *testing.T) { <-done c.Close() } + +func TestTwoLocal(t *testing.T) { + var trA Transport + var trB Transport + laddr, _ := ma.NewMultiaddr("/ip4/127.0.0.1/tcp/0") + listenerA, err := trA.Listen(laddr) + if err != nil { + t.Fatal(err) + } + defer listenerA.Close() + + listenerB1, err := trB.Listen(laddr) + if err != nil { + t.Fatal(err) + } + defer listenerB1.Close() + + listenerB2, err := trB.Listen(laddr) + if err != nil { + t.Fatal(err) + } + defer listenerB2.Close() + + done := make(chan struct{}) + go func() { + defer close(done) + c, err := listenerA.Accept() + if err != nil { + t.Fatal(err) + } + c.Close() + }() + + c, err := trB.Dial(listenerA.Multiaddr()) + if err != nil { + t.Fatal(err) + } + localPort := c.LocalAddr().(*net.TCPAddr).Port + if localPort != listenerB1.Addr().(*net.TCPAddr).Port && + localPort != listenerB2.Addr().(*net.TCPAddr).Port { + t.Fatal("didn't dial from one of our listener ports") + } + <-done + c.Close() +} From 95f137a647c33a34a97d836982193175ebd48716 Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Thu, 29 Mar 2018 00:14:19 -0700 Subject: [PATCH 10/29] reset the dialer (which ports to dial from) when closing a listener --- p2p/net/reuseport/listen.go | 1 + 1 file changed, 1 insertion(+) diff --git a/p2p/net/reuseport/listen.go b/p2p/net/reuseport/listen.go index 04bd01b5..7b2a4c39 100644 --- a/p2p/net/reuseport/listen.go +++ b/p2p/net/reuseport/listen.go @@ -16,6 +16,7 @@ type listener struct { func (l *listener) Close() error { l.network.mu.Lock() delete(l.network.listeners, l) + l.network.dialer = nil l.network.mu.Unlock() return l.Listener.Close() } From bf8af04add48645254b4a224106f8cda54a5d96a Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Thu, 29 Mar 2018 00:14:24 -0700 Subject: [PATCH 11/29] add a test for dialing with a local and an unspecified listener --- p2p/net/reuseport/transport_test.go | 71 +++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/p2p/net/reuseport/transport_test.go b/p2p/net/reuseport/transport_test.go index 2564595f..44d90b39 100644 --- a/p2p/net/reuseport/transport_test.go +++ b/p2p/net/reuseport/transport_test.go @@ -84,3 +84,74 @@ func TestTwoLocal(t *testing.T) { <-done c.Close() } + +func TestLocalAndUnspecified(t *testing.T) { + var trA Transport + var trB Transport + laddr, _ := ma.NewMultiaddr("/ip4/127.0.0.1/tcp/0") + unspec, _ := ma.NewMultiaddr("/ip4/0.0.0.0/tcp/0") + listenerA, err := trA.Listen(laddr) + if err != nil { + t.Fatal(err) + } + defer listenerA.Close() + + listenerB1, err := trB.Listen(laddr) + if err != nil { + t.Fatal(err) + } + defer listenerB1.Close() + + listenerB2, err := trB.Listen(unspec) + if err != nil { + t.Fatal(err) + } + defer listenerB2.Close() + + done := make(chan struct{}) + go func() { + defer close(done) + c, err := listenerA.Accept() + if err != nil { + t.Fatal(err) + } + c.Close() + }() + + c, err := trB.Dial(listenerA.Multiaddr()) + if err != nil { + t.Fatal(err) + } + actual := c.LocalAddr().(*net.TCPAddr).Port + expected := listenerB1.Addr().(*net.TCPAddr).Port + if actual != expected { + t.Errorf("expected to use port %d, used port %d", expected, actual) + } + <-done + c.Close() + + // Closing the listener should reset the dialer. + listenerB1.Close() + + done = make(chan struct{}) + go func() { + defer close(done) + c, err := listenerA.Accept() + if err != nil { + t.Fatal(err) + } + c.Close() + }() + + c, err = trB.Dial(listenerA.Multiaddr()) + if err != nil { + t.Fatal(err) + } + actual = c.LocalAddr().(*net.TCPAddr).Port + expected = listenerB2.Addr().(*net.TCPAddr).Port + if actual != expected { + t.Errorf("expected to use port %d, used port %d", expected, actual) + } + <-done + c.Close() +} From 9eafb986cf7df5c75e13d6d869a7a1a3b829c3b1 Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Thu, 29 Mar 2018 00:18:10 -0700 Subject: [PATCH 12/29] test dialing with no listeners --- p2p/net/reuseport/transport_test.go | 35 ++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/p2p/net/reuseport/transport_test.go b/p2p/net/reuseport/transport_test.go index 44d90b39..4b155035 100644 --- a/p2p/net/reuseport/transport_test.go +++ b/p2p/net/reuseport/transport_test.go @@ -7,7 +7,7 @@ import ( ma "github.com/multiformats/go-multiaddr" ) -func TestSingle(t *testing.T) { +func TestNoneAndSingle(t *testing.T) { var trA Transport var trB Transport laddr, _ := ma.NewMultiaddr("/ip4/127.0.0.1/tcp/0") @@ -16,11 +16,6 @@ func TestSingle(t *testing.T) { t.Fatal(err) } defer listenerA.Close() - listenerB, err := trB.Listen(laddr) - if err != nil { - t.Fatal(err) - } - defer listenerB.Close() done := make(chan struct{}) go func() { @@ -38,6 +33,34 @@ func TestSingle(t *testing.T) { } <-done c.Close() + + listenerB, err := trB.Listen(laddr) + if err != nil { + t.Fatal(err) + } + defer listenerB.Close() + + done = make(chan struct{}) + go func() { + defer close(done) + c, err := listenerA.Accept() + if err != nil { + t.Fatal(err) + } + c.Close() + }() + + c, err = trB.Dial(listenerA.Multiaddr()) + if err != nil { + t.Fatal(err) + } + actual := c.LocalAddr().(*net.TCPAddr).Port + expected := listenerB.Addr().(*net.TCPAddr).Port + if actual != expected { + t.Errorf("expected to use port %d, used port %d", expected, actual) + } + <-done + c.Close() } func TestTwoLocal(t *testing.T) { From bd345d7a98c4dbb19822d93d24d19d16911bd53d Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Thu, 29 Mar 2018 00:55:57 -0700 Subject: [PATCH 13/29] fix use of global address condition --- p2p/net/reuseport/multidialer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/net/reuseport/multidialer.go b/p2p/net/reuseport/multidialer.go index 7098637f..c7d388c1 100644 --- a/p2p/net/reuseport/multidialer.go +++ b/p2p/net/reuseport/multidialer.go @@ -94,7 +94,7 @@ func newMultiDialer(unspec net.IP, listeners map[*listener]struct{}) dialer { // find. // // TODO: Port priority? Addr priority? - if m.global != nil { + if m.global == nil { m.global = &net.TCPAddr{ IP: unspec, Port: laddr.Port, From 910deaeb2f8f5af0f25281faa9a34ba58b5ba82f Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Thu, 29 Mar 2018 01:02:43 -0700 Subject: [PATCH 14/29] more tests --- p2p/net/reuseport/transport_test.go | 271 +++++++++++++++++----------- 1 file changed, 162 insertions(+), 109 deletions(-) diff --git a/p2p/net/reuseport/transport_test.go b/p2p/net/reuseport/transport_test.go index 4b155035..e78d8c82 100644 --- a/p2p/net/reuseport/transport_test.go +++ b/p2p/net/reuseport/transport_test.go @@ -5,176 +5,229 @@ import ( "testing" ma "github.com/multiformats/go-multiaddr" + manet "github.com/multiformats/go-multiaddr-net" ) +var loopback, _ = ma.NewMultiaddr("/ip4/127.0.0.1/tcp/0") +var unspec, _ = ma.NewMultiaddr("/ip4/0.0.0.0/tcp/0") + +var global ma.Multiaddr + +func init() { + addrs, err := manet.InterfaceMultiaddrs() + if err != nil { + return + } + for _, addr := range addrs { + if !manet.IsIP6LinkLocal(addr) && !manet.IsIPLoopback(addr) { + tcp, _ := ma.NewMultiaddr("/tcp/0") + global = addr.Encapsulate(tcp) + return + } + } +} + +func acceptOne(t *testing.T, listener manet.Listener) <-chan struct{} { + t.Helper() + done := make(chan struct{}) + go func() { + defer close(done) + c, err := listener.Accept() + if err != nil { + t.Error(err) + return + } + c.Close() + }() + return done +} + +func dialOne(t *testing.T, tr *Transport, listener manet.Listener, expected ...int) int { + t.Helper() + + done := acceptOne(t, listener) + c, err := tr.Dial(listener.Multiaddr()) + if err != nil { + t.Fatal(err) + } + port := c.LocalAddr().(*net.TCPAddr).Port + <-done + c.Close() + if len(expected) == 0 { + return port + } + for _, p := range expected { + if p == port { + return port + } + } + t.Errorf("dialed from %d, expected to dial from one of %v", port, expected) + return 0 +} + func TestNoneAndSingle(t *testing.T) { var trA Transport var trB Transport - laddr, _ := ma.NewMultiaddr("/ip4/127.0.0.1/tcp/0") - listenerA, err := trA.Listen(laddr) + listenerA, err := trA.Listen(loopback) if err != nil { t.Fatal(err) } defer listenerA.Close() - done := make(chan struct{}) - go func() { - defer close(done) - c, err := listenerA.Accept() - if err != nil { - t.Fatal(err) - } - c.Close() - }() + dialOne(t, &trB, listenerA) - c, err := trB.Dial(listenerA.Multiaddr()) - if err != nil { - t.Fatal(err) - } - <-done - c.Close() - - listenerB, err := trB.Listen(laddr) + listenerB, err := trB.Listen(loopback) if err != nil { t.Fatal(err) } defer listenerB.Close() - done = make(chan struct{}) - go func() { - defer close(done) - c, err := listenerA.Accept() - if err != nil { - t.Fatal(err) - } - c.Close() - }() - - c, err = trB.Dial(listenerA.Multiaddr()) - if err != nil { - t.Fatal(err) - } - actual := c.LocalAddr().(*net.TCPAddr).Port - expected := listenerB.Addr().(*net.TCPAddr).Port - if actual != expected { - t.Errorf("expected to use port %d, used port %d", expected, actual) - } - <-done - c.Close() + dialOne(t, &trB, listenerA, listenerB.Addr().(*net.TCPAddr).Port) } func TestTwoLocal(t *testing.T) { var trA Transport var trB Transport - laddr, _ := ma.NewMultiaddr("/ip4/127.0.0.1/tcp/0") - listenerA, err := trA.Listen(laddr) + listenerA, err := trA.Listen(loopback) if err != nil { t.Fatal(err) } defer listenerA.Close() - listenerB1, err := trB.Listen(laddr) + listenerB1, err := trB.Listen(loopback) if err != nil { t.Fatal(err) } defer listenerB1.Close() - listenerB2, err := trB.Listen(laddr) + listenerB2, err := trB.Listen(loopback) if err != nil { t.Fatal(err) } defer listenerB2.Close() - done := make(chan struct{}) - go func() { - defer close(done) - c, err := listenerA.Accept() - if err != nil { - t.Fatal(err) - } - c.Close() - }() - - c, err := trB.Dial(listenerA.Multiaddr()) - if err != nil { - t.Fatal(err) - } - localPort := c.LocalAddr().(*net.TCPAddr).Port - if localPort != listenerB1.Addr().(*net.TCPAddr).Port && - localPort != listenerB2.Addr().(*net.TCPAddr).Port { - t.Fatal("didn't dial from one of our listener ports") - } - <-done - c.Close() + dialOne(t, &trB, listenerA, + listenerB1.Addr().(*net.TCPAddr).Port, + listenerB2.Addr().(*net.TCPAddr).Port) } -func TestLocalAndUnspecified(t *testing.T) { +func TestGlobalPreference(t *testing.T) { + if global == nil { + t.Skip("no global addresses configured") + return + } + testPrefer(t, loopback, loopback, global) + testPrefer(t, loopback, unspec, global) + + testPrefer(t, global, unspec, global) + testPrefer(t, global, unspec, loopback) +} + +func TestLoopbackPreference(t *testing.T) { + testPrefer(t, loopback, loopback, unspec) +} + +func testPrefer(t *testing.T, listen, prefer, avoid ma.Multiaddr) { var trA Transport var trB Transport - laddr, _ := ma.NewMultiaddr("/ip4/127.0.0.1/tcp/0") - unspec, _ := ma.NewMultiaddr("/ip4/0.0.0.0/tcp/0") - listenerA, err := trA.Listen(laddr) + listenerA, err := trA.Listen(listen) if err != nil { t.Fatal(err) } defer listenerA.Close() - listenerB1, err := trB.Listen(laddr) + listenerB1, err := trB.Listen(avoid) if err != nil { t.Fatal(err) } defer listenerB1.Close() - listenerB2, err := trB.Listen(unspec) + dialOne(t, &trB, listenerA, listenerB1.Addr().(*net.TCPAddr).Port) + + listenerB2, err := trB.Listen(prefer) if err != nil { t.Fatal(err) } defer listenerB2.Close() - done := make(chan struct{}) - go func() { - defer close(done) - c, err := listenerA.Accept() - if err != nil { - t.Fatal(err) - } - c.Close() - }() - - c, err := trB.Dial(listenerA.Multiaddr()) - if err != nil { - t.Fatal(err) - } - actual := c.LocalAddr().(*net.TCPAddr).Port - expected := listenerB1.Addr().(*net.TCPAddr).Port - if actual != expected { - t.Errorf("expected to use port %d, used port %d", expected, actual) - } - <-done - c.Close() + dialOne(t, &trB, listenerA, listenerB2.Addr().(*net.TCPAddr).Port) // Closing the listener should reset the dialer. - listenerB1.Close() + listenerB2.Close() - done = make(chan struct{}) - go func() { - defer close(done) - c, err := listenerA.Accept() - if err != nil { - t.Fatal(err) - } - c.Close() - }() + dialOne(t, &trB, listenerA, listenerB1.Addr().(*net.TCPAddr).Port) +} - c, err = trB.Dial(listenerA.Multiaddr()) +func TestGlobalToGlobal(t *testing.T) { + if global == nil { + t.Skip("no global addresses configured") + return + } + + var trA Transport + var trB Transport + listenerA, err := trA.Listen(global) if err != nil { t.Fatal(err) } - actual = c.LocalAddr().(*net.TCPAddr).Port - expected = listenerB2.Addr().(*net.TCPAddr).Port - if actual != expected { - t.Errorf("expected to use port %d, used port %d", expected, actual) + defer listenerA.Close() + + listenerB1, err := trB.Listen(loopback) + if err != nil { + t.Fatal(err) + } + defer listenerB1.Close() + + // It works (random port) + dialOne(t, &trB, listenerA) + + listenerB2, err := trB.Listen(global) + if err != nil { + t.Fatal(err) + } + defer listenerB2.Close() + + // Uses global port. + dialOne(t, &trB, listenerA, listenerB2.Addr().(*net.TCPAddr).Port) + + // Closing the listener should reset the dialer. + listenerB2.Close() + + // It still works. + dialOne(t, &trB, listenerA) +} + +func TestDuplicateGlobal(t *testing.T) { + if global == nil { + t.Skip("no global addresses configured") + return + } + + var trA Transport + var trB Transport + listenerA, err := trA.Listen(global) + if err != nil { + t.Fatal(err) + } + defer listenerA.Close() + + listenerB1, err := trB.Listen(global) + if err != nil { + t.Fatal(err) + } + defer listenerB1.Close() + + listenerB2, err := trB.Listen(global) + if err != nil { + t.Fatal(err) + } + defer listenerB2.Close() + + // Check which port we're using + port := dialOne(t, &trB, listenerA) + + // Check consistency + for i := 0; i < 10; i++ { + dialOne(t, &trB, listenerA, port) } - <-done - c.Close() } From 33bd60aa147f336e07db02e711e5b65f44608694 Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Thu, 29 Mar 2018 01:14:22 -0700 Subject: [PATCH 15/29] test ipv6 --- p2p/net/reuseport/transport_test.go | 91 ++++++++++++++++++++--------- 1 file changed, 62 insertions(+), 29 deletions(-) diff --git a/p2p/net/reuseport/transport_test.go b/p2p/net/reuseport/transport_test.go index e78d8c82..942019b0 100644 --- a/p2p/net/reuseport/transport_test.go +++ b/p2p/net/reuseport/transport_test.go @@ -8,10 +8,13 @@ import ( manet "github.com/multiformats/go-multiaddr-net" ) -var loopback, _ = ma.NewMultiaddr("/ip4/127.0.0.1/tcp/0") -var unspec, _ = ma.NewMultiaddr("/ip4/0.0.0.0/tcp/0") +var loopbackV4, _ = ma.NewMultiaddr("/ip4/127.0.0.1/tcp/0") +var loopbackV6, _ = ma.NewMultiaddr("/ip6/::1/tcp/0") +var unspecV6, _ = ma.NewMultiaddr("/ip6/::/tcp/0") +var unspecV4, _ = ma.NewMultiaddr("/ip4/0.0.0.0/tcp/0") -var global ma.Multiaddr +var globalV4 ma.Multiaddr +var globalV6 ma.Multiaddr func init() { addrs, err := manet.InterfaceMultiaddrs() @@ -21,8 +24,16 @@ func init() { for _, addr := range addrs { if !manet.IsIP6LinkLocal(addr) && !manet.IsIPLoopback(addr) { tcp, _ := ma.NewMultiaddr("/tcp/0") - global = addr.Encapsulate(tcp) - return + switch addr.Protocols()[0].Code { + case ma.P_IP4: + if globalV4 == nil { + globalV4 = addr.Encapsulate(tcp) + } + case ma.P_IP6: + if globalV6 == nil { + globalV6 = addr.Encapsulate(tcp) + } + } } } } @@ -68,7 +79,7 @@ func dialOne(t *testing.T, tr *Transport, listener manet.Listener, expected ...i func TestNoneAndSingle(t *testing.T) { var trA Transport var trB Transport - listenerA, err := trA.Listen(loopback) + listenerA, err := trA.Listen(loopbackV4) if err != nil { t.Fatal(err) } @@ -76,7 +87,7 @@ func TestNoneAndSingle(t *testing.T) { dialOne(t, &trB, listenerA) - listenerB, err := trB.Listen(loopback) + listenerB, err := trB.Listen(loopbackV4) if err != nil { t.Fatal(err) } @@ -88,19 +99,19 @@ func TestNoneAndSingle(t *testing.T) { func TestTwoLocal(t *testing.T) { var trA Transport var trB Transport - listenerA, err := trA.Listen(loopback) + listenerA, err := trA.Listen(loopbackV4) if err != nil { t.Fatal(err) } defer listenerA.Close() - listenerB1, err := trB.Listen(loopback) + listenerB1, err := trB.Listen(loopbackV4) if err != nil { t.Fatal(err) } defer listenerB1.Close() - listenerB2, err := trB.Listen(loopback) + listenerB2, err := trB.Listen(loopbackV4) if err != nil { t.Fatal(err) } @@ -111,20 +122,33 @@ func TestTwoLocal(t *testing.T) { listenerB2.Addr().(*net.TCPAddr).Port) } -func TestGlobalPreference(t *testing.T) { - if global == nil { - t.Skip("no global addresses configured") +func TestGlobalPreferenceV4(t *testing.T) { + if globalV4 == nil { + t.Skip("no global IPv4 addresses configured") return } - testPrefer(t, loopback, loopback, global) - testPrefer(t, loopback, unspec, global) + testPrefer(t, loopbackV4, loopbackV4, globalV4) + testPrefer(t, loopbackV4, unspecV4, globalV4) - testPrefer(t, global, unspec, global) - testPrefer(t, global, unspec, loopback) + testPrefer(t, globalV4, unspecV4, globalV4) + testPrefer(t, globalV4, unspecV4, loopbackV4) +} + +func TestGlobalPreferenceV6(t *testing.T) { + if globalV6 == nil { + t.Skip("no global IPv6 addresses configured") + return + } + testPrefer(t, loopbackV6, loopbackV6, globalV6) + testPrefer(t, loopbackV6, unspecV6, globalV6) + + testPrefer(t, globalV6, unspecV6, globalV6) + testPrefer(t, globalV6, unspecV6, loopbackV6) } func TestLoopbackPreference(t *testing.T) { - testPrefer(t, loopback, loopback, unspec) + testPrefer(t, loopbackV4, loopbackV4, unspecV4) + testPrefer(t, loopbackV6, loopbackV6, unspecV6) } func testPrefer(t *testing.T, listen, prefer, avoid ma.Multiaddr) { @@ -158,21 +182,30 @@ func testPrefer(t *testing.T, listen, prefer, avoid ma.Multiaddr) { dialOne(t, &trB, listenerA, listenerB1.Addr().(*net.TCPAddr).Port) } +func TestV6V4(t *testing.T) { + testUseFirst(t, loopbackV4, loopbackV4, loopbackV6) + testUseFirst(t, loopbackV6, loopbackV6, loopbackV4) +} + func TestGlobalToGlobal(t *testing.T) { - if global == nil { - t.Skip("no global addresses configured") + if globalV4 == nil { + t.Skip("no globalV4 addresses configured") return } + testUseFirst(t, globalV4, globalV4, loopbackV4) + testUseFirst(t, globalV6, globalV6, loopbackV6) +} +func testUseFirst(t *testing.T, listen, use, never ma.Multiaddr) { var trA Transport var trB Transport - listenerA, err := trA.Listen(global) + listenerA, err := trA.Listen(globalV4) if err != nil { t.Fatal(err) } defer listenerA.Close() - listenerB1, err := trB.Listen(loopback) + listenerB1, err := trB.Listen(loopbackV4) if err != nil { t.Fatal(err) } @@ -181,13 +214,13 @@ func TestGlobalToGlobal(t *testing.T) { // It works (random port) dialOne(t, &trB, listenerA) - listenerB2, err := trB.Listen(global) + listenerB2, err := trB.Listen(globalV4) if err != nil { t.Fatal(err) } defer listenerB2.Close() - // Uses global port. + // Uses globalV4 port. dialOne(t, &trB, listenerA, listenerB2.Addr().(*net.TCPAddr).Port) // Closing the listener should reset the dialer. @@ -198,26 +231,26 @@ func TestGlobalToGlobal(t *testing.T) { } func TestDuplicateGlobal(t *testing.T) { - if global == nil { - t.Skip("no global addresses configured") + if globalV4 == nil { + t.Skip("no globalV4 addresses configured") return } var trA Transport var trB Transport - listenerA, err := trA.Listen(global) + listenerA, err := trA.Listen(globalV4) if err != nil { t.Fatal(err) } defer listenerA.Close() - listenerB1, err := trB.Listen(global) + listenerB1, err := trB.Listen(globalV4) if err != nil { t.Fatal(err) } defer listenerB1.Close() - listenerB2, err := trB.Listen(global) + listenerB2, err := trB.Listen(globalV4) if err != nil { t.Fatal(err) } From 440615fe4e6edac9b01968c36365e9c69f41ebe6 Mon Sep 17 00:00:00 2001 From: Matt Joiner Date: Mon, 31 Dec 2018 14:17:30 +1100 Subject: [PATCH 16/29] Use new go-reuseport.Control --- p2p/net/reuseport/reuseport.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/p2p/net/reuseport/reuseport.go b/p2p/net/reuseport/reuseport.go index 47ceac22..35e8a85c 100644 --- a/p2p/net/reuseport/reuseport.go +++ b/p2p/net/reuseport/reuseport.go @@ -44,10 +44,9 @@ func reuseDial(ctx context.Context, laddr *net.TCPAddr, network, raddr string) ( return fallbackDialer.DialContext(ctx, network, raddr) } - d := reuseport.Dialer{ - D: net.Dialer{ - LocalAddr: laddr, - }, + d := net.Dialer{ + LocalAddr: laddr, + Control: reuseport.Control, } con, err := d.DialContext(ctx, network, raddr) From 8b89db36a5082fb37aca1cbaba815c0eace85a79 Mon Sep 17 00:00:00 2001 From: Matt Joiner Date: Wed, 2 Jan 2019 17:43:57 +1100 Subject: [PATCH 17/29] Add SetLinger(0) in reuseDial No longer implicitly provided by go-reuseport's Dialer as it was removed. --- p2p/net/reuseport/reuseport.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/p2p/net/reuseport/reuseport.go b/p2p/net/reuseport/reuseport.go index 35e8a85c..dd271d11 100644 --- a/p2p/net/reuseport/reuseport.go +++ b/p2p/net/reuseport/reuseport.go @@ -38,8 +38,9 @@ func reuseErrShouldRetry(err error) bool { } } -// Dials using reusport and then redials normally if that fails. -func reuseDial(ctx context.Context, laddr *net.TCPAddr, network, raddr string) (net.Conn, error) { +// Dials using reuseport and then redials normally if that fails. +// TODO(anacrolix): This shouldn't fail anymore: Remove fallback. +func reuseDial(ctx context.Context, laddr *net.TCPAddr, network, raddr string) (con net.Conn, err error) { if laddr == nil { return fallbackDialer.DialContext(ctx, network, raddr) } @@ -48,8 +49,17 @@ func reuseDial(ctx context.Context, laddr *net.TCPAddr, network, raddr string) ( LocalAddr: laddr, Control: reuseport.Control, } + defer func() { + if err != nil { + return + } + // This is transplanted from go-reuseport, which once set no linger on + // dialing and may be a requirement for desired behaviour in this + // package. + con.(*net.TCPConn).SetLinger(0) + }() - con, err := d.DialContext(ctx, network, raddr) + con, err = d.DialContext(ctx, network, raddr) if err == nil { return con, nil } From 5abb5b9ff06139d46e1ed28024b32cb74203f079 Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Wed, 3 Apr 2019 19:26:55 -0700 Subject: [PATCH 18/29] don't set linger to 0 We should do this from go-tcp-transport (https://github.com/libp2p/go-tcp-transport/pull/36), not here, as setting linger to 0 will cause us to send RST packets instead of nicely closing connections. Also, document why we need the fallback dialer. --- p2p/net/reuseport/reuseport.go | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/p2p/net/reuseport/reuseport.go b/p2p/net/reuseport/reuseport.go index dd271d11..82f9f37c 100644 --- a/p2p/net/reuseport/reuseport.go +++ b/p2p/net/reuseport/reuseport.go @@ -39,7 +39,6 @@ func reuseErrShouldRetry(err error) bool { } // Dials using reuseport and then redials normally if that fails. -// TODO(anacrolix): This shouldn't fail anymore: Remove fallback. func reuseDial(ctx context.Context, laddr *net.TCPAddr, network, raddr string) (con net.Conn, err error) { if laddr == nil { return fallbackDialer.DialContext(ctx, network, raddr) @@ -49,15 +48,6 @@ func reuseDial(ctx context.Context, laddr *net.TCPAddr, network, raddr string) ( LocalAddr: laddr, Control: reuseport.Control, } - defer func() { - if err != nil { - return - } - // This is transplanted from go-reuseport, which once set no linger on - // dialing and may be a requirement for desired behaviour in this - // package. - con.(*net.TCPConn).SetLinger(0) - }() con, err = d.DialContext(ctx, network, raddr) if err == nil { @@ -65,6 +55,8 @@ func reuseDial(ctx context.Context, laddr *net.TCPAddr, network, raddr string) ( } if reuseErrShouldRetry(err) && ctx.Err() == nil { + // We could have an existing socket open or we could have one + // stuck in TIME-WAIT. log.Debugf("failed to reuse port, dialing with a random port: %s", err) con, err = fallbackDialer.DialContext(ctx, network, raddr) } From ba298b7231e6f08176fcca5fb381efec1879fddd Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Fri, 21 Feb 2020 18:30:15 -0500 Subject: [PATCH 19/29] fix: less confusing log message fixes https://github.com/ipfs/go-ipfs/issues/6922 --- p2p/net/reuseport/reuseport.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/net/reuseport/reuseport.go b/p2p/net/reuseport/reuseport.go index 82f9f37c..eb14068f 100644 --- a/p2p/net/reuseport/reuseport.go +++ b/p2p/net/reuseport/reuseport.go @@ -57,7 +57,7 @@ func reuseDial(ctx context.Context, laddr *net.TCPAddr, network, raddr string) ( if reuseErrShouldRetry(err) && ctx.Err() == nil { // We could have an existing socket open or we could have one // stuck in TIME-WAIT. - log.Debugf("failed to reuse port, dialing with a random port: %s", err) + log.Debugf("failed to reuse port, will try again with a random port: %s", err) con, err = fallbackDialer.DialContext(ctx, network, raddr) } return con, err From 0c848886fc608fa782ea07bbc2a95116d3c06761 Mon Sep 17 00:00:00 2001 From: Will Date: Thu, 2 Apr 2020 13:20:01 -0700 Subject: [PATCH 20/29] Use Netroute (#25) Use OS routing table as a hint when dialing --- p2p/net/reuseport/multidialer.go | 128 ++++++++++++---------------- p2p/net/reuseport/transport_test.go | 13 +-- 2 files changed, 62 insertions(+), 79 deletions(-) diff --git a/p2p/net/reuseport/multidialer.go b/p2p/net/reuseport/multidialer.go index c7d388c1..edb200f8 100644 --- a/p2p/net/reuseport/multidialer.go +++ b/p2p/net/reuseport/multidialer.go @@ -5,12 +5,15 @@ import ( "fmt" "math/rand" "net" + + "github.com/libp2p/go-netroute" ) type multiDialer struct { - loopback []*net.TCPAddr - unspecified []*net.TCPAddr - global *net.TCPAddr + listeningAddresses []*net.TCPAddr + loopback []*net.TCPAddr + unspecified []*net.TCPAddr + fallback net.TCPAddr } func (d *multiDialer) Dial(network, addr string) (net.Conn, error) { @@ -24,87 +27,64 @@ func randAddr(addrs []*net.TCPAddr) *net.TCPAddr { return nil } +// DialContext dials a target addr. +// Dialing preference is +// * If there is a listener on the local interface the OS expects to use to route towards addr, use that. +// * If there is a listener on a loopback address, addr is loopback, use that. +// * If there is a listener on an undefined address (0.0.0.0 or ::), use that. +// * Use the fallback IP specified during construction, with a port that's already being listened on, if one exists. func (d *multiDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) { tcpAddr, err := net.ResolveTCPAddr(network, addr) if err != nil { return nil, err } - - // We pick the source *port* based on the following algorithm. - // - // 1. If we're dialing loopback, choose a source-port in order of - // preference: - // 1. A port in-use by an explicit loopback listener. - // 2. A port in-use by a listener on an unspecified address (must - // also be listening on localhost). - // 3. A port in-use by a listener on a global address. We don't have - // any other better options (other than picking a random port). - // 2. If we're dialing a global address, choose a source-port in order - // of preference: - // 1. A port in-use by a listener on an unspecified address (the most - // general case). - // 2. A port in-use by a listener on the global address. - // 3. Fail on link-local dials (go-ipfs currently forbids this and I - // figured we could try lifting this restriction later). - // - // - // Note: We *always* dial from the unspecified address (regardless of - // the port we pick). In the future, we could use netlink (on Linux) to - // figure out the right source address but we're going to punt on that. - ip := tcpAddr.IP - source := d.global - switch { - case ip.IsLoopback(): - switch { - case len(d.loopback) > 0: - source = randAddr(d.loopback) - case len(d.unspecified) > 0: - source = randAddr(d.unspecified) - } - case ip.IsGlobalUnicast(): - switch { - case len(d.unspecified) > 0: - source = randAddr(d.unspecified) - } - default: - return nil, fmt.Errorf("undialable IP: %s", tcpAddr.IP) + if !ip.IsLoopback() && !ip.IsGlobalUnicast() { + return nil, fmt.Errorf("undialable IP: %s", ip) } - return reuseDial(ctx, source, network, addr) + + if router, err := netroute.New(); err == nil { + if _, _, preferredSrc, err := router.Route(ip); err == nil { + for _, optAddr := range d.listeningAddresses { + if optAddr.IP.Equal(preferredSrc) { + return reuseDial(ctx, optAddr, network, addr) + } + } + } + } + + if ip.IsLoopback() && len(d.loopback) > 0 { + return reuseDial(ctx, randAddr(d.loopback), network, addr) + } + if len(d.unspecified) == 0 { + return reuseDial(ctx, &d.fallback, network, addr) + } + + return reuseDial(ctx, randAddr(d.unspecified), network, addr) } -func newMultiDialer(unspec net.IP, listeners map[*listener]struct{}) dialer { - m := new(multiDialer) +func newMultiDialer(unspec net.IP, listeners map[*listener]struct{}) (m dialer) { + addrs := make([]*net.TCPAddr, 0) + loopback := make([]*net.TCPAddr, 0) + unspecified := make([]*net.TCPAddr, 0) + existingPort := 0 + for l := range listeners { - laddr := l.Addr().(*net.TCPAddr) - switch { - case laddr.IP.IsLoopback(): - m.loopback = append(m.loopback, laddr) - case laddr.IP.IsGlobalUnicast(): - // Different global ports? Crap. - // - // The *proper* way to deal with this is to, e.g., use - // netlink to figure out which source address we would - // normally use to dial a destination address and then - // pick one of the ports we're listening on on that - // source address. However, this is a pain in the ass. - // - // Instead, we're just going to always dial from the - // unspecified address with the first global port we - // find. - // - // TODO: Port priority? Addr priority? - if m.global == nil { - m.global = &net.TCPAddr{ - IP: unspec, - Port: laddr.Port, - } - } else { - log.Warning("listening on external interfaces on multiple ports, will dial from %d, not %s", m.global, laddr) - } - case laddr.IP.IsUnspecified(): - m.unspecified = append(m.unspecified, laddr) + addr := l.Addr().(*net.TCPAddr) + addrs = append(addrs, addr) + if addr.IP.IsLoopback() { + loopback = append(loopback, addr) + } else if addr.IP.IsGlobalUnicast() && existingPort == 0 { + existingPort = addr.Port + } else if addr.IP.IsUnspecified() { + unspecified = append(unspecified, addr) } } - return m + m = &multiDialer{ + listeningAddresses: addrs, + loopback: loopback, + unspecified: unspecified, + fallback: net.TCPAddr{IP: unspec, Port: existingPort}, + } + return } diff --git a/p2p/net/reuseport/transport_test.go b/p2p/net/reuseport/transport_test.go index 942019b0..876f71d5 100644 --- a/p2p/net/reuseport/transport_test.go +++ b/p2p/net/reuseport/transport_test.go @@ -38,11 +38,12 @@ func init() { } } -func acceptOne(t *testing.T, listener manet.Listener) <-chan struct{} { +func acceptOne(t *testing.T, listener manet.Listener) <-chan interface{} { t.Helper() - done := make(chan struct{}) + done := make(chan interface{}, 1) go func() { defer close(done) + done <- nil c, err := listener.Accept() if err != nil { t.Error(err) @@ -50,6 +51,7 @@ func acceptOne(t *testing.T, listener manet.Listener) <-chan struct{} { } c.Close() }() + <-done return done } @@ -72,7 +74,7 @@ func dialOne(t *testing.T, tr *Transport, listener manet.Listener, expected ...i return port } } - t.Errorf("dialed from %d, expected to dial from one of %v", port, expected) + t.Errorf("dialed %s from %v. expected to dial from port %v", listener.Multiaddr(), c.LocalAddr(), expected) return 0 } @@ -127,10 +129,12 @@ func TestGlobalPreferenceV4(t *testing.T) { t.Skip("no global IPv4 addresses configured") return } + t.Logf("when listening on %v, should prefer %v over %v", loopbackV4, loopbackV4, globalV4) testPrefer(t, loopbackV4, loopbackV4, globalV4) + t.Logf("when listening on %v, should prefer %v over %v", loopbackV4, unspecV4, globalV4) testPrefer(t, loopbackV4, unspecV4, globalV4) - testPrefer(t, globalV4, unspecV4, globalV4) + t.Logf("when listening on %v, should prefer %v over %v", globalV4, unspecV4, loopbackV4) testPrefer(t, globalV4, unspecV4, loopbackV4) } @@ -142,7 +146,6 @@ func TestGlobalPreferenceV6(t *testing.T) { testPrefer(t, loopbackV6, loopbackV6, globalV6) testPrefer(t, loopbackV6, unspecV6, globalV6) - testPrefer(t, globalV6, unspecV6, globalV6) testPrefer(t, globalV6, unspecV6, loopbackV6) } From 90e1f36ac233515dc6f3b0ac4089ead06acae2c9 Mon Sep 17 00:00:00 2001 From: Fazlul Shahriar Date: Fri, 10 Jul 2020 19:34:57 -0400 Subject: [PATCH 21/29] Fix build on Plan 9 Changes to go.mod has been left out: requires go-netroute@v0.1.3 and go-reuseport@master for Plan 9 support. --- p2p/net/reuseport/reuseport.go | 29 --------------------- p2p/net/reuseport/reuseport_plan9.go | 39 ++++++++++++++++++++++++++++ p2p/net/reuseport/reuseport_posix.go | 36 +++++++++++++++++++++++++ p2p/net/reuseport/reuseport_test.go | 2 ++ 4 files changed, 77 insertions(+), 29 deletions(-) create mode 100644 p2p/net/reuseport/reuseport_plan9.go create mode 100644 p2p/net/reuseport/reuseport_posix.go diff --git a/p2p/net/reuseport/reuseport.go b/p2p/net/reuseport/reuseport.go index eb14068f..4299ddc0 100644 --- a/p2p/net/reuseport/reuseport.go +++ b/p2p/net/reuseport/reuseport.go @@ -3,41 +3,12 @@ package tcpreuse import ( "context" "net" - "syscall" reuseport "github.com/libp2p/go-reuseport" ) var fallbackDialer net.Dialer -// reuseErrShouldRetry diagnoses whether to retry after a reuse error. -// if we failed to bind, we should retry. if bind worked and this is a -// real dial error (remote end didnt answer) then we should not retry. -func reuseErrShouldRetry(err error) bool { - if err == nil { - return false // hey, it worked! no need to retry. - } - - // if it's a network timeout error, it's a legitimate failure. - if nerr, ok := err.(net.Error); ok && nerr.Timeout() { - return false - } - - errno, ok := err.(syscall.Errno) - if !ok { // not an errno? who knows what this is. retry. - return true - } - - switch errno { - case syscall.EADDRINUSE, syscall.EADDRNOTAVAIL: - return true // failure to bind. retry. - case syscall.ECONNREFUSED: - return false // real dial error - default: - return true // optimistically default to retry. - } -} - // Dials using reuseport and then redials normally if that fails. func reuseDial(ctx context.Context, laddr *net.TCPAddr, network, raddr string) (con net.Conn, err error) { if laddr == nil { diff --git a/p2p/net/reuseport/reuseport_plan9.go b/p2p/net/reuseport/reuseport_plan9.go new file mode 100644 index 00000000..fd8aa31f --- /dev/null +++ b/p2p/net/reuseport/reuseport_plan9.go @@ -0,0 +1,39 @@ +package tcpreuse + +import ( + "net" + "os" +) + +// reuseErrShouldRetry diagnoses whether to retry after a reuse error. +// if we failed to bind, we should retry. if bind worked and this is a +// real dial error (remote end didnt answer) then we should not retry. +func reuseErrShouldRetry(err error) bool { + if err == nil { + return false // hey, it worked! no need to retry. + } + + // if it's a network timeout error, it's a legitimate failure. + if nerr, ok := err.(net.Error); ok && nerr.Timeout() { + return false + } + + e, ok := err.(*net.OpError) + if !ok { + return true + } + + e1, ok := e.Err.(*os.PathError) + if !ok { + return true + } + + switch e1.Err.Error() { + case "address in use": + return true + case "connection refused": + return false + default: + return true // optimistically default to retry. + } +} diff --git a/p2p/net/reuseport/reuseport_posix.go b/p2p/net/reuseport/reuseport_posix.go new file mode 100644 index 00000000..bfe7f39d --- /dev/null +++ b/p2p/net/reuseport/reuseport_posix.go @@ -0,0 +1,36 @@ +// +build !plan9 + +package tcpreuse + +import ( + "net" + "syscall" +) + +// reuseErrShouldRetry diagnoses whether to retry after a reuse error. +// if we failed to bind, we should retry. if bind worked and this is a +// real dial error (remote end didnt answer) then we should not retry. +func reuseErrShouldRetry(err error) bool { + if err == nil { + return false // hey, it worked! no need to retry. + } + + // if it's a network timeout error, it's a legitimate failure. + if nerr, ok := err.(net.Error); ok && nerr.Timeout() { + return false + } + + errno, ok := err.(syscall.Errno) + if !ok { // not an errno? who knows what this is. retry. + return true + } + + switch errno { + case syscall.EADDRINUSE, syscall.EADDRNOTAVAIL: + return true // failure to bind. retry. + case syscall.ECONNREFUSED: + return false // real dial error + default: + return true // optimistically default to retry. + } +} diff --git a/p2p/net/reuseport/reuseport_test.go b/p2p/net/reuseport/reuseport_test.go index b0ee4de6..7dbd5616 100644 --- a/p2p/net/reuseport/reuseport_test.go +++ b/p2p/net/reuseport/reuseport_test.go @@ -1,3 +1,5 @@ +// +build !plan9 + package tcpreuse import ( From 2a09bb753794125e929014cf08c431faefeeec32 Mon Sep 17 00:00:00 2001 From: Fazlul Shahriar Date: Fri, 10 Jul 2020 21:31:04 -0400 Subject: [PATCH 22/29] Update go-netroute and go-reuseport for Plan 9 support --- p2p/net/reuseport/reuseport_plan9.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/p2p/net/reuseport/reuseport_plan9.go b/p2p/net/reuseport/reuseport_plan9.go index fd8aa31f..9318652c 100644 --- a/p2p/net/reuseport/reuseport_plan9.go +++ b/p2p/net/reuseport/reuseport_plan9.go @@ -5,6 +5,11 @@ import ( "os" ) +const ( + EADDRINUSE = "address in use" + ECONNREFUSED = "connection refused" +) + // reuseErrShouldRetry diagnoses whether to retry after a reuse error. // if we failed to bind, we should retry. if bind worked and this is a // real dial error (remote end didnt answer) then we should not retry. @@ -29,9 +34,9 @@ func reuseErrShouldRetry(err error) bool { } switch e1.Err.Error() { - case "address in use": + case EADDRINUSE: return true - case "connection refused": + case ECONNREFUSED: return false default: return true // optimistically default to retry. From b304d8d69a7b9f5fb4d9300e020fd3792e04e66c Mon Sep 17 00:00:00 2001 From: Marten Seemann Date: Thu, 1 Jul 2021 19:36:52 -0700 Subject: [PATCH 23/29] stop using the deprecated go-multiaddr-net package --- p2p/net/reuseport/dial.go | 4 ++-- p2p/net/reuseport/listen.go | 4 ++-- p2p/net/reuseport/transport_test.go | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/p2p/net/reuseport/dial.go b/p2p/net/reuseport/dial.go index a16b5b2b..12a70b7e 100644 --- a/p2p/net/reuseport/dial.go +++ b/p2p/net/reuseport/dial.go @@ -4,9 +4,9 @@ import ( "context" "net" - reuseport "github.com/libp2p/go-reuseport" + "github.com/libp2p/go-reuseport" ma "github.com/multiformats/go-multiaddr" - manet "github.com/multiformats/go-multiaddr-net" + manet "github.com/multiformats/go-multiaddr/net" ) type dialer interface { diff --git a/p2p/net/reuseport/listen.go b/p2p/net/reuseport/listen.go index 7b2a4c39..8bb2c402 100644 --- a/p2p/net/reuseport/listen.go +++ b/p2p/net/reuseport/listen.go @@ -3,9 +3,9 @@ package tcpreuse import ( "net" - reuseport "github.com/libp2p/go-reuseport" + "github.com/libp2p/go-reuseport" ma "github.com/multiformats/go-multiaddr" - manet "github.com/multiformats/go-multiaddr-net" + manet "github.com/multiformats/go-multiaddr/net" ) type listener struct { diff --git a/p2p/net/reuseport/transport_test.go b/p2p/net/reuseport/transport_test.go index 876f71d5..5d779467 100644 --- a/p2p/net/reuseport/transport_test.go +++ b/p2p/net/reuseport/transport_test.go @@ -5,7 +5,7 @@ import ( "testing" ma "github.com/multiformats/go-multiaddr" - manet "github.com/multiformats/go-multiaddr-net" + manet "github.com/multiformats/go-multiaddr/net" ) var loopbackV4, _ = ma.NewMultiaddr("/ip4/127.0.0.1/tcp/0") From 15037361bc837c02c11911bf2c6ae93480275929 Mon Sep 17 00:00:00 2001 From: web3-bot Date: Tue, 17 Aug 2021 13:32:54 +0000 Subject: [PATCH 24/29] run gofmt -s --- p2p/net/reuseport/reuseport_posix.go | 1 + p2p/net/reuseport/reuseport_test.go | 1 + 2 files changed, 2 insertions(+) diff --git a/p2p/net/reuseport/reuseport_posix.go b/p2p/net/reuseport/reuseport_posix.go index bfe7f39d..4f0d89c6 100644 --- a/p2p/net/reuseport/reuseport_posix.go +++ b/p2p/net/reuseport/reuseport_posix.go @@ -1,3 +1,4 @@ +//go:build !plan9 // +build !plan9 package tcpreuse diff --git a/p2p/net/reuseport/reuseport_test.go b/p2p/net/reuseport/reuseport_test.go index 7dbd5616..b7a080d7 100644 --- a/p2p/net/reuseport/reuseport_test.go +++ b/p2p/net/reuseport/reuseport_test.go @@ -1,3 +1,4 @@ +//go:build !plan9 // +build !plan9 package tcpreuse From c12d890cf7d648883068c470e096733611f05161 Mon Sep 17 00:00:00 2001 From: Marten Seemann Date: Mon, 20 Sep 2021 15:25:35 +0100 Subject: [PATCH 25/29] disable failing tests on OSX --- p2p/net/reuseport/transport_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/p2p/net/reuseport/transport_test.go b/p2p/net/reuseport/transport_test.go index 5d779467..186e1406 100644 --- a/p2p/net/reuseport/transport_test.go +++ b/p2p/net/reuseport/transport_test.go @@ -2,6 +2,7 @@ package tcpreuse import ( "net" + "runtime" "testing" ma "github.com/multiformats/go-multiaddr" @@ -125,6 +126,9 @@ func TestTwoLocal(t *testing.T) { } func TestGlobalPreferenceV4(t *testing.T) { + if runtime.GOOS == "darwin" { + t.Skip("This test is failing on OSX: https://github.com/libp2p/go-reuseport-transport/issues/33") + } if globalV4 == nil { t.Skip("no global IPv4 addresses configured") return @@ -139,6 +143,9 @@ func TestGlobalPreferenceV4(t *testing.T) { } func TestGlobalPreferenceV6(t *testing.T) { + if runtime.GOOS == "darwin" { + t.Skip("This test is failing on OSX: https://github.com/libp2p/go-reuseport-transport/issues/33") + } if globalV6 == nil { t.Skip("no global IPv6 addresses configured") return @@ -150,6 +157,9 @@ func TestGlobalPreferenceV6(t *testing.T) { } func TestLoopbackPreference(t *testing.T) { + if runtime.GOOS == "darwin" { + t.Skip("This test is failing on OSX: https://github.com/libp2p/go-reuseport-transport/issues/33") + } testPrefer(t, loopbackV4, loopbackV4, unspecV4) testPrefer(t, loopbackV6, loopbackV6, unspecV6) } @@ -234,6 +244,9 @@ func testUseFirst(t *testing.T, listen, use, never ma.Multiaddr) { } func TestDuplicateGlobal(t *testing.T) { + if runtime.GOOS == "darwin" { + t.Skip("This test is failing on OSX: https://github.com/libp2p/go-reuseport-transport/issues/33") + } if globalV4 == nil { t.Skip("no globalV4 addresses configured") return From 3f74a3a8d09f8d2f6b1467778bca792dff0ecbe5 Mon Sep 17 00:00:00 2001 From: Marten Seemann Date: Mon, 20 Sep 2021 15:27:13 +0100 Subject: [PATCH 26/29] chore: update go-log to v2 --- p2p/net/reuseport/transport.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/net/reuseport/transport.go b/p2p/net/reuseport/transport.go index 5f094d1a..ce7c4c50 100644 --- a/p2p/net/reuseport/transport.go +++ b/p2p/net/reuseport/transport.go @@ -4,7 +4,7 @@ import ( "errors" "sync" - logging "github.com/ipfs/go-log" + logging "github.com/ipfs/go-log/v2" ) var log = logging.Logger("reuseport-transport") From c42772b95ca14c3ceb6c1c38028ceddb98ce4ba7 Mon Sep 17 00:00:00 2001 From: Marten Seemann Date: Wed, 18 May 2022 13:53:11 +0200 Subject: [PATCH 27/29] disable failing TestV6V4 on OSX (#41) --- p2p/net/reuseport/transport_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/p2p/net/reuseport/transport_test.go b/p2p/net/reuseport/transport_test.go index 186e1406..e4918591 100644 --- a/p2p/net/reuseport/transport_test.go +++ b/p2p/net/reuseport/transport_test.go @@ -196,6 +196,9 @@ func testPrefer(t *testing.T, listen, prefer, avoid ma.Multiaddr) { } func TestV6V4(t *testing.T) { + if runtime.GOOS == "darwin" { + t.Skip("This test is failing on OSX: https://github.com/libp2p/go-reuseport-transport/issues/40") + } testUseFirst(t, loopbackV4, loopbackV4, loopbackV6) testUseFirst(t, loopbackV6, loopbackV6, loopbackV4) } From 2f38a8797b2cf780adfc3d87cda6632b34a09ab4 Mon Sep 17 00:00:00 2001 From: Marten Seemann Date: Thu, 19 May 2022 20:30:31 +0200 Subject: [PATCH 28/29] set linger to 0 on OSX in tests (#42) --- p2p/net/reuseport/transport_test.go | 33 ++++++++++++----------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/p2p/net/reuseport/transport_test.go b/p2p/net/reuseport/transport_test.go index e4918591..19a1fe97 100644 --- a/p2p/net/reuseport/transport_test.go +++ b/p2p/net/reuseport/transport_test.go @@ -39,33 +39,40 @@ func init() { } } -func acceptOne(t *testing.T, listener manet.Listener) <-chan interface{} { +func setLingerZero(c manet.Conn) { + if runtime.GOOS == "darwin" { + c.(interface{ SetLinger(int) error }).SetLinger(0) + } +} + +func acceptOne(t *testing.T, listener manet.Listener) <-chan manet.Conn { t.Helper() - done := make(chan interface{}, 1) + done := make(chan manet.Conn, 1) go func() { defer close(done) - done <- nil c, err := listener.Accept() if err != nil { t.Error(err) return } - c.Close() + setLingerZero(c) + done <- c }() - <-done return done } func dialOne(t *testing.T, tr *Transport, listener manet.Listener, expected ...int) int { t.Helper() - done := acceptOne(t, listener) + connChan := acceptOne(t, listener) c, err := tr.Dial(listener.Multiaddr()) if err != nil { t.Fatal(err) } + setLingerZero(c) port := c.LocalAddr().(*net.TCPAddr).Port - <-done + serverConn := <-connChan + serverConn.Close() c.Close() if len(expected) == 0 { return port @@ -126,9 +133,6 @@ func TestTwoLocal(t *testing.T) { } func TestGlobalPreferenceV4(t *testing.T) { - if runtime.GOOS == "darwin" { - t.Skip("This test is failing on OSX: https://github.com/libp2p/go-reuseport-transport/issues/33") - } if globalV4 == nil { t.Skip("no global IPv4 addresses configured") return @@ -143,9 +147,6 @@ func TestGlobalPreferenceV4(t *testing.T) { } func TestGlobalPreferenceV6(t *testing.T) { - if runtime.GOOS == "darwin" { - t.Skip("This test is failing on OSX: https://github.com/libp2p/go-reuseport-transport/issues/33") - } if globalV6 == nil { t.Skip("no global IPv6 addresses configured") return @@ -157,9 +158,6 @@ func TestGlobalPreferenceV6(t *testing.T) { } func TestLoopbackPreference(t *testing.T) { - if runtime.GOOS == "darwin" { - t.Skip("This test is failing on OSX: https://github.com/libp2p/go-reuseport-transport/issues/33") - } testPrefer(t, loopbackV4, loopbackV4, unspecV4) testPrefer(t, loopbackV6, loopbackV6, unspecV6) } @@ -247,9 +245,6 @@ func testUseFirst(t *testing.T, listen, use, never ma.Multiaddr) { } func TestDuplicateGlobal(t *testing.T) { - if runtime.GOOS == "darwin" { - t.Skip("This test is failing on OSX: https://github.com/libp2p/go-reuseport-transport/issues/33") - } if globalV4 == nil { t.Skip("no globalV4 addresses configured") return From 27b9721651912e927821cf61612f2d77fba56c11 Mon Sep 17 00:00:00 2001 From: Marten Seemann Date: Fri, 20 May 2022 10:38:56 +0200 Subject: [PATCH 29/29] improve package-level documentation (#43) --- p2p/net/reuseport/transport.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/p2p/net/reuseport/transport.go b/p2p/net/reuseport/transport.go index ce7c4c50..a9a12c4c 100644 --- a/p2p/net/reuseport/transport.go +++ b/p2p/net/reuseport/transport.go @@ -1,3 +1,12 @@ +// Package tcpreuse provides a basic transport for automatically (and intelligently) reusing TCP ports. +// +// To use, construct a new Transport and configure listeners tr.Listen(...). +// When dialing (tr.Dial(...)), the transport will attempt to reuse the ports it's currently listening on, +// choosing the best one depending on the destination address. +// +// It is recommended to set set SO_LINGER to 0 for all connections, otherwise +// reusing the port may fail when re-dialing a recently closed connection. +// See https://hea-www.harvard.edu/~fine/Tech/addrinuse.html for details. package tcpreuse import ( @@ -13,6 +22,7 @@ var log = logging.Logger("reuseport-transport") var ErrWrongProto = errors.New("can only dial TCP over IPv4 or IPv6") // Transport is a TCP reuse transport that reuses listener ports. +// The zero value is safe to use. type Transport struct { v4 network v6 network