-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Connection multiaddress should match the address actually dialed (WSS, WebTransport, WebRTC) #2988
base: master
Are you sure you want to change the base?
Changes from all commits
2f126d0
f008fe3
2bf2ad6
f9d8302
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,10 +3,17 @@ package transport_integration | |
import ( | ||
"bytes" | ||
"context" | ||
"crypto/ecdsa" | ||
"crypto/elliptic" | ||
"crypto/rand" | ||
"crypto/tls" | ||
"crypto/x509" | ||
"crypto/x509/pkix" | ||
"encoding/pem" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"math/big" | ||
"net" | ||
"runtime" | ||
"strings" | ||
|
@@ -30,8 +37,9 @@ import ( | |
"github.com/libp2p/go-libp2p/p2p/net/swarm" | ||
"github.com/libp2p/go-libp2p/p2p/protocol/ping" | ||
"github.com/libp2p/go-libp2p/p2p/security/noise" | ||
tls "github.com/libp2p/go-libp2p/p2p/security/tls" | ||
sectls "github.com/libp2p/go-libp2p/p2p/security/tls" | ||
libp2pwebrtc "github.com/libp2p/go-libp2p/p2p/transport/webrtc" | ||
"github.com/libp2p/go-libp2p/p2p/transport/websocket" | ||
"go.uber.org/mock/gomock" | ||
|
||
ma "github.com/multiformats/go-multiaddr" | ||
|
@@ -48,6 +56,7 @@ type TransportTestCaseOpts struct { | |
NoRcmgr bool | ||
ConnGater connmgr.ConnectionGater | ||
ResourceManager network.ResourceManager | ||
HostSeed string | ||
} | ||
|
||
func transformOpts(opts TransportTestCaseOpts) []config.Option { | ||
|
@@ -87,7 +96,7 @@ var transportsToTest = []TransportTestCase{ | |
Name: "TCP / TLS / Yamux", | ||
HostGenerator: func(t *testing.T, opts TransportTestCaseOpts) host.Host { | ||
libp2pOpts := transformOpts(opts) | ||
libp2pOpts = append(libp2pOpts, libp2p.Security(tls.ID, tls.New)) | ||
libp2pOpts = append(libp2pOpts, libp2p.Security(sectls.ID, sectls.New)) | ||
libp2pOpts = append(libp2pOpts, libp2p.Muxer(yamux.ID, yamux.DefaultTransport)) | ||
if opts.NoListen { | ||
libp2pOpts = append(libp2pOpts, libp2p.NoListenAddrs) | ||
|
@@ -113,6 +122,26 @@ var transportsToTest = []TransportTestCase{ | |
return h | ||
}, | ||
}, | ||
{ | ||
Name: "Secure WebSocket with CA Certificate", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As with the WebSockets example the second security layer negotiation + multiplexer are left undefined (i.e. use the defaults), is that a mistake? |
||
HostGenerator: func(t *testing.T, opts TransportTestCaseOpts) host.Host { | ||
libp2pOpts := transformOpts(opts) | ||
wsOpts := []interface{}{websocket.WithTLSClientConfig(&tls.Config{InsecureSkipVerify: true})} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could also add a shared rootCA here, thoughts? |
||
if opts.NoListen { | ||
libp2pOpts = append(libp2pOpts, libp2p.NoListenAddrs) | ||
} else { | ||
dnsName := fmt.Sprintf("example%s.com", opts.HostSeed) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can drop |
||
cert, err := generateSelfSignedCert(dnsName) | ||
require.NoError(t, err) | ||
libp2pOpts = append(libp2pOpts, libp2p.ListenAddrStrings(fmt.Sprintf("/ip4/127.0.0.1/tcp/0/tls/sni/%s/ws", dnsName))) | ||
wsOpts = append(wsOpts, websocket.WithTLSConfig(&tls.Config{Certificates: []tls.Certificate{cert}})) | ||
} | ||
libp2pOpts = append(libp2pOpts, libp2p.Transport(websocket.New, wsOpts...)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This makes WSS a standout compared to the others in that the others all support multiple transports, but this host only supports WSS. Should the other host generators also be narrowed down to support a single transport? |
||
h, err := libp2p.New(libp2pOpts...) | ||
require.NoError(t, err) | ||
return h | ||
}, | ||
}, | ||
{ | ||
Name: "QUIC", | ||
HostGenerator: func(t *testing.T, opts TransportTestCaseOpts) host.Host { | ||
|
@@ -158,6 +187,46 @@ var transportsToTest = []TransportTestCase{ | |
}, | ||
} | ||
|
||
func generateSelfSignedCert(dnsName string) (tls.Certificate, error) { | ||
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) | ||
if err != nil { | ||
return tls.Certificate{}, err | ||
} | ||
|
||
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) | ||
if err != nil { | ||
return tls.Certificate{}, err | ||
} | ||
|
||
template := x509.Certificate{ | ||
SerialNumber: serialNumber, | ||
Subject: pkix.Name{ | ||
Organization: []string{"My Organization"}, | ||
}, | ||
NotBefore: time.Now(), | ||
NotAfter: time.Now().Add(365 * 24 * time.Hour), // Valid for 1 year | ||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, | ||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, | ||
BasicConstraintsValid: true, | ||
DNSNames: []string{dnsName}, | ||
} | ||
|
||
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) | ||
if err != nil { | ||
return tls.Certificate{}, err | ||
} | ||
|
||
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) | ||
privDER, err := x509.MarshalECPrivateKey(priv) | ||
if err != nil { | ||
return tls.Certificate{}, err | ||
} | ||
privPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: privDER}) | ||
|
||
// Load the certificate and key into tls.Certificate | ||
return tls.X509KeyPair(certPEM, privPEM) | ||
} | ||
|
||
func TestPing(t *testing.T) { | ||
for _, tc := range transportsToTest { | ||
t.Run(tc.Name, func(t *testing.T) { | ||
|
@@ -798,3 +867,29 @@ func TestConnClosedWhenRemoteCloses(t *testing.T) { | |
}) | ||
} | ||
} | ||
|
||
func TestConnMatchingAddress(t *testing.T) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here's the new test with the behavior that IIUC is that's expected and was driving most of the changes (although the connection gater ones might be different) |
||
for _, tc := range transportsToTest { | ||
t.Run(tc.Name, func(t *testing.T) { | ||
server := tc.HostGenerator(t, TransportTestCaseOpts{}) | ||
client1 := tc.HostGenerator(t, TransportTestCaseOpts{NoListen: true}) | ||
client2 := tc.HostGenerator(t, TransportTestCaseOpts{NoListen: true}) | ||
defer server.Close() | ||
defer client1.Close() | ||
defer client2.Close() | ||
|
||
client1.Peerstore().AddAddrs(server.ID(), server.Addrs(), peerstore.PermanentAddrTTL) | ||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | ||
defer cancel() | ||
err := client1.Connect(ctx, peer.AddrInfo{ID: server.ID(), Addrs: server.Addrs()}) | ||
require.NoError(t, err) | ||
|
||
client1Conns := client1.Network().ConnsToPeer(server.ID()) | ||
require.Equal(t, 1, len(client1Conns)) | ||
remoteMA := client1Conns[0].RemoteMultiaddr() | ||
|
||
err = client2.Connect(ctx, peer.AddrInfo{ID: server.ID(), Addrs: []ma.Multiaddr{remoteMA}}) | ||
require.NoError(t, err) | ||
}) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,9 @@ | ||
package websocket | ||
|
||
import ( | ||
"fmt" | ||
ma "github.com/multiformats/go-multiaddr" | ||
manet "github.com/multiformats/go-multiaddr/net" | ||
"io" | ||
"net" | ||
"sync" | ||
|
@@ -25,17 +28,72 @@ type Conn struct { | |
closeOnce sync.Once | ||
|
||
readLock, writeLock sync.Mutex | ||
|
||
laddr, raddr *Addr | ||
laddrma, raddrma ma.Multiaddr | ||
} | ||
|
||
var _ net.Conn = (*Conn)(nil) | ||
|
||
// NewConn creates a Conn given a regular gorilla/websocket Conn. | ||
func NewConn(raw *ws.Conn, secure bool) *Conn { | ||
// NewOutboundConn creates an outbound Conn given a regular gorilla/websocket Conn. | ||
func NewOutboundConn(raw *ws.Conn, secure bool, sni string) (*Conn, error) { | ||
return newConn(raw, secure, sni, false) | ||
} | ||
|
||
// NewInboundConn creates an inbound Conn given a regular gorilla/websocket Conn. | ||
func NewInboundConn(raw *ws.Conn, secure bool, sni string) (*Conn, error) { | ||
return newConn(raw, secure, sni, true) | ||
} | ||
|
||
// newConn creates a Conn given a regular gorilla/websocket Conn. | ||
func newConn(raw *ws.Conn, secure bool, sni string, inbound bool) (*Conn, error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function could use some review. Additionally, I don't feel like we've tested the
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I haven't looked at this PR yet, but the connection's RemoteMultiaddr should not have a dns component. It should have the IP address that is used for the current connection (along with tls/sni info). Apologies if that's already the case, and I haven't looked at this yet. |
||
laddr := NewAddrWithScheme(raw.LocalAddr().String(), secure) | ||
raddr := NewAddrWithScheme(raw.RemoteAddr().String(), secure) | ||
|
||
laddrma, err := manet.FromNetAddr(laddr) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to convert connection address to multiaddr: %s", err) | ||
} | ||
|
||
raddrma, err := manet.FromNetAddr(raddr) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to convert connection address to multiaddr: %s", err) | ||
} | ||
|
||
if secure && sni != "" { | ||
var wssMA ma.Multiaddr | ||
if inbound { | ||
wssMA = laddrma | ||
} else { | ||
wssMA = raddrma | ||
} | ||
|
||
if withoutWSS := wssMA.Decapsulate(ma.StringCast("/wss")); withoutWSS.Equal(wssMA) { | ||
return nil, fmt.Errorf("missing wss component from converted multiaddr") | ||
} else { | ||
tlsSniWsMa, err := ma.NewMultiaddr(fmt.Sprintf("/tls/sni/%s/ws", sni)) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to convert connection address to multiaddr: %s", err) | ||
} | ||
wssMA = withoutWSS.Encapsulate(tlsSniWsMa) | ||
} | ||
|
||
if inbound { | ||
laddrma = wssMA | ||
} else { | ||
raddrma = wssMA | ||
} | ||
} | ||
|
||
return &Conn{ | ||
Conn: raw, | ||
secure: secure, | ||
DefaultMessageType: ws.BinaryMessage, | ||
} | ||
laddr: laddr, | ||
raddr: raddr, | ||
laddrma: laddrma, | ||
raddrma: raddrma, | ||
}, nil | ||
} | ||
|
||
func (c *Conn) Read(b []byte) (int, error) { | ||
|
@@ -122,11 +180,19 @@ func (c *Conn) Close() error { | |
} | ||
|
||
func (c *Conn) LocalAddr() net.Addr { | ||
return NewAddrWithScheme(c.Conn.LocalAddr().String(), c.secure) | ||
return c.laddr | ||
} | ||
|
||
func (c *Conn) RemoteAddr() net.Addr { | ||
return NewAddrWithScheme(c.Conn.RemoteAddr().String(), c.secure) | ||
return c.raddr | ||
} | ||
|
||
func (c *Conn) LocalMultiaddr() ma.Multiaddr { | ||
return c.laddrma | ||
} | ||
|
||
func (c *Conn) RemoteMultiaddr() ma.Multiaddr { | ||
return c.raddrma | ||
} | ||
|
||
func (c *Conn) SetDeadline(t time.Time) error { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wasn't sure what the correct move was for the connection gaters (both in terms of what should be expected and how people are using this and how it might break).
At the moment I made a decision here to strip out the certhashes for
InterceptAccept
but notInterceptSecured
based on the idea that Accept shouldn't really be thinking about security-related, but if it's available maybe it's still worth exposing and perhaps the complexity of being different is too confusing.Either way, once we decide what's going on here I think adding better documentation to the interfaces seems needed