// Package libp2pwebrtc implements the WebRTC transport for go-libp2p, // as described in https://github.com/libp2p/specs/tree/master/webrtc.
package libp2pwebrtc import ( mrand ic tpt libp2pquic ma manet ) var webrtcComponent *ma.Component func init() { var error webrtcComponent, = ma.NewComponent(ma.ProtocolWithCode(ma.P_WEBRTC_DIRECT).Name, "") if != nil { log.Fatal() } } const ( // handshakeChannelNegotiated is used to specify that the // handshake data channel does not need negotiation via DCEP. // A constant is used since the `DataChannelInit` struct takes // references instead of values. handshakeChannelNegotiated = true // handshakeChannelID is the agreed ID for the handshake data // channel. A constant is used since the `DataChannelInit` struct takes // references instead of values. We specify the type here as this // value is only ever copied and passed by reference handshakeChannelID = uint16(0) ) // timeout values for the peerconnection // https://github.com/pion/webrtc/blob/v3.1.50/settingengine.go#L102-L109 const ( DefaultDisconnectedTimeout = 20 * time.Second DefaultFailedTimeout = 30 * time.Second DefaultKeepaliveTimeout = 15 * time.Second // sctpReceiveBufferSize is the size of the buffer for incoming messages. // // This is enough space for enqueuing 10 full sized messages. // Besides throughput, this only matters if an application is using multiple dependent // streams, say streams 1 & 2. It reads from stream 1 only after receiving message from // stream 2. A buffer of 10 messages should serve all such situations. sctpReceiveBufferSize = 10 * maxReceiveMessageSize ) type WebRTCTransport struct { webrtcConfig webrtc.Configuration rcmgr network.ResourceManager gater connmgr.ConnectionGater privKey ic.PrivKey noiseTpt *noise.Transport localPeerId peer.ID listenUDP func(network string, laddr *net.UDPAddr) (net.PacketConn, error) // timeouts peerConnectionTimeouts iceTimeouts // in-flight connections maxInFlightConnections uint32 } var _ tpt.Transport = &WebRTCTransport{} type Option func(*WebRTCTransport) error type iceTimeouts struct { Disconnect time.Duration Failed time.Duration Keepalive time.Duration } type ListenUDPFn func(network string, laddr *net.UDPAddr) (net.PacketConn, error) func ( ic.PrivKey, pnet.PSK, connmgr.ConnectionGater, network.ResourceManager, ListenUDPFn, ...Option) (*WebRTCTransport, error) { if != nil { log.Error("WebRTC doesn't support private networks yet.") return nil, fmt.Errorf("WebRTC doesn't support private networks yet") } if == nil { = &network.NullResourceManager{} } , := peer.IDFromPrivateKey() if != nil { return nil, fmt.Errorf("get local peer ID: %w", ) } // We use elliptic P-256 since it is widely supported by browsers. // // Implementation note: Testing with the browser, // it seems like Chromium only supports ECDSA P-256 or RSA key signatures in the webrtc TLS certificate. // We tried using P-228 and P-384 which caused the DTLS handshake to fail with Illegal Parameter // // Please refer to this is a list of suggested algorithms for the WebCrypto API. // The algorithm for generating a certificate for an RTCPeerConnection // must adhere to the WebCrpyto API. From my observation, // RSA and ECDSA P-256 is supported on almost all browsers. // Ed25519 is not present on the list. , := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if != nil { return nil, fmt.Errorf("generate key for cert: %w", ) } , := webrtc.GenerateCertificate() if != nil { return nil, fmt.Errorf("generate certificate: %w", ) } := webrtc.Configuration{ Certificates: []webrtc.Certificate{*}, } , := noise.New(noise.ID, , nil) if != nil { return nil, fmt.Errorf("unable to create noise transport: %w", ) } := &WebRTCTransport{ rcmgr: , gater: , webrtcConfig: , privKey: , noiseTpt: , localPeerId: , listenUDP: , peerConnectionTimeouts: iceTimeouts{ Disconnect: DefaultDisconnectedTimeout, Failed: DefaultFailedTimeout, Keepalive: DefaultKeepaliveTimeout, }, maxInFlightConnections: DefaultMaxInFlightConnections, } for , := range { if := (); != nil { return nil, } } return , nil } func ( *WebRTCTransport) () int { return libp2pquic.ListenOrder + 1 // We want to listen after QUIC listens so we can possibly reuse the same port. } func ( *WebRTCTransport) () []int { return []int{ma.P_WEBRTC_DIRECT} } func ( *WebRTCTransport) () bool { return false } func ( *WebRTCTransport) ( ma.Multiaddr) bool { , := IsWebRTCDirectMultiaddr() return && > 0 } // Listen returns a listener for addr. // // The IP, Port combination for addr must be exclusive to this listener as a WebRTC listener cannot // be multiplexed on the same port as other UDP based transports like QUIC and WebTransport. // See https://github.com/libp2p/go-libp2p/issues/2446 for details. func ( *WebRTCTransport) ( ma.Multiaddr) (tpt.Listener, error) { , := ma.SplitLast() := .Equal(webrtcComponent) if ! { return nil, fmt.Errorf("must listen on webrtc multiaddr") } , , := manet.DialArgs() if != nil { return nil, fmt.Errorf("listener could not fetch dialargs: %w", ) } , := net.ResolveUDPAddr(, ) if != nil { return nil, fmt.Errorf("listener could not resolve udp address: %w", ) } , := .listenUDP(, ) if != nil { return nil, fmt.Errorf("listen on udp: %w", ) } , := .listenSocket() if != nil { .Close() return nil, } return , nil } func ( *WebRTCTransport) ( net.PacketConn) (tpt.Listener, error) { , := manet.FromNetAddr(.LocalAddr()) if != nil { return nil, } , := .getCertificateFingerprint() if != nil { return nil, } , := encodeDTLSFingerprint() if != nil { return nil, } , := ma.NewComponent(ma.ProtocolWithCode(ma.P_CERTHASH).Name, ) if != nil { return nil, } = .AppendComponent(webrtcComponent, ) return newListener( , , , .webrtcConfig, ) } func ( *WebRTCTransport) ( context.Context, ma.Multiaddr, peer.ID) (tpt.CapableConn, error) { , := .rcmgr.OpenConnection(network.DirOutbound, false, ) if != nil { return nil, } if := .SetPeer(); != nil { .Done() return nil, } , := .dial(, , , ) if != nil { .Done() return nil, } return , nil } func ( *WebRTCTransport) ( context.Context, network.ConnManagementScope, ma.Multiaddr, peer.ID) ( tpt.CapableConn, error) { var webRTCConnection defer func() { if != nil { if .PeerConnection != nil { _ = .PeerConnection.Close() } if != nil { _ = .Close() = nil } } }() , := decodeRemoteFingerprint() if != nil { return nil, fmt.Errorf("decode fingerprint: %w", ) } , := getSupportedSDPHash(.Code) if ! { return nil, fmt.Errorf("unsupported hash function: %w", nil) } , , := manet.DialArgs() if != nil { return nil, fmt.Errorf("generate dial args: %w", ) } , := net.ResolveUDPAddr(, ) if != nil { return nil, fmt.Errorf("resolve udp address: %w", ) } // 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. := genUfrag() := webrtc.SettingEngine{ LoggerFactory: pionLoggerFactory, } .SetICECredentials(, ) .DetachDataChannels() // use the first best address candidate .SetPrflxAcceptanceMinWait(0) .SetICETimeouts( .peerConnectionTimeouts.Disconnect, .peerConnectionTimeouts.Failed, .peerConnectionTimeouts.Keepalive, ) // By default, webrtc will not collect candidates on the loopback address. // This is disallowed in the ICE specification. However, implementations // do not strictly follow this, for eg. Chrome gathers TCP loopback candidates. // If you run pion on a system with only the loopback interface UP, // it will not connect to anything. .SetIncludeLoopbackCandidate(true) .SetSCTPMaxReceiveBufferSize(sctpReceiveBufferSize) if := .ReserveMemory(sctpReceiveBufferSize, network.ReservationPriorityMedium); != nil { return nil, } , = newWebRTCConnection(, .webrtcConfig) if != nil { return nil, fmt.Errorf("instantiating peer connection failed: %w", ) } := addOnConnectionStateChangeCallback(.PeerConnection) // do offer-answer exchange , := .PeerConnection.CreateOffer(nil) if != nil { return nil, fmt.Errorf("create offer: %w", ) } = .PeerConnection.SetLocalDescription() if != nil { return nil, fmt.Errorf("set local description: %w", ) } , := createServerSDP(, , *) if != nil { return nil, fmt.Errorf("render server SDP: %w", ) } := webrtc.SessionDescription{SDP: , Type: webrtc.SDPTypeAnswer} = .PeerConnection.SetRemoteDescription() if != nil { return nil, fmt.Errorf("set remote description: %w", ) } // await peerconnection opening select { case := <-: if != nil { return nil, } case <-.Done(): return nil, errors.New("peerconnection opening timed out") } // We are connected, run the noise handshake , := detachHandshakeDataChannel(, .HandshakeDataChannel) if != nil { return nil, } := newStream(.HandshakeDataChannel, , maxSendMessageSize, nil) , := .noiseHandshake(, .PeerConnection, , , , false) if != nil { return nil, } // Setup local and remote address for the connection , := .HandshakeDataChannel.Transport().Transport().ICETransport().GetSelectedCandidatePair() if == nil { return nil, errors.New("ice connection did not have selected candidate pair: nil result") } if != nil { return nil, fmt.Errorf("ice connection did not have selected candidate pair: error: %w", ) } // the local address of the selected candidate pair should be the local address for the connection , := manet.FromNetAddr(&net.UDPAddr{IP: net.ParseIP(.Local.Address), Port: int(.Local.Port)}) if != nil { return nil, } , := ma.SplitFunc(, func( ma.Component) bool { return .Protocol().Code == ma.P_CERTHASH }) , := newConnection( network.DirOutbound, .PeerConnection, , , .localPeerId, , , , , .IncomingDataChannels, .PeerConnectionClosedCh, ) if != nil { return nil, } if .gater != nil && !.gater.InterceptSecured(network.DirOutbound, , ) { return nil, fmt.Errorf("secured connection gated") } return , nil } func genUfrag() string { const ( = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" = "libp2p+webrtc+v1/" = 32 = len() + ) := [32]byte{} rand.Read([:]) := mrand.New(mrand.New(mrand.NewChaCha8())) := make([]byte, ) for := 0; < len(); ++ { [] = [] } for := len(); < ; ++ { [] = [.IntN(len())] } return string() } func ( *WebRTCTransport) () (webrtc.DTLSFingerprint, error) { , := .webrtcConfig.Certificates[0].GetFingerprints() if != nil { return webrtc.DTLSFingerprint{}, } return [0], nil } func ( *WebRTCTransport) ( *webrtc.PeerConnection, crypto.Hash, bool) ([]byte, error) { := .SCTP().Transport().GetRemoteCertificate() , := x509.ParseCertificate() if != nil { return nil, } // NOTE: should we want we can fork the cert code as well to avoid // all the extra allocations due to unneeded string interspersing (hex) , := .getCertificateFingerprint() if != nil { return nil, } , := parseFingerprint(, ) if != nil { return nil, } , := decodeInterspersedHexFromASCIIString(.Value) if != nil { return nil, } , := multihash.Encode(, multihash.SHA2_256) if != nil { log.Debugf("could not encode multihash for local fingerprint") return nil, } , := multihash.Encode(, multihash.SHA2_256) if != nil { log.Debugf("could not encode multihash for remote fingerprint") return nil, } := []byte("libp2p-webrtc-noise:") if { = append(, ...) = append(, ...) } else { = append(, ...) = append(, ...) } return , nil } func ( *WebRTCTransport) ( context.Context, *webrtc.PeerConnection, *stream, peer.ID, crypto.Hash, bool) (ic.PubKey, error) { , := .generateNoisePrologue(, , ) if != nil { return nil, fmt.Errorf("generate prologue: %w", ) } := make([]noise.SessionOption, 0, 2) = append(, noise.Prologue()) if == "" { = append(, noise.DisablePeerIDCheck()) } , := .noiseTpt.WithSessionOptions(...) if != nil { return nil, fmt.Errorf("failed to instantiate Noise transport: %w", ) } var sec.SecureConn if { , = .SecureOutbound(, netConnWrapper{}, ) if != nil { return nil, fmt.Errorf("failed to secure inbound connection: %w", ) } } else { , = .SecureInbound(, netConnWrapper{}, ) if != nil { return nil, fmt.Errorf("failed to secure outbound connection: %w", ) } } return .RemotePublicKey(), nil } func ( *WebRTCTransport) ( ma.Multiaddr) (ma.Multiaddr, bool) { , := .getCertificateFingerprint() if != nil { return nil, false } , := encodeDTLSFingerprint() if != nil { return nil, false } , := ma.NewComponent(ma.ProtocolWithCode(ma.P_CERTHASH).Name, ) if != nil { return nil, false } return .AppendComponent(), true } type netConnWrapper struct { *stream } func (netConnWrapper) () net.Addr { return nil } func (netConnWrapper) () net.Addr { return nil } func ( netConnWrapper) () error { // Close called while running the security handshake is an error and we should Reset the // stream in that case rather than gracefully closing .stream.Reset() return nil } // detachHandshakeDataChannel detaches the handshake data channel func detachHandshakeDataChannel( context.Context, *webrtc.DataChannel) (datachannel.ReadWriteCloser, error) { := make(chan struct{}) var datachannel.ReadWriteCloser var error .OnOpen(func() { defer close() , = .Detach() }) // this is safe since for detached datachannels, the peerconnection runs the onOpen // callback immediately if the SCTP transport is also connected. select { case <-: return , case <-.Done(): return nil, .Err() } } // webRTCConnection holds the webrtc.PeerConnection with the handshake channel and the queue for // incoming data channels created by the peer. // // When creating a webrtc.PeerConnection, It is important to set the OnDataChannel handler upfront // before connecting with the peer. If the handler's set up after connecting with the peer, there's // a small window of time where datachannels created by the peer may not surface to us and cause a // memory leak. type webRTCConnection struct { PeerConnection *webrtc.PeerConnection HandshakeDataChannel *webrtc.DataChannel IncomingDataChannels chan dataChannel PeerConnectionClosedCh chan struct{} } func newWebRTCConnection( webrtc.SettingEngine, webrtc.Configuration) (webRTCConnection, error) { := webrtc.NewAPI(webrtc.WithSettingEngine()) , := .NewPeerConnection() if != nil { return webRTCConnection{}, fmt.Errorf("failed to create peer connection: %w", ) } , := handshakeChannelNegotiated, handshakeChannelID , := .CreateDataChannel("", &webrtc.DataChannelInit{ Negotiated: &, ID: &, }) if != nil { .Close() return webRTCConnection{}, fmt.Errorf("failed to create handshake channel: %w", ) } := make(chan dataChannel, maxAcceptQueueLen) .OnDataChannel(func( *webrtc.DataChannel) { .OnOpen(func() { , := .Detach() if != nil { log.Warnf("could not detach datachannel: id: %d", *.ID()) return } select { case <- dataChannel{, }: default: log.Warnf("connection busy, rejecting stream") , := proto.Marshal(&pb.Message{Flag: pb.Message_RESET.Enum()}) := msgio.NewWriter() .WriteMsg() .Close() } }) }) := make(chan struct{}, 1) .SCTP().OnClose(func( error) { // We only need one message. Closing a connection is a problem as pion might invoke the callback more than once. select { case <- struct{}{}: default: } }) return webRTCConnection{ PeerConnection: , HandshakeDataChannel: , IncomingDataChannels: , PeerConnectionClosedCh: , }, nil } // IsWebRTCDirectMultiaddr returns whether addr is a /webrtc-direct multiaddr with the count of certhashes // in addr func ( ma.Multiaddr) (bool, int) { var , bool := 0 ma.ForEach(, func( ma.Component) bool { if ! { if .Protocol().Code == ma.P_UDP { = true } return true } if ! && { // protocol after udp must be webrtc-direct if .Protocol().Code != ma.P_WEBRTC_DIRECT { return false } = true return true } if { if .Protocol().Code == ma.P_CERTHASH { ++ } else { return false } } return true }) return && , }