Skip to content

Commit 5a47a90

Browse files
aschmahmannsukunrtMarcoPolo
authored
feat(tcpreuse): add options for sharing TCP listeners amongst TCP, WS and WSS transports (#2984)
Allows the same socket to be shared amongst TCP,WS,WSS transports. --------- Co-authored-by: sukun <[email protected]> Co-authored-by: Marco Munizaga <[email protected]>
1 parent 362e583 commit 5a47a90

32 files changed

+1597
-106
lines changed

config/config.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import (
3838
"github.com/libp2p/go-libp2p/p2p/protocol/holepunch"
3939
"github.com/libp2p/go-libp2p/p2p/protocol/identify"
4040
"github.com/libp2p/go-libp2p/p2p/transport/quicreuse"
41+
"github.com/libp2p/go-libp2p/p2p/transport/tcpreuse"
4142
libp2pwebrtc "github.com/libp2p/go-libp2p/p2p/transport/webrtc"
4243
"github.com/prometheus/client_golang/prometheus"
4344

@@ -145,6 +146,8 @@ type Config struct {
145146
CustomIPv6BlackHoleSuccessCounter bool
146147

147148
UserFxOptions []fx.Option
149+
150+
ShareTCPListener bool
148151
}
149152

150153
func (cfg *Config) makeSwarm(eventBus event.Bus, enableMetrics bool) (*swarm.Swarm, error) {
@@ -289,6 +292,12 @@ func (cfg *Config) addTransports() ([]fx.Option, error) {
289292
fx.Provide(func() connmgr.ConnectionGater { return cfg.ConnectionGater }),
290293
fx.Provide(func() pnet.PSK { return cfg.PSK }),
291294
fx.Provide(func() network.ResourceManager { return cfg.ResourceManager }),
295+
fx.Provide(func(gater connmgr.ConnectionGater, rcmgr network.ResourceManager) *tcpreuse.ConnMgr {
296+
if !cfg.ShareTCPListener {
297+
return nil
298+
}
299+
return tcpreuse.NewConnMgr(tcpreuse.EnvReuseportVal, gater, rcmgr)
300+
}),
292301
fx.Provide(func(cm *quicreuse.ConnManager, sw *swarm.Swarm) libp2pwebrtc.ListenUDPFn {
293302
hasQuicAddrPortFor := func(network string, laddr *net.UDPAddr) bool {
294303
quicAddrPorts := map[string]struct{}{}

libp2p_test.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func TestTransportConstructor(t *testing.T) {
5959
_ connmgr.ConnectionGater,
6060
upgrader transport.Upgrader,
6161
) transport.Transport {
62-
tpt, err := tcp.NewTCPTransport(upgrader, nil)
62+
tpt, err := tcp.NewTCPTransport(upgrader, nil, nil)
6363
require.NoError(t, err)
6464
return tpt
6565
}
@@ -751,3 +751,27 @@ func getTLSConf(t *testing.T, ip net.IP, start, end time.Time) *tls.Config {
751751
}},
752752
}
753753
}
754+
755+
func TestSharedTCPAddr(t *testing.T) {
756+
h, err := New(
757+
ShareTCPListener(),
758+
Transport(tcp.NewTCPTransport),
759+
Transport(websocket.New),
760+
ListenAddrStrings("/ip4/0.0.0.0/tcp/8888"),
761+
ListenAddrStrings("/ip4/0.0.0.0/tcp/8888/ws"),
762+
)
763+
require.NoError(t, err)
764+
sawTCP := false
765+
sawWS := false
766+
for _, addr := range h.Addrs() {
767+
if strings.HasSuffix(addr.String(), "/tcp/8888") {
768+
sawTCP = true
769+
}
770+
if strings.HasSuffix(addr.String(), "/tcp/8888/ws") {
771+
sawWS = true
772+
}
773+
}
774+
require.True(t, sawTCP)
775+
require.True(t, sawWS)
776+
h.Close()
777+
}

options.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,3 +643,15 @@ func WithFxOption(opts ...fx.Option) Option {
643643
return nil
644644
}
645645
}
646+
647+
// ShareTCPListener shares the same listen address between TCP and Websocket
648+
// transports. This lets both transports use the same TCP port.
649+
//
650+
// Currently this behavior is Opt-in. In a future release this will be the
651+
// default, and this option will be removed.
652+
func ShareTCPListener() Option {
653+
return func(cfg *Config) error {
654+
cfg.ShareTCPListener = true
655+
return nil
656+
}
657+
}

p2p/net/swarm/dial_worker_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ func makeSwarmWithNoListenAddrs(t *testing.T, opts ...Option) *Swarm {
8484
upgrader := makeUpgrader(t, s)
8585
var tcpOpts []tcp.Option
8686
tcpOpts = append(tcpOpts, tcp.DisableReuseport())
87-
tcpTransport, err := tcp.NewTCPTransport(upgrader, nil, tcpOpts...)
87+
tcpTransport, err := tcp.NewTCPTransport(upgrader, nil, nil, tcpOpts...)
8888
require.NoError(t, err)
8989
if err := s.AddTransport(tcpTransport); err != nil {
9090
t.Fatal(err)

p2p/net/swarm/swarm_addr_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ func TestDialAddressSelection(t *testing.T) {
7979
s, err := swarm.NewSwarm("local", nil, eventbus.NewBus())
8080
require.NoError(t, err)
8181

82-
tcpTr, err := tcp.NewTCPTransport(nil, nil)
82+
tcpTr, err := tcp.NewTCPTransport(nil, nil, nil)
8383
require.NoError(t, err)
8484
require.NoError(t, s.AddTransport(tcpTr))
8585
reuse, err := quicreuse.NewConnManager(quic.StatelessResetKey{}, quic.TokenGeneratorKey{})

p2p/net/swarm/swarm_dial_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func TestAddrsForDial(t *testing.T) {
5353
ps.AddPrivKey(id, priv)
5454
t.Cleanup(func() { ps.Close() })
5555

56-
tpt, err := websocket.New(nil, &network.NullResourceManager{})
56+
tpt, err := websocket.New(nil, &network.NullResourceManager{}, nil)
5757
require.NoError(t, err)
5858
s, err := NewSwarm(id, ps, eventbus.NewBus(), WithMultiaddrResolver(ResolverFromMaDNS{resolver}))
5959
require.NoError(t, err)
@@ -100,7 +100,7 @@ func TestDedupAddrsForDial(t *testing.T) {
100100
require.NoError(t, err)
101101
defer s.Close()
102102

103-
tpt, err := tcp.NewTCPTransport(nil, &network.NullResourceManager{})
103+
tpt, err := tcp.NewTCPTransport(nil, &network.NullResourceManager{}, nil)
104104
require.NoError(t, err)
105105
err = s.AddTransport(tpt)
106106
require.NoError(t, err)
@@ -134,7 +134,7 @@ func newTestSwarmWithResolver(t *testing.T, resolver *madns.Resolver) *Swarm {
134134
})
135135

136136
// Add a tcp transport so that we know we can dial a tcp multiaddr and we don't filter it out.
137-
tpt, err := tcp.NewTCPTransport(nil, &network.NullResourceManager{})
137+
tpt, err := tcp.NewTCPTransport(nil, &network.NullResourceManager{}, nil)
138138
require.NoError(t, err)
139139
err = s.AddTransport(tpt)
140140
require.NoError(t, err)
@@ -151,7 +151,7 @@ func newTestSwarmWithResolver(t *testing.T, resolver *madns.Resolver) *Swarm {
151151
err = s.AddTransport(wtTpt)
152152
require.NoError(t, err)
153153

154-
wsTpt, err := websocket.New(nil, &network.NullResourceManager{})
154+
wsTpt, err := websocket.New(nil, &network.NullResourceManager{}, nil)
155155
require.NoError(t, err)
156156
err = s.AddTransport(wsTpt)
157157
require.NoError(t, err)

p2p/net/swarm/testing/testing.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ func GenSwarm(t testing.TB, opts ...Option) *swarm.Swarm {
164164
if cfg.disableReuseport {
165165
tcpOpts = append(tcpOpts, tcp.DisableReuseport())
166166
}
167-
tcpTransport, err := tcp.NewTCPTransport(upgrader, nil, tcpOpts...)
167+
tcpTransport, err := tcp.NewTCPTransport(upgrader, nil, nil, tcpOpts...)
168168
require.NoError(t, err)
169169
if err := s.AddTransport(tcpTransport); err != nil {
170170
t.Fatal(err)

p2p/net/upgrader/listener.go

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -84,23 +84,33 @@ func (l *listener) handleIncoming() {
8484
}
8585
catcher.Reset()
8686

87-
// gate the connection if applicable
88-
if l.upgrader.connGater != nil && !l.upgrader.connGater.InterceptAccept(maconn) {
89-
log.Debugf("gater blocked incoming connection on local addr %s from %s",
90-
maconn.LocalMultiaddr(), maconn.RemoteMultiaddr())
91-
if err := maconn.Close(); err != nil {
92-
log.Warnf("failed to close incoming connection rejected by gater: %s", err)
93-
}
94-
continue
87+
// Check if we already have a connection scope. See the comment in tcpreuse/listener.go for an explanation.
88+
var connScope network.ConnManagementScope
89+
if sc, ok := maconn.(interface {
90+
Scope() network.ConnManagementScope
91+
}); ok {
92+
connScope = sc.Scope()
9593
}
94+
if connScope == nil {
95+
// gate the connection if applicable
96+
if l.upgrader.connGater != nil && !l.upgrader.connGater.InterceptAccept(maconn) {
97+
log.Debugf("gater blocked incoming connection on local addr %s from %s",
98+
maconn.LocalMultiaddr(), maconn.RemoteMultiaddr())
99+
if err := maconn.Close(); err != nil {
100+
log.Warnf("failed to close incoming connection rejected by gater: %s", err)
101+
}
102+
continue
103+
}
96104

97-
connScope, err := l.rcmgr.OpenConnection(network.DirInbound, true, maconn.RemoteMultiaddr())
98-
if err != nil {
99-
log.Debugw("resource manager blocked accept of new connection", "error", err)
100-
if err := maconn.Close(); err != nil {
101-
log.Warnf("failed to incoming connection rejected by resource manager: %s", err)
105+
var err error
106+
connScope, err = l.rcmgr.OpenConnection(network.DirInbound, true, maconn.RemoteMultiaddr())
107+
if err != nil {
108+
log.Debugw("resource manager blocked accept of new connection", "error", err)
109+
if err := maconn.Close(); err != nil {
110+
log.Warnf("failed to open incoming connection. Rejected by resource manager: %s", err)
111+
}
112+
continue
102113
}
103-
continue
104114
}
105115

106116
// The go routine below calls Release when the context is

p2p/protocol/circuitv2/relay/relay_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func getNetHosts(t *testing.T, ctx context.Context, n int) (hosts []host.Host, u
6060
upgrader := swarmt.GenUpgrader(t, netw, nil)
6161
upgraders = append(upgraders, upgrader)
6262

63-
tpt, err := tcp.NewTCPTransport(upgrader, nil)
63+
tpt, err := tcp.NewTCPTransport(upgrader, nil, nil)
6464
if err != nil {
6565
t.Fatal(err)
6666
}

p2p/test/transport/gating_test.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package transport_integration
22

33
import (
44
"context"
5+
"encoding/binary"
6+
"net/netip"
57
"strings"
68
"testing"
79
"time"
@@ -30,6 +32,23 @@ func stripCertHash(addr ma.Multiaddr) ma.Multiaddr {
3032
return addr
3133
}
3234

35+
func addrPort(addr ma.Multiaddr) netip.AddrPort {
36+
a := netip.Addr{}
37+
p := uint16(0)
38+
ma.ForEach(addr, func(c ma.Component) bool {
39+
if c.Protocol().Code == ma.P_IP4 || c.Protocol().Code == ma.P_IP6 {
40+
a, _ = netip.AddrFromSlice(c.RawValue())
41+
return false
42+
}
43+
if c.Protocol().Code == ma.P_UDP || c.Protocol().Code == ma.P_TCP {
44+
p = binary.BigEndian.Uint16(c.RawValue())
45+
return true
46+
}
47+
return false
48+
})
49+
return netip.AddrPortFrom(a, p)
50+
}
51+
3352
func TestInterceptPeerDial(t *testing.T) {
3453
if race.WithRace() {
3554
t.Skip("The upgrader spawns a new Go routine, which leads to race conditions when using GoMock.")
@@ -173,10 +192,14 @@ func TestInterceptAccept(t *testing.T) {
173192
// remove the certhash component from WebTransport addresses
174193
require.Equal(t, stripCertHash(h2.Addrs()[0]), addrs.LocalMultiaddr())
175194
}).AnyTimes()
195+
} else if strings.Contains(tc.Name, "WebSocket-Shared") {
196+
connGater.EXPECT().InterceptAccept(gomock.Any()).Do(func(addrs network.ConnMultiaddrs) {
197+
require.Equal(t, addrPort(h2.Addrs()[0]), addrPort(addrs.LocalMultiaddr()))
198+
})
176199
} else {
177200
connGater.EXPECT().InterceptAccept(gomock.Any()).Do(func(addrs network.ConnMultiaddrs) {
178201
// remove the certhash component from WebTransport addresses
179-
require.Equal(t, stripCertHash(h2.Addrs()[0]), addrs.LocalMultiaddr())
202+
require.Equal(t, stripCertHash(h2.Addrs()[0]), addrs.LocalMultiaddr(), "%s\n%s", h2.Addrs()[0], addrs.LocalMultiaddr())
180203
})
181204
}
182205

p2p/test/transport/transport_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,38 @@ var transportsToTest = []TransportTestCase{
9999
return h
100100
},
101101
},
102+
{
103+
Name: "TCP-Shared / TLS / Yamux",
104+
HostGenerator: func(t *testing.T, opts TransportTestCaseOpts) host.Host {
105+
libp2pOpts := transformOpts(opts)
106+
libp2pOpts = append(libp2pOpts, libp2p.ShareTCPListener())
107+
libp2pOpts = append(libp2pOpts, libp2p.Security(tls.ID, tls.New))
108+
libp2pOpts = append(libp2pOpts, libp2p.Muxer(yamux.ID, yamux.DefaultTransport))
109+
if opts.NoListen {
110+
libp2pOpts = append(libp2pOpts, libp2p.NoListenAddrs)
111+
} else {
112+
libp2pOpts = append(libp2pOpts, libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0"))
113+
}
114+
h, err := libp2p.New(libp2pOpts...)
115+
require.NoError(t, err)
116+
return h
117+
},
118+
},
119+
{
120+
Name: "WebSocket-Shared",
121+
HostGenerator: func(t *testing.T, opts TransportTestCaseOpts) host.Host {
122+
libp2pOpts := transformOpts(opts)
123+
libp2pOpts = append(libp2pOpts, libp2p.ShareTCPListener())
124+
if opts.NoListen {
125+
libp2pOpts = append(libp2pOpts, libp2p.NoListenAddrs)
126+
} else {
127+
libp2pOpts = append(libp2pOpts, libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0/ws"))
128+
}
129+
h, err := libp2p.New(libp2pOpts...)
130+
require.NoError(t, err)
131+
return h
132+
},
133+
},
102134
{
103135
Name: "WebSocket",
104136
HostGenerator: func(t *testing.T, opts TransportTestCaseOpts) host.Host {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// go:build: unix
2+
3+
package tcp
4+
5+
import (
6+
"testing"
7+
8+
tptu "github.com/libp2p/go-libp2p/p2p/net/upgrader"
9+
"github.com/libp2p/go-libp2p/p2p/transport/tcpreuse"
10+
ttransport "github.com/libp2p/go-libp2p/p2p/transport/testsuite"
11+
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func TestTcpTransportCollectsMetricsWithSharedTcpSocket(t *testing.T) {
16+
17+
peerA, ia := makeInsecureMuxer(t)
18+
_, ib := makeInsecureMuxer(t)
19+
20+
sharedTCPSocketA := tcpreuse.NewConnMgr(false, nil, nil)
21+
sharedTCPSocketB := tcpreuse.NewConnMgr(false, nil, nil)
22+
23+
ua, err := tptu.New(ia, muxers, nil, nil, nil)
24+
require.NoError(t, err)
25+
ta, err := NewTCPTransport(ua, nil, sharedTCPSocketA, WithMetrics())
26+
require.NoError(t, err)
27+
ub, err := tptu.New(ib, muxers, nil, nil, nil)
28+
require.NoError(t, err)
29+
tb, err := NewTCPTransport(ub, nil, sharedTCPSocketB, WithMetrics())
30+
require.NoError(t, err)
31+
32+
zero := "/ip4/127.0.0.1/tcp/0"
33+
34+
// Not running any test that needs more than 1 conn because the testsuite
35+
// opens multiple conns via multiple listeners, which is not expected to work
36+
// with the shared TCP socket.
37+
subtestsToRun := []ttransport.TransportSubTestFn{
38+
ttransport.SubtestProtocols,
39+
ttransport.SubtestBasic,
40+
ttransport.SubtestCancel,
41+
ttransport.SubtestPingPong,
42+
43+
// Stolen from the stream muxer test suite.
44+
ttransport.SubtestStress1Conn1Stream1Msg,
45+
ttransport.SubtestStress1Conn1Stream100Msg,
46+
ttransport.SubtestStress1Conn100Stream100Msg,
47+
ttransport.SubtestStress1Conn1000Stream10Msg,
48+
ttransport.SubtestStress1Conn100Stream100Msg10MB,
49+
ttransport.SubtestStreamOpenStress,
50+
ttransport.SubtestStreamReset,
51+
}
52+
53+
ttransport.SubtestTransportWithFs(t, ta, tb, zero, peerA, subtestsToRun)
54+
}

0 commit comments

Comments
 (0)