Skip to content
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
2 changes: 1 addition & 1 deletion p2p/test/transport/rcmgr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func TestResourceManagerIsUsed(t *testing.T) {
}
return nil
})
if tc.Name == "WebRTC" {
if strings.Contains(tc.Name, "WebRTC") {
// webrtc receive buffer is a fix sized buffer allocated up front
connScope.EXPECT().ReserveMemory(gomock.Any(), gomock.Any())
}
Expand Down
31 changes: 25 additions & 6 deletions p2p/test/transport/transport_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,10 +339,29 @@ var transportsToTest = []TransportTestCase{
},
},
{
Name: "WebRTC",
// WebRTC-v1 dials with the v1 flow (SDP munging).
Name: "WebRTC-v1",
HostGenerator: func(t *testing.T, opts TransportTestCaseOpts) host.Host {
libp2pOpts := transformOpts(opts)
libp2pOpts = append(libp2pOpts, libp2p.Transport(libp2pwebrtc.New))
libp2pOpts = append(libp2pOpts, libp2p.Transport(libp2pwebrtc.New, libp2pwebrtc.WithDialerVersion(1)))
if opts.NoListen {
libp2pOpts = append(libp2pOpts, libp2p.NoListenAddrs)
} else {
libp2pOpts = append(libp2pOpts, libp2p.ListenAddrStrings("/ip4/127.0.0.1/udp/0/webrtc-direct"))
}
h, err := libp2p.New(libp2pOpts...)
require.NoError(t, err)
return h
},
},
{
// WebRTC-v2 dials with the v2 flow (no SDP munging). The listener accepts
// both versions and picks one from the ufrag prefix, so only the dialer
// needs the option; the same generator is fine for both hosts.
Name: "WebRTC-v2",
HostGenerator: func(t *testing.T, opts TransportTestCaseOpts) host.Host {
libp2pOpts := transformOpts(opts)
libp2pOpts = append(libp2pOpts, libp2p.Transport(libp2pwebrtc.New, libp2pwebrtc.WithDialerVersion(2)))
if opts.NoListen {
libp2pOpts = append(libp2pOpts, libp2p.NoListenAddrs)
} else {
Expand Down Expand Up @@ -893,7 +912,7 @@ func TestDiscoverPeerIDFromSecurityNegotiation(t *testing.T) {
func TestCloseConnWhenBlocked(t *testing.T) {
for _, tc := range transportsToTest {
// WebRTC doesn't have a connection when rcmgr blocks it, so there's nothing to close.
if tc.Name == "WebRTC" {
if strings.Contains(tc.Name, "WebRTC") {
continue
}
t.Run(tc.Name, func(t *testing.T) {
Expand Down Expand Up @@ -933,7 +952,7 @@ func TestCloseConnWhenBlocked(t *testing.T) {
// connection attempt
func TestConnDroppedWhenBlocked(t *testing.T) {
for _, tc := range transportsToTest {
if tc.Name != "WebRTC" {
if !strings.Contains(tc.Name, "WebRTC") {
continue
}
t.Run(tc.Name, func(t *testing.T) {
Expand Down Expand Up @@ -1096,7 +1115,7 @@ func TestErrorCodes(t *testing.T) {
})

t.Run("StreamResetByConnCloseWithError", func(t *testing.T) {
if tc.Name == "WebRTC" {
if strings.Contains(tc.Name, "WebRTC") {
t.Skipf("skipping: %s, not implemented", tc.Name)
return
}
Expand Down Expand Up @@ -1124,7 +1143,7 @@ func TestErrorCodes(t *testing.T) {
})

t.Run("NewStreamErrorByConnCloseWithError", func(t *testing.T) {
if tc.Name == "WebRTC" {
if strings.Contains(tc.Name, "WebRTC") {
t.Skipf("skipping: %s, not implemented", tc.Name)
return
}
Expand Down
20 changes: 14 additions & 6 deletions p2p/transport/webrtc/listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,8 @@ func (l *listener) listen() {

conn, err := l.handleCandidate(ctx, candidate)
if err != nil {
l.mux.RemoveConnByUfrag(candidate.Ufrag)
log.Debug("could not accept connection", "ufrag", candidate.Ufrag, "error", err)
l.mux.RemoveConnByUfrag(candidate.LocalUfrag)
log.Debug("could not accept connection", "ufrag", candidate.LocalUfrag, "error", err)
return
}

Expand Down Expand Up @@ -196,9 +196,15 @@ func (l *listener) setupConnection(
}
}()

// The udpmux has already parsed and validated the STUN USERNAME: LocalUfrag is
// the server (local) ufrag, RemoteUfrag the client ufrag, and RemotePwd the
// client ICE password (recovered per WebRTC Direct version). See
// udpmux.credentialsFromSTUNMessage.
serverUfrag := candidate.LocalUfrag

settingEngine := webrtc.SettingEngine{LoggerFactory: pionLoggerFactory}
settingEngine.SetAnsweringDTLSRole(webrtc.DTLSRoleServer)
settingEngine.SetICECredentials(candidate.Ufrag, candidate.Ufrag)
settingEngine.SetICECredentials(serverUfrag, serverUfrag)
settingEngine.SetLite(true)
settingEngine.SetICEUDPMux(l.mux)
settingEngine.SetIncludeLoopbackCandidate(true)
Expand All @@ -224,9 +230,11 @@ func (l *listener) setupConnection(
}

errC := addOnConnectionStateChangeCallback(w.PeerConnection)
// Infer the client SDP from the incoming STUN message by setting the ice-ufrag.
// Infer the client SDP offer from the incoming STUN message using the client
// ufrag and password. pion validates the full "server_ufrag:client_ufrag"
// USERNAME on inbound checks, so the remote ice-ufrag must be the client ufrag.
if err := w.PeerConnection.SetRemoteDescription(webrtc.SessionDescription{
SDP: createClientSDP(candidate.Addr, candidate.Ufrag),
SDP: createClientSDP(candidate.Addr, candidate.RemoteUfrag, candidate.RemotePwd),
Type: webrtc.SDPTypeOffer,
}); err != nil {
return nil, err
Expand All @@ -244,7 +252,7 @@ func (l *listener) setupConnection(
return nil, ctx.Err()
case err := <-errC:
if err != nil {
return nil, fmt.Errorf("peer connection failed for ufrag: %s", candidate.Ufrag)
return nil, fmt.Errorf("peer connection failed for ufrag: %s", serverUfrag)
}
}

Expand Down
9 changes: 7 additions & 2 deletions p2p/transport/webrtc/sdp.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import (
// The fingerprint used to render a client SDP is arbitrary since
// it fingerprint verification is disabled in favour of a noise
// handshake. The max message size is fixed to 16384 bytes.
//
// The ice-ufrag and ice-pwd are rendered separately. In WebRTC Direct v1 they
// are the same shared value; in v2 the client ufrag and the recovered client
// password differ. See https://github.com/libp2p/specs/blob/master/webrtc/webrtc-direct.md.
const clientSDP = `v=0
o=- 0 0 IN %[1]s %[2]s
s=-
Expand All @@ -24,14 +28,14 @@ m=application %[3]d UDP/DTLS/SCTP webrtc-datachannel
a=mid:0
a=ice-options:ice2
a=ice-ufrag:%[4]s
a=ice-pwd:%[4]s
a=ice-pwd:%[5]s
a=fingerprint:sha-256 ba:78:16:bf:8f:01:cf:ea:41:41:40:de:5d:ae:22:23:b0:03:61:a3:96:17:7a:9c:b4:10:ff:61:f2:00:15:ad
a=setup:actpass
a=sctp-port:5000
a=max-message-size:16384
`

func createClientSDP(addr *net.UDPAddr, ufrag string) string {
func createClientSDP(addr *net.UDPAddr, ufrag, pwd string) string {
ipVersion := "IP4"
if addr.IP.To4() == nil {
ipVersion = "IP6"
Expand All @@ -42,6 +46,7 @@ func createClientSDP(addr *net.UDPAddr, ufrag string) string {
addr.IP,
addr.Port,
ufrag,
pwd,
)
}

Expand Down
16 changes: 14 additions & 2 deletions p2p/transport/webrtc/sdp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,28 @@ a=max-message-size:16384
func TestRenderClientSDP(t *testing.T) {
addr := &net.UDPAddr{IP: net.IPv4(0, 0, 0, 0), Port: 37826}
ufrag := "d2c0fc07-8bb3-42ae-bae2-a6fce8a0b581"
sdp := createClientSDP(addr, ufrag)
sdp := createClientSDP(addr, ufrag, ufrag)
require.Equal(t, expectedClientSDP, sdp)
}

// In WebRTC Direct v2 the inferred client offer carries distinct ice-ufrag and
// ice-pwd values: the client ufrag from the STUN USERNAME and the client
// password recovered from the "libp2p+webrtc+v2/<client_pwd>" server ufrag.
func TestRenderClientSDPV2DistinctCredentials(t *testing.T) {
addr := &net.UDPAddr{IP: net.IPv4(0, 0, 0, 0), Port: 37826}
clientUfrag := "browserClientUfrag"
clientPwd := "browserClientPassword1234"
sdp := createClientSDP(addr, clientUfrag, clientPwd)
require.Contains(t, sdp, "a=ice-ufrag:"+clientUfrag+"\n")
require.Contains(t, sdp, "a=ice-pwd:"+clientPwd+"\n")
}

func BenchmarkRenderClientSDP(b *testing.B) {
addr := &net.UDPAddr{IP: net.IPv4(0, 0, 0, 0), Port: 37826}
ufrag := "d2c0fc07-8bb3-42ae-bae2-a6fce8a0b581"

for i := 0; i < b.N; i++ {
createClientSDP(addr, ufrag)
createClientSDP(addr, ufrag, ufrag)
}
}

Expand Down
99 changes: 78 additions & 21 deletions p2p/transport/webrtc/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/libp2p/go-libp2p/p2p/security/noise"
libp2pquic "github.com/libp2p/go-libp2p/p2p/transport/quic"
"github.com/libp2p/go-libp2p/p2p/transport/webrtc/pb"
"github.com/libp2p/go-libp2p/p2p/transport/webrtc/udpmux"
"github.com/libp2p/go-msgio"

ma "github.com/multiformats/go-multiaddr"
Expand Down Expand Up @@ -89,6 +90,14 @@ type WebRTCTransport struct {

listenUDP func(network string, laddr *net.UDPAddr) (net.PacketConn, error)

// dialerVersion picks which WebRTC Direct handshake to use when dialing: 1
// uses v1 (SDP munging), 2 uses v2 (no munging; the client password rides in
// the server ufrag). New defaults it to 1, and WithDialerVersion only accepts
// 1 or 2, so dial never sees the confusing 0 value. The listener accepts both
// either way; it detects the version from the ICE username fragment prefix.
// See https://github.com/libp2p/specs/blob/master/webrtc/webrtc-direct.md.
dialerVersion int

// timeouts
peerConnectionTimeouts iceTimeouts

Expand All @@ -100,6 +109,24 @@ var _ tpt.Transport = &WebRTCTransport{}

type Option func(*WebRTCTransport) error

// WithDialerVersion picks which WebRTC Direct handshake to use when dialing. It
// must be 1 (v1, SDP munging) or 2 (v2, no munging); any other value, including
// 0, is rejected. With the option unset, dialing defaults to v1. The listener
// accepts both either way. Browsers will need v2 once Chromium's
// WebRTC-NoSdpMangleUfrag field trial ships; this option lets a go dialer use the
// same v2 wire format. See https://github.com/libp2p/specs/blob/master/webrtc/webrtc-direct.md.
func WithDialerVersion(version int) Option {
return func(t *WebRTCTransport) error {
switch version {
case 1, 2:
t.dialerVersion = version
return nil
default:
return fmt.Errorf("unknown WebRTC Direct dialer version %d", version)
}
}
}

type iceTimeouts struct {
Disconnect time.Duration
Failed time.Duration
Expand Down Expand Up @@ -162,6 +189,9 @@ func New(privKey ic.PrivKey, psk pnet.PSK, gater connmgr.ConnectionGater, rcmgr
},

maxInFlightConnections: DefaultMaxInFlightConnections,

// Default to v1; WithDialerVersion can override it before dial time.
dialerVersion: 1,
}
for _, opt := range opts {
if err := opt(transport); err != nil {
Expand Down Expand Up @@ -301,17 +331,38 @@ func (t *WebRTCTransport) dial(ctx context.Context, scope network.ConnManagement
return nil, fmt.Errorf("resolve udp address: %w", err)
}

// Instead of encoding the local fingerprint we
// generate a random UUID as the connection ufrag.
// The only requirement here is that the ufrag and password
// must be equal, which will allow the server to determine
// the password using the STUN message.
ufrag := genUfrag()
// The server has no signaling channel, so it must infer our ICE credentials
// from the STUN connectivity checks.
//
// In v1 we generate a single random value, prefixed with "libp2p+webrtc+v1/",
// and use it as both our local ufrag and password and as the server's. The
// server reads it from the STUN USERNAME and derives the password from it.
//
// In v2 our local ufrag and password are independent random values (as a
// browser's would be) and we encode our password into the server ufrag as
// "libp2p+webrtc+v2/<pwd>", so the server can recover it from the STUN
// USERNAME without us munging our local SDP. See https://github.com/libp2p/specs/blob/master/webrtc/webrtc-direct.md.
var localUfrag, localPwd, serverUfrag string
switch t.dialerVersion {
// TODO: flip the default to v2 roughly 12 months after Chromium ships its
// no-munging behavior (the WebRTC-NoSdpMangleUfrag field trial,
// https://webrtc-review.googlesource.com/c/src/+/385721) in stable, giving
// servers and other implementations a grace period to adopt v2 first.
case 1:
localUfrag = genUfrag()
localPwd = localUfrag
serverUfrag = localUfrag
case 2:
localUfrag, localPwd = genV2ClientCredentials()
serverUfrag = udpmux.UfragPrefixV2 + localPwd
default:
return nil, fmt.Errorf("unsupported WebRTC Direct dialer version %d", t.dialerVersion)
}

settingEngine := webrtc.SettingEngine{
LoggerFactory: pionLoggerFactory,
}
settingEngine.SetICECredentials(ufrag, ufrag)
settingEngine.SetICECredentials(localUfrag, localPwd)
settingEngine.DetachDataChannels()
// use the first best address candidate
settingEngine.SetPrflxAcceptanceMinWait(0)
Expand Down Expand Up @@ -349,7 +400,7 @@ func (t *WebRTCTransport) dial(ctx context.Context, scope network.ConnManagement
return nil, fmt.Errorf("set local description: %w", err)
}

answerSDPString, err := createServerSDP(raddr, ufrag, *remoteMultihash)
answerSDPString, err := createServerSDP(raddr, serverUfrag, *remoteMultihash)
if err != nil {
return nil, fmt.Errorf("render server SDP: %w", err)
}
Expand Down Expand Up @@ -421,22 +472,28 @@ func (t *WebRTCTransport) dial(ctx context.Context, scope network.ConnManagement
}

func genUfrag() string {
const (
uFragAlphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
uFragPrefix = "libp2p+webrtc+v1/"
uFragIdLength = 32
uFragLength = len(uFragPrefix) + uFragIdLength
)
return udpmux.UfragPrefixV1 + randIceString(32)
}

// genV2ClientCredentials returns a random ICE ufrag and password for the WebRTC
// Direct v2 dial flow. Unlike v1, the two values are independent and carry no
// prefix; the password is instead encoded into the server ufrag as
// "libp2p+webrtc+v2/<pwd>". Both are 32 ice-chars, comfortably above the RFC
// 8839 section 5.4 minimums (4 for the ufrag, 22 for the password).
func genV2ClientCredentials() (ufrag, pwd string) {
return randIceString(32), randIceString(32)
}

// randIceString returns a string of n characters drawn from the ICE ufrag/pwd
// alphabet (the alphanumeric subset of RFC 8839 ice-char, also used by browsers).
func randIceString(n int) string {
const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
seed := [32]byte{}
rand.Read(seed[:])
r := mrand.New(mrand.New(mrand.NewChaCha8(seed)))
b := make([]byte, uFragLength)
for i := range len(uFragPrefix) {
b[i] = uFragPrefix[i]
}
for i := len(uFragPrefix); i < uFragLength; i++ {
b[i] = uFragAlphabet[r.IntN(len(uFragAlphabet))]
r := mrand.New(mrand.NewChaCha8(seed))
b := make([]byte, n)
for i := range b {
b[i] = alphabet[r.IntN(len(alphabet))]
}
return string(b)
}
Expand Down
Loading
Loading