Skip to content

Commit 29d83ed

Browse files
authored
feat(transport/websocket): support SOCKS proxy with ws(s) (#3137)
1 parent 49c9549 commit 29d83ed

File tree

2 files changed

+92
-5
lines changed

2 files changed

+92
-5
lines changed

p2p/transport/websocket/websocket.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ func (t *WebsocketTransport) Resolve(_ context.Context, maddr ma.Multiaddr) ([]m
160160
return []ma.Multiaddr{parsed.toMultiaddr()}, nil
161161
}
162162

163+
// Dial will dial the given multiaddr and expect the given peer. If an
164+
// HTTPS_PROXY env is set, it will use that for the dial out.
163165
func (t *WebsocketTransport) Dial(ctx context.Context, raddr ma.Multiaddr, p peer.ID) (transport.CapableConn, error) {
164166
connScope, err := t.rcmgr.OpenConnection(network.DirOutbound, true, raddr)
165167
if err != nil {
@@ -191,7 +193,11 @@ func (t *WebsocketTransport) maDial(ctx context.Context, raddr ma.Multiaddr) (ma
191193
return nil, err
192194
}
193195
isWss := wsurl.Scheme == "wss"
194-
dialer := ws.Dialer{HandshakeTimeout: 30 * time.Second}
196+
dialer := ws.Dialer{
197+
HandshakeTimeout: 30 * time.Second,
198+
// Inherit the default proxy behavior
199+
Proxy: ws.DefaultDialer.Proxy,
200+
}
195201
if isWss {
196202
sni := ""
197203
sni, err = raddr.ValueForProtocol(ma.P_SNI)
@@ -203,17 +209,23 @@ func (t *WebsocketTransport) maDial(ctx context.Context, raddr ma.Multiaddr) (ma
203209
copytlsClientConf := t.tlsClientConf.Clone()
204210
copytlsClientConf.ServerName = sni
205211
dialer.TLSClientConfig = copytlsClientConf
206-
ipAddr := wsurl.Host
207-
// Setting the NetDial because we already have the resolved IP address, so we don't want to do another resolution.
212+
ipPortAddr := wsurl.Host
208213
// We set the `.Host` to the sni field so that the host header gets properly set.
214+
wsurl.Host = sni + ":" + wsurl.Port()
215+
// Setting the NetDial because we already have the resolved IP address, so we can avoid another resolution.
209216
dialer.NetDial = func(network, address string) (net.Conn, error) {
210-
tcpAddr, err := net.ResolveTCPAddr(network, ipAddr)
217+
var tcpAddr *net.TCPAddr
218+
var err error
219+
if address == wsurl.Host {
220+
tcpAddr, err = net.ResolveTCPAddr(network, ipPortAddr) // Use our already resolved IP address
221+
} else {
222+
tcpAddr, err = net.ResolveTCPAddr(network, address)
223+
}
211224
if err != nil {
212225
return nil, err
213226
}
214227
return net.DialTCP("tcp", nil, tcpAddr)
215228
}
216-
wsurl.Host = sni + ":" + wsurl.Port()
217229
} else {
218230
dialer.TLSClientConfig = t.tlsClientConf
219231
}

p2p/transport/websocket/websocket_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package websocket
22

33
import (
4+
"bytes"
45
"context"
56
"crypto/ecdsa"
67
"crypto/elliptic"
@@ -15,10 +16,12 @@ import (
1516
"math/big"
1617
"net"
1718
"net/http"
19+
"net/url"
1820
"strings"
1921
"testing"
2022
"time"
2123

24+
gws "github.com/gorilla/websocket"
2225
"github.com/libp2p/go-libp2p/core/crypto"
2326
"github.com/libp2p/go-libp2p/core/network"
2427
"github.com/libp2p/go-libp2p/core/peer"
@@ -548,3 +551,75 @@ func TestResolveMultiaddr(t *testing.T) {
548551
})
549552
}
550553
}
554+
555+
func TestSocksProxy(t *testing.T) {
556+
testCases := []string{
557+
"/ip4/1.2.3.4/tcp/1/ws", // No TLS
558+
"/ip4/1.2.3.4/tcp/1/tls/ws", // TLS no SNI
559+
"/ip4/1.2.3.4/tcp/1/tls/sni/example.com/ws", // TLS with an SNI
560+
}
561+
562+
for _, tc := range testCases {
563+
t.Run(tc, func(t *testing.T) {
564+
proxyServer, err := net.Listen("tcp", "127.0.0.1:0")
565+
require.NoError(t, err)
566+
proxyServerErr := make(chan error, 1)
567+
568+
go func() {
569+
defer proxyServer.Close()
570+
c, err := proxyServer.Accept()
571+
if err != nil {
572+
proxyServerErr <- err
573+
return
574+
}
575+
defer c.Close()
576+
577+
req := [32]byte{}
578+
_, err = io.ReadFull(c, req[:3])
579+
if err != nil {
580+
proxyServerErr <- err
581+
return
582+
}
583+
584+
// Handshake a SOCKS5 client: https://www.rfc-editor.org/rfc/rfc1928.html#section-3
585+
if !bytes.Equal([]byte{0x05, 0x01, 0x00}, req[:3]) {
586+
t.Log("expected SOCKS5 connect request")
587+
proxyServerErr <- err
588+
return
589+
}
590+
_, err = c.Write([]byte{0x05, 0x00})
591+
if err != nil {
592+
proxyServerErr <- err
593+
return
594+
}
595+
596+
proxyServerErr <- nil
597+
}()
598+
599+
orig := gws.DefaultDialer.Proxy
600+
defer func() { gws.DefaultDialer.Proxy = orig }()
601+
602+
proxyUrl, err := url.Parse("socks5://" + proxyServer.Addr().String())
603+
require.NoError(t, err)
604+
gws.DefaultDialer.Proxy = http.ProxyURL(proxyUrl)
605+
606+
tlsConfig := &tls.Config{InsecureSkipVerify: true} // Our test server doesn't have a cert signed by a CA
607+
_, u := newSecureUpgrader(t)
608+
tpt, err := New(u, &network.NullResourceManager{}, nil, WithTLSClientConfig(tlsConfig))
609+
require.NoError(t, err)
610+
611+
// This can be any wss address. We aren't actually going to dial it.
612+
maToDial := ma.StringCast(tc)
613+
_, err = tpt.Dial(context.Background(), maToDial, "")
614+
require.ErrorContains(t, err, "failed to read connect reply from SOCKS5 proxy", "This should error as we don't have a real socks server")
615+
616+
select {
617+
case <-time.After(1 * time.Second):
618+
case err := <-proxyServerErr:
619+
if err != nil {
620+
t.Fatal(err)
621+
}
622+
}
623+
})
624+
}
625+
}

0 commit comments

Comments
 (0)