Skip to content

feat(transport/websocket): support SOCKS proxy with wss #3137

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Comment on lines +214 to +215
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel parseMultiaddr should handle this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR didnt introduce this, so it's fine to leave this as is.

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)
}
}
})
}
}
Loading