Skip to content

Commit

Permalink
feat(transport/websocket): support SOCKS proxy with ws(s) (#3137)
Browse files Browse the repository at this point in the history
  • Loading branch information
MarcoPolo authored Jan 24, 2025
1 parent 49c9549 commit 29d83ed
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 5 deletions.
22 changes: 17 additions & 5 deletions p2p/transport/websocket/websocket.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ func (t *WebsocketTransport) Resolve(_ context.Context, maddr ma.Multiaddr) ([]m
return []ma.Multiaddr{parsed.toMultiaddr()}, nil
}

// Dial will dial the given multiaddr and expect the given peer. If an
// HTTPS_PROXY env is set, it will use that for the dial out.
func (t *WebsocketTransport) Dial(ctx context.Context, raddr ma.Multiaddr, p peer.ID) (transport.CapableConn, error) {
connScope, err := t.rcmgr.OpenConnection(network.DirOutbound, true, raddr)
if err != nil {
Expand Down Expand Up @@ -191,7 +193,11 @@ func (t *WebsocketTransport) maDial(ctx context.Context, raddr ma.Multiaddr) (ma
return nil, err
}
isWss := wsurl.Scheme == "wss"
dialer := ws.Dialer{HandshakeTimeout: 30 * time.Second}
dialer := ws.Dialer{
HandshakeTimeout: 30 * time.Second,
// Inherit the default proxy behavior
Proxy: ws.DefaultDialer.Proxy,
}
if isWss {
sni := ""
sni, err = raddr.ValueForProtocol(ma.P_SNI)
Expand All @@ -203,17 +209,23 @@ func (t *WebsocketTransport) maDial(ctx context.Context, raddr ma.Multiaddr) (ma
copytlsClientConf := t.tlsClientConf.Clone()
copytlsClientConf.ServerName = sni
dialer.TLSClientConfig = copytlsClientConf
ipAddr := wsurl.Host
// Setting the NetDial because we already have the resolved IP address, so we don't want to do another resolution.
ipPortAddr := wsurl.Host
// We set the `.Host` to the sni field so that the host header gets properly set.
wsurl.Host = sni + ":" + wsurl.Port()
// Setting the NetDial because we already have the resolved IP address, so we can avoid another resolution.
dialer.NetDial = func(network, address string) (net.Conn, error) {
tcpAddr, err := net.ResolveTCPAddr(network, ipAddr)
var tcpAddr *net.TCPAddr
var err error
if address == wsurl.Host {
tcpAddr, err = net.ResolveTCPAddr(network, ipPortAddr) // Use our already resolved IP address
} else {
tcpAddr, err = net.ResolveTCPAddr(network, address)
}
if err != nil {
return nil, err
}
return net.DialTCP("tcp", nil, tcpAddr)
}
wsurl.Host = sni + ":" + wsurl.Port()
} else {
dialer.TLSClientConfig = t.tlsClientConf
}
Expand Down
75 changes: 75 additions & 0 deletions p2p/transport/websocket/websocket_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package websocket

import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/elliptic"
Expand All @@ -15,10 +16,12 @@ import (
"math/big"
"net"
"net/http"
"net/url"
"strings"
"testing"
"time"

gws "github.com/gorilla/websocket"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer"
Expand Down Expand Up @@ -548,3 +551,75 @@ func TestResolveMultiaddr(t *testing.T) {
})
}
}

func TestSocksProxy(t *testing.T) {
testCases := []string{
"/ip4/1.2.3.4/tcp/1/ws", // No TLS
"/ip4/1.2.3.4/tcp/1/tls/ws", // TLS no SNI
"/ip4/1.2.3.4/tcp/1/tls/sni/example.com/ws", // TLS with an SNI
}

for _, tc := range testCases {
t.Run(tc, func(t *testing.T) {
proxyServer, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
proxyServerErr := make(chan error, 1)

go func() {
defer proxyServer.Close()
c, err := proxyServer.Accept()
if err != nil {
proxyServerErr <- err
return
}
defer c.Close()

req := [32]byte{}
_, err = io.ReadFull(c, req[:3])
if err != nil {
proxyServerErr <- err
return
}

// Handshake a SOCKS5 client: https://www.rfc-editor.org/rfc/rfc1928.html#section-3
if !bytes.Equal([]byte{0x05, 0x01, 0x00}, req[:3]) {
t.Log("expected SOCKS5 connect request")
proxyServerErr <- err
return
}
_, err = c.Write([]byte{0x05, 0x00})
if err != nil {
proxyServerErr <- err
return
}

proxyServerErr <- nil
}()

orig := gws.DefaultDialer.Proxy
defer func() { gws.DefaultDialer.Proxy = orig }()

proxyUrl, err := url.Parse("socks5://" + proxyServer.Addr().String())
require.NoError(t, err)
gws.DefaultDialer.Proxy = http.ProxyURL(proxyUrl)

tlsConfig := &tls.Config{InsecureSkipVerify: true} // Our test server doesn't have a cert signed by a CA
_, u := newSecureUpgrader(t)
tpt, err := New(u, &network.NullResourceManager{}, nil, WithTLSClientConfig(tlsConfig))
require.NoError(t, err)

// This can be any wss address. We aren't actually going to dial it.
maToDial := ma.StringCast(tc)
_, err = tpt.Dial(context.Background(), maToDial, "")
require.ErrorContains(t, err, "failed to read connect reply from SOCKS5 proxy", "This should error as we don't have a real socks server")

select {
case <-time.After(1 * time.Second):
case err := <-proxyServerErr:
if err != nil {
t.Fatal(err)
}
}
})
}
}

0 comments on commit 29d83ed

Please sign in to comment.