diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e81b85c00..bb0f5ee3ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### Next + +- Add server side ICE consent checks to detect silent WebRTC disconnections ([PR #1332](https://github.com/versatica/mediasoup/pull/1332)). + ### 3.13.19 - Node: Fix `router.createWebRtcTransport()` with `listenIps` ([PR #1330](https://github.com/versatica/mediasoup/pull/1330)). diff --git a/node/src/test/test-WebRtcTransport.ts b/node/src/test/test-WebRtcTransport.ts index 72bfd48e4a..203438fc5e 100644 --- a/node/src/test/test-WebRtcTransport.ts +++ b/node/src/test/test-WebRtcTransport.ts @@ -299,12 +299,57 @@ test('webRtcTransport.connect() succeeds', async () => { }; await expect( - webRtcTransport.connect({ dtlsParameters: dtlsRemoteParameters }) + webRtcTransport.connect({ + dtlsParameters: dtlsRemoteParameters, + }) ).resolves.toBeUndefined(); // Must fail if connected. await expect( - webRtcTransport.connect({ dtlsParameters: dtlsRemoteParameters }) + webRtcTransport.connect({ + dtlsParameters: dtlsRemoteParameters, + }) + ).rejects.toThrow(Error); + + expect(webRtcTransport.dtlsParameters.role).toBe('server'); +}, 2000); + +test('webRtcTransport.connect() with iceParameters succeeds', async () => { + const webRtcTransport = await ctx.router!.createWebRtcTransport({ + listenInfos: [ + { protocol: 'udp', ip: '127.0.0.1', announcedAddress: '9.9.9.1' }, + ], + }); + + const iceRemoteParameters: mediasoup.types.IceParameters = { + usernameFragment: 'foo', + password: 'xxxx', + }; + + const dtlsRemoteParameters: mediasoup.types.DtlsParameters = { + fingerprints: [ + { + algorithm: 'sha-256', + value: + '82:5A:68:3D:36:C3:0A:DE:AF:E7:32:43:D2:88:83:57:AC:2D:65:E5:80:C4:B6:FB:AF:1A:A0:21:9F:6D:0C:AD', + }, + ], + role: 'client', + }; + + await expect( + webRtcTransport.connect({ + iceParameters: iceRemoteParameters, + dtlsParameters: dtlsRemoteParameters, + }) + ).resolves.toBeUndefined(); + + // Must fail if connected. + await expect( + webRtcTransport.connect({ + iceParameters: iceRemoteParameters, + dtlsParameters: dtlsRemoteParameters, + }) ).rejects.toThrow(Error); expect(webRtcTransport.dtlsParameters.role).toBe('server'); diff --git a/rust/examples/echo.rs b/rust/examples/echo.rs index d8d751f55f..733f2a4863 100644 --- a/rust/examples/echo.rs +++ b/rust/examples/echo.rs @@ -298,7 +298,10 @@ impl Handler for EchoConnection { // synchronous actix::spawn(async move { match transport - .connect(WebRtcTransportRemoteParameters { dtls_parameters }) + .connect(WebRtcTransportRemoteParameters { + ice_parameters: None, + dtls_parameters, + }) .await { Ok(_) => { @@ -346,7 +349,10 @@ impl Handler for EchoConnection { // The same as producer transport, but for consumer transport actix::spawn(async move { match transport - .connect(WebRtcTransportRemoteParameters { dtls_parameters }) + .connect(WebRtcTransportRemoteParameters { + ice_parameters: None, + dtls_parameters, + }) .await { Ok(_) => { diff --git a/rust/examples/multiopus.rs b/rust/examples/multiopus.rs index d78e8fd859..456fad1f56 100644 --- a/rust/examples/multiopus.rs +++ b/rust/examples/multiopus.rs @@ -331,7 +331,10 @@ impl Handler for EchoConnection { // The same as producer transport, but for consumer transport actix::spawn(async move { match transport - .connect(WebRtcTransportRemoteParameters { dtls_parameters }) + .connect(WebRtcTransportRemoteParameters { + ice_parameters: None, + dtls_parameters, + }) .await { Ok(_) => { diff --git a/rust/examples/svc-simulcast.rs b/rust/examples/svc-simulcast.rs index 4bf34bbf6c..d48dba6091 100644 --- a/rust/examples/svc-simulcast.rs +++ b/rust/examples/svc-simulcast.rs @@ -344,7 +344,10 @@ impl Handler for SvcSimulcastConnection { // synchronous actix::spawn(async move { match transport - .connect(WebRtcTransportRemoteParameters { dtls_parameters }) + .connect(WebRtcTransportRemoteParameters { + ice_parameters: None, + dtls_parameters, + }) .await { Ok(_) => { @@ -392,7 +395,10 @@ impl Handler for SvcSimulcastConnection { // The same as producer transport, but for consumer transport actix::spawn(async move { match transport - .connect(WebRtcTransportRemoteParameters { dtls_parameters }) + .connect(WebRtcTransportRemoteParameters { + ice_parameters: None, + dtls_parameters, + }) .await { Ok(_) => { diff --git a/rust/examples/videoroom.rs b/rust/examples/videoroom.rs index 379958534e..98ffdd493a 100644 --- a/rust/examples/videoroom.rs +++ b/rust/examples/videoroom.rs @@ -659,7 +659,10 @@ mod participant { // synchronous actix::spawn(async move { match transport - .connect(WebRtcTransportRemoteParameters { dtls_parameters }) + .connect(WebRtcTransportRemoteParameters { + ice_parameters: None, + dtls_parameters, + }) .await { Ok(_) => { @@ -718,7 +721,10 @@ mod participant { // The same as producer transport, but for consumer transport actix::spawn(async move { match transport - .connect(WebRtcTransportRemoteParameters { dtls_parameters }) + .connect(WebRtcTransportRemoteParameters { + ice_parameters: None, + dtls_parameters, + }) .await { Ok(_) => { diff --git a/rust/src/data_structures.rs b/rust/src/data_structures.rs index 807dee3990..59f7e77420 100644 --- a/rust/src/data_structures.rs +++ b/rust/src/data_structures.rs @@ -153,6 +153,14 @@ pub struct IceParameters { } impl IceParameters { + pub(crate) fn to_fbs(&self) -> web_rtc_transport::IceParameters { + web_rtc_transport::IceParameters { + username_fragment: self.username_fragment.to_string(), + password: self.password.to_string(), + ice_lite: self.ice_lite.unwrap_or(false), + } + } + pub(crate) fn from_fbs(parameters: web_rtc_transport::IceParameters) -> Self { Self { username_fragment: parameters.username_fragment.to_string(), diff --git a/rust/src/messages.rs b/rust/src/messages.rs index c05eed1e08..79e86db53c 100644 --- a/rust/src/messages.rs +++ b/rust/src/messages.rs @@ -669,6 +669,7 @@ pub(crate) struct RouterCreateWebrtcTransportData { enable_tcp: bool, prefer_udp: bool, prefer_tcp: bool, + ice_consent_timeout: u8, enable_sctp: bool, num_sctp_streams: NumSctpStreams, max_sctp_message_size: u32, @@ -701,6 +702,7 @@ impl RouterCreateWebrtcTransportData { enable_tcp: webrtc_transport_options.enable_tcp, prefer_udp: webrtc_transport_options.prefer_udp, prefer_tcp: webrtc_transport_options.prefer_tcp, + ice_consent_timeout: webrtc_transport_options.ice_consent_timeout, enable_sctp: webrtc_transport_options.enable_sctp, num_sctp_streams: webrtc_transport_options.num_sctp_streams, max_sctp_message_size: webrtc_transport_options.max_sctp_message_size, @@ -726,6 +728,7 @@ impl RouterCreateWebrtcTransportData { enable_tcp: self.enable_tcp, prefer_udp: self.prefer_udp, prefer_tcp: self.prefer_tcp, + ice_consent_timeout: self.ice_consent_timeout, } } } @@ -1405,6 +1408,7 @@ pub(crate) struct WebRtcTransportConnectResponse { #[derive(Debug)] pub(crate) struct WebRtcTransportConnectRequest { + pub(crate) ice_parameters: Option, pub(crate) dtls_parameters: DtlsParameters, } @@ -1415,8 +1419,11 @@ impl Request for WebRtcTransportConnectRequest { fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { let mut builder = Builder::new(); - let data = - web_rtc_transport::ConnectRequest::create(&mut builder, self.dtls_parameters.to_fbs()); + let data = web_rtc_transport::ConnectRequest::create( + &mut builder, + self.ice_parameters.map(|parameters| parameters.to_fbs()), + self.dtls_parameters.to_fbs(), + ); let request_body = request::Body::create_web_rtc_transport_connect_request(&mut builder, data); let request = request::Request::create( diff --git a/rust/src/router/webrtc_transport.rs b/rust/src/router/webrtc_transport.rs index a50a9dfa31..3129da2d5e 100644 --- a/rust/src/router/webrtc_transport.rs +++ b/rust/src/router/webrtc_transport.rs @@ -129,6 +129,10 @@ pub struct WebRtcTransportOptions { /// Prefer TCP. /// Default false. pub prefer_tcp: bool, + /// ICE consent timeout (in seconds). If 0 it is disabled. + /// For it to be enabled, iceParameters must be given in connect() method. + /// Default 30. + pub ice_consent_timeout: u8, /// Create a SCTP association. /// Default false. pub enable_sctp: bool, @@ -155,6 +159,7 @@ impl WebRtcTransportOptions { enable_tcp: false, prefer_udp: false, prefer_tcp: false, + ice_consent_timeout: 30, enable_sctp: false, num_sctp_streams: NumSctpStreams::default(), max_sctp_message_size: 262_144, @@ -172,6 +177,7 @@ impl WebRtcTransportOptions { enable_tcp: true, prefer_udp: false, prefer_tcp: false, + ice_consent_timeout: 30, enable_sctp: false, num_sctp_streams: NumSctpStreams::default(), max_sctp_message_size: 262_144, @@ -384,6 +390,8 @@ impl WebRtcTransportStat { #[derive(Debug, Clone, PartialOrd, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct WebRtcTransportRemoteParameters { + /// Remote ICE parameters. + pub ice_parameters: Option, /// Remote DTLS parameters. pub dtls_parameters: DtlsParameters, } @@ -920,7 +928,7 @@ impl WebRtcTransport { /// /// # Example /// ```rust - /// use mediasoup::data_structures::{DtlsParameters, DtlsRole, DtlsFingerprint}; + /// use mediasoup::data_structures::{IceParameters, DtlsParameters, DtlsRole, DtlsFingerprint}; /// use mediasoup::webrtc_transport::WebRtcTransportRemoteParameters; /// /// # async fn f( @@ -929,6 +937,11 @@ impl WebRtcTransport { /// // Calling connect() on a PlainTransport created with comedia and rtcp_mux set. /// webrtc_transport /// .connect(WebRtcTransportRemoteParameters { + /// ice_parameters: Some(IceParameters { + /// username_fragment: "foo".to_string(), + /// password: "xxxx".to_string(), + /// ice_lite: None, + /// }), /// dtls_parameters: DtlsParameters { /// role: DtlsRole::Server, /// fingerprints: vec![ @@ -958,6 +971,7 @@ impl WebRtcTransport { .request( self.id(), WebRtcTransportConnectRequest { + ice_parameters: remote_parameters.ice_parameters, dtls_parameters: remote_parameters.dtls_parameters, }, ) diff --git a/rust/tests/integration/webrtc_transport.rs b/rust/tests/integration/webrtc_transport.rs index c1e9b4bffb..45e2e60346 100644 --- a/rust/tests/integration/webrtc_transport.rs +++ b/rust/tests/integration/webrtc_transport.rs @@ -1,8 +1,8 @@ use futures_lite::future; use hash_hasher::HashedSet; use mediasoup::data_structures::{ - AppData, DtlsFingerprint, DtlsParameters, DtlsRole, DtlsState, IceCandidateType, IceRole, - IceState, ListenInfo, Protocol, SctpState, + AppData, DtlsFingerprint, DtlsParameters, DtlsRole, DtlsState, IceCandidateType, IceParameters, + IceRole, IceState, ListenInfo, Protocol, SctpState, }; use mediasoup::prelude::*; use mediasoup::router::{Router, RouterOptions}; @@ -418,6 +418,7 @@ fn connect_succeeds() { transport .connect(WebRtcTransportRemoteParameters { + ice_parameters: None, dtls_parameters: dtls_parameters.clone(), }) .await @@ -426,7 +427,70 @@ fn connect_succeeds() { // Must fail if connected. assert!(matches!( transport - .connect(WebRtcTransportRemoteParameters { dtls_parameters }) + .connect(WebRtcTransportRemoteParameters { + ice_parameters: None, + dtls_parameters + }) + .await, + Err(RequestError::Response { .. }), + )); + + assert_eq!(transport.dtls_parameters().role, DtlsRole::Server); + }); +} + +#[test] +fn connect_with_ice_parameters_succeeds() { + future::block_on(async move { + let (_worker, router) = init().await; + + let transport = router + .create_webrtc_transport(WebRtcTransportOptions::new( + WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: Some("9.9.9.1".to_string()), + port: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + }), + )) + .await + .expect("Failed to create WebRTC transport"); + + let ice_parameters = IceParameters { + username_fragment: "foo".to_string(), + password: "xxxx".to_string(), + ice_lite: None, + }; + + let dtls_parameters = DtlsParameters { + role: DtlsRole::Client, + fingerprints: vec![DtlsFingerprint::Sha256 { + value: [ + 0x82, 0x5A, 0x68, 0x3D, 0x36, 0xC3, 0x0A, 0xDE, 0xAF, 0xE7, 0x32, 0x43, 0xD2, + 0x88, 0x83, 0x57, 0xAC, 0x2D, 0x65, 0xE5, 0x80, 0xC4, 0xB6, 0xFB, 0xAF, 0x1A, + 0xA0, 0x21, 0x9F, 0x6D, 0x0C, 0xAD, + ], + }], + }; + + transport + .connect(WebRtcTransportRemoteParameters { + ice_parameters: Some(ice_parameters.clone()), + dtls_parameters: dtls_parameters.clone(), + }) + .await + .expect("Failed to establish WebRTC transport connection"); + + // Must fail if connected. + assert!(matches!( + transport + .connect(WebRtcTransportRemoteParameters { + ice_parameters: Some(ice_parameters), + dtls_parameters + }) .await, Err(RequestError::Response { .. }), )); diff --git a/worker/src/RTC/IceServer.cpp b/worker/src/RTC/IceServer.cpp index 133ed94a71..04d7a07892 100644 --- a/worker/src/RTC/IceServer.cpp +++ b/worker/src/RTC/IceServer.cpp @@ -82,7 +82,11 @@ namespace RTC { MS_TRACE(); - if (consentTimeoutSec < ConsentCheckMinTimeoutSec) + if (consentTimeoutSec == 0u) + { + // 0 means disabled so it's a valid value. + } + else if (consentTimeoutSec < ConsentCheckMinTimeoutSec) { MS_WARN_TAG( ice,