/* * Copyright 2025 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. An additional intellectual property rights grant can be found * in the file PATENTS. All contributing project authors may * be found in the AUTHORS file in the root of the source tree. */ #include "pc/sdp_munging_detector.h" #include #include #include #include #include "absl/algorithm/container.h" #include "absl/strings/str_split.h" #include "absl/strings/string_view.h" #include "api/field_trials_view.h" #include "api/jsep.h" #include "api/media_types.h" #include "api/uma_metrics.h" #include "media/base/codec.h" #include "media/base/media_constants.h" #include "media/base/stream_params.h" #include "p2p/base/transport_description.h" #include "p2p/base/transport_info.h" #include "pc/session_description.h" #include "rtc_base/checks.h" #include "rtc_base/logging.h" namespace webrtc { namespace { SdpMungingType DetermineTransportModification( const TransportInfos& last_created_transport_infos, const TransportInfos& transport_infos_to_set) { if (last_created_transport_infos.size() != transport_infos_to_set.size()) { RTC_LOG(LS_WARNING) << "SDP munging: Number of transport-infos does not " "match last created description."; // Number of transports should always match number of contents so this // should never happen. return SdpMungingType::kNumberOfContents; } for (size_t i = 0; i < last_created_transport_infos.size(); i++) { if (last_created_transport_infos[i].description.ice_ufrag != transport_infos_to_set[i].description.ice_ufrag) { RTC_LOG(LS_WARNING) << "SDP munging: ice-ufrag does not match last created description."; return SdpMungingType::kIceUfrag; } if (last_created_transport_infos[i].description.ice_pwd != transport_infos_to_set[i].description.ice_pwd) { RTC_LOG(LS_WARNING) << "SDP munging: ice-pwd does not match last created description."; return SdpMungingType::kIcePwd; } if (last_created_transport_infos[i].description.ice_mode != transport_infos_to_set[i].description.ice_mode) { RTC_LOG(LS_WARNING) << "SDP munging: ice mode does not match last created description."; return SdpMungingType::kIceMode; } if (last_created_transport_infos[i].description.connection_role != transport_infos_to_set[i].description.connection_role) { RTC_LOG(LS_WARNING) << "SDP munging: DTLS role does not match last created description."; return SdpMungingType::kDtlsSetup; } if (last_created_transport_infos[i].description.transport_options != transport_infos_to_set[i].description.transport_options) { RTC_LOG(LS_WARNING) << "SDP munging: ice_options does not match last " "created description."; bool created_renomination = absl::c_find( last_created_transport_infos[i].description.transport_options, ICE_OPTION_RENOMINATION) != last_created_transport_infos[i].description.transport_options.end(); bool set_renomination = absl::c_find(transport_infos_to_set[i].description.transport_options, ICE_OPTION_RENOMINATION) != transport_infos_to_set[i].description.transport_options.end(); if (!created_renomination && set_renomination) { return SdpMungingType::kIceOptionsRenomination; } bool created_trickle = absl::c_find( last_created_transport_infos[i].description.transport_options, ICE_OPTION_TRICKLE) != last_created_transport_infos[i].description.transport_options.end(); bool set_trickle = absl::c_find(transport_infos_to_set[i].description.transport_options, ICE_OPTION_TRICKLE) != transport_infos_to_set[i].description.transport_options.end(); if (created_trickle && !set_trickle) { return SdpMungingType::kIceOptionsTrickle; } return SdpMungingType::kIceOptions; } } return SdpMungingType::kNoModification; } SdpMungingType DetermineAudioSdpMungingType( const MediaContentDescription* last_created_media_description, const MediaContentDescription* media_description_to_set) { RTC_DCHECK(last_created_media_description); RTC_DCHECK(media_description_to_set); // Removing codecs should be done via setCodecPreferences or negotiation, not // munging. if (last_created_media_description->codecs().size() > media_description_to_set->codecs().size()) { RTC_LOG(LS_WARNING) << "SDP munging: audio codecs removed."; return SdpMungingType::kAudioCodecsRemoved; } // Adding audio codecs is measured after the more specific multiopus and L16 // checks. // Opus stereo modification required to enabled stereo playout for opus. bool created_opus_stereo = absl::c_find_if(last_created_media_description->codecs(), [](const Codec codec) { std::string value; return codec.name == kOpusCodecName && codec.GetParam(kCodecParamStereo, &value) && value == kParamValueTrue; }) != last_created_media_description->codecs().end(); bool set_opus_stereo = absl::c_find_if( media_description_to_set->codecs(), [](const Codec codec) { std::string value; return codec.name == kOpusCodecName && codec.GetParam(kCodecParamStereo, &value) && value == kParamValueTrue; }) != media_description_to_set->codecs().end(); if (!created_opus_stereo && set_opus_stereo) { RTC_LOG(LS_WARNING) << "SDP munging: Opus stereo enabled."; return SdpMungingType::kAudioCodecsFmtpOpusStereo; } // Nonstandard 5.1/7.1 opus variant. bool created_multiopus = absl::c_find_if(last_created_media_description->codecs(), [](const Codec codec) { return codec.name == "multiopus"; }) != last_created_media_description->codecs().end(); bool set_multiopus = absl::c_find_if(media_description_to_set->codecs(), [](const Codec codec) { return codec.name == "multiopus"; }) != media_description_to_set->codecs().end(); if (!created_multiopus && set_multiopus) { RTC_LOG(LS_WARNING) << "SDP munging: multiopus enabled."; return SdpMungingType::kAudioCodecsAddedMultiOpus; } // L16. bool created_l16 = absl::c_find_if(last_created_media_description->codecs(), [](const Codec codec) { return codec.name == kL16CodecName; }) != last_created_media_description->codecs().end(); bool set_l16 = absl::c_find_if(media_description_to_set->codecs(), [](const Codec codec) { return codec.name == kL16CodecName; }) != media_description_to_set->codecs().end(); if (!created_l16 && set_l16) { RTC_LOG(LS_WARNING) << "SDP munging: L16 enabled."; return SdpMungingType::kAudioCodecsAddedL16; } if (last_created_media_description->codecs().size() < media_description_to_set->codecs().size()) { RTC_LOG(LS_WARNING) << "SDP munging: audio codecs added."; return SdpMungingType::kAudioCodecsAdded; } // Audio NACK is not offered by default. bool created_nack = absl::c_find_if( last_created_media_description->codecs(), [](const Codec codec) { return codec.HasFeedbackParam(FeedbackParam(kRtcpFbParamNack)); }) != last_created_media_description->codecs().end(); bool set_nack = absl::c_find_if( media_description_to_set->codecs(), [](const Codec codec) { return codec.HasFeedbackParam(FeedbackParam(kRtcpFbParamNack)); }) != media_description_to_set->codecs().end(); if (!created_nack && set_nack) { RTC_LOG(LS_WARNING) << "SDP munging: audio nack enabled."; return SdpMungingType::kAudioCodecsRtcpFbAudioNack; } // RRTR is not offered by default. bool created_rrtr = absl::c_find_if( last_created_media_description->codecs(), [](const Codec codec) { return codec.HasFeedbackParam(FeedbackParam(kRtcpFbParamRrtr)); }) != last_created_media_description->codecs().end(); bool set_rrtr = absl::c_find_if( media_description_to_set->codecs(), [](const Codec codec) { return codec.HasFeedbackParam(FeedbackParam(kRtcpFbParamRrtr)); }) != media_description_to_set->codecs().end(); if (!created_rrtr && set_rrtr) { RTC_LOG(LS_WARNING) << "SDP munging: audio rrtr enabled."; return SdpMungingType::kAudioCodecsRtcpFbRrtr; } // Opus FEC is on by default. Should not be munged, can be controlled by // the other side. bool created_opus_fec = absl::c_find_if(last_created_media_description->codecs(), [](const Codec codec) { std::string value; return codec.name == kOpusCodecName && codec.GetParam(kCodecParamUseInbandFec, &value) && value == kParamValueTrue; }) != last_created_media_description->codecs().end(); bool set_opus_fec = absl::c_find_if( media_description_to_set->codecs(), [](const Codec codec) { std::string value; return codec.name == kOpusCodecName && codec.GetParam(kCodecParamUseInbandFec, &value) && value == kParamValueTrue; }) != media_description_to_set->codecs().end(); if (created_opus_fec && !set_opus_fec) { RTC_LOG(LS_WARNING) << "SDP munging: Opus FEC disabled."; return SdpMungingType::kAudioCodecsFmtpOpusFec; } // Opus DTX is off by default. Should not be munged, can be controlled by // the other side. bool created_opus_dtx = absl::c_find_if(last_created_media_description->codecs(), [](const Codec codec) { std::string value; return codec.name == kOpusCodecName && codec.GetParam(kCodecParamUseDtx, &value) && value == kParamValueTrue; }) != last_created_media_description->codecs().end(); bool set_opus_dtx = absl::c_find_if( media_description_to_set->codecs(), [](const Codec codec) { std::string value; return codec.name == kOpusCodecName && codec.GetParam(kCodecParamUseDtx, &value) && value == kParamValueTrue; }) != media_description_to_set->codecs().end(); if (!created_opus_dtx && set_opus_dtx) { RTC_LOG(LS_WARNING) << "SDP munging: Opus DTX enabled."; return SdpMungingType::kAudioCodecsFmtpOpusDtx; } // Opus CBR is off by default. Should not be munged, can be controlled by // the other side. bool created_opus_cbr = absl::c_find_if(last_created_media_description->codecs(), [](const Codec codec) { std::string value; return codec.name == kOpusCodecName && codec.GetParam(kCodecParamCbr, &value) && value == kParamValueTrue; }) != last_created_media_description->codecs().end(); bool set_opus_cbr = absl::c_find_if( media_description_to_set->codecs(), [](const Codec codec) { std::string value; return codec.name == kOpusCodecName && codec.GetParam(kCodecParamCbr, &value) && value == kParamValueTrue; }) != media_description_to_set->codecs().end(); if (!created_opus_cbr && set_opus_cbr) { RTC_LOG(LS_WARNING) << "SDP munging: Opus CBR enabled."; return SdpMungingType::kAudioCodecsFmtpOpusCbr; } return SdpMungingType::kNoModification; } SdpMungingType DetermineVideoSdpMungingType( const MediaContentDescription* last_created_media_description, const MediaContentDescription* media_description_to_set) { RTC_DCHECK(last_created_media_description); RTC_DCHECK(media_description_to_set); // Removing codecs should be done via setCodecPreferences or negotiation, not // munging. if (last_created_media_description->codecs().size() > media_description_to_set->codecs().size()) { RTC_LOG(LS_WARNING) << "SDP munging: video codecs removed."; return SdpMungingType::kVideoCodecsRemoved; } if (last_created_media_description->codecs().size() < media_description_to_set->codecs().size()) { // Nonstandard a=packetization:raw bool created_raw_packetization = absl::c_find_if(last_created_media_description->codecs(), [](const Codec codec) { return codec.packetization.has_value(); }) != last_created_media_description->codecs().end(); bool set_raw_packetization = absl::c_find_if(media_description_to_set->codecs(), [](const Codec codec) { return codec.packetization.has_value(); }) != media_description_to_set->codecs().end(); if (!created_raw_packetization && set_raw_packetization) { RTC_LOG(LS_WARNING) << "SDP munging: video codecs with raw packetization added."; return SdpMungingType::kVideoCodecsAddedWithRawPacketization; } RTC_LOG(LS_WARNING) << "SDP munging: video codecs added."; return SdpMungingType::kVideoCodecsAdded; } // Simulcast munging. if (last_created_media_description->streams().size() == 1 && media_description_to_set->streams().size() == 1) { bool created_sim = absl::c_find_if( last_created_media_description->streams()[0].ssrc_groups, [](const SsrcGroup group) { return group.semantics == kSimSsrcGroupSemantics; }) != last_created_media_description->streams()[0].ssrc_groups.end(); bool set_sim = absl::c_find_if(media_description_to_set->streams()[0].ssrc_groups, [](const SsrcGroup group) { return group.semantics == kSimSsrcGroupSemantics; }) != media_description_to_set->streams()[0].ssrc_groups.end(); if (!created_sim && set_sim) { RTC_LOG(LS_WARNING) << "SDP munging: legacy simulcast group created."; return SdpMungingType::kVideoCodecsLegacySimulcast; } } // sps-pps-idr-in-keyframe. bool created_sps_pps_idr_in_keyframe = absl::c_find_if(last_created_media_description->codecs(), [](const Codec codec) { std::string value; return codec.name == kH264CodecName && codec.GetParam(kH264FmtpSpsPpsIdrInKeyframe, &value) && value == kParamValueTrue; }) != last_created_media_description->codecs().end(); bool set_sps_pps_idr_in_keyframe = absl::c_find_if( media_description_to_set->codecs(), [](const Codec codec) { std::string value; return codec.name == kH264CodecName && codec.GetParam(kH264FmtpSpsPpsIdrInKeyframe, &value) && value == kParamValueTrue; }) != media_description_to_set->codecs().end(); if (!created_sps_pps_idr_in_keyframe && set_sps_pps_idr_in_keyframe) { RTC_LOG(LS_WARNING) << "SDP munging: sps-pps-idr-in-keyframe enabled."; return SdpMungingType::kVideoCodecsFmtpH264SpsPpsIdrInKeyframe; } return SdpMungingType::kNoModification; } } // namespace // Determine if the SDP was modified between createOffer and // setLocalDescription. SdpMungingType DetermineSdpMungingType( const SessionDescriptionInterface* sdesc, const SessionDescriptionInterface* last_created_desc) { if (!sdesc || !sdesc->description()) { RTC_LOG(LS_WARNING) << "SDP munging: Failed to parse session description."; return SdpMungingType::kUnknownModification; } if (!last_created_desc || !last_created_desc->description()) { RTC_LOG(LS_WARNING) << "SDP munging: SetLocalDescription called without " "CreateOffer or CreateAnswer."; if (sdesc->GetType() == SdpType::kOffer) { return SdpMungingType::kWithoutCreateOffer; } else { // answer or pranswer. return SdpMungingType::kWithoutCreateAnswer; } } // TODO: crbug.com/40567530 - we currently allow answer->pranswer // so can not check sdesc->GetType() == last_created_desc->GetType(). SdpMungingType type; // TODO: crbug.com/40567530 - change Chromium so that pointer comparison works // at least for implicit local description. if (sdesc->description() == last_created_desc->description()) { return SdpMungingType::kNoModification; } // Validate contents. const auto& last_created_contents = last_created_desc->description()->contents(); const auto& contents_to_set = sdesc->description()->contents(); if (last_created_contents.size() != contents_to_set.size()) { RTC_LOG(LS_WARNING) << "SDP munging: Number of m= sections does not match " "last created description."; return SdpMungingType::kNumberOfContents; } for (size_t content_index = 0; content_index < last_created_contents.size(); content_index++) { // TODO: crbug.com/40567530 - more checks are needed here. if (last_created_contents[content_index].mid() != contents_to_set[content_index].mid()) { RTC_LOG(LS_WARNING) << "SDP munging: mid does not match " "last created description."; return SdpMungingType::kMid; } auto* last_created_media_description = last_created_contents[content_index].media_description(); auto* media_description_to_set = contents_to_set[content_index].media_description(); if (!(last_created_media_description && media_description_to_set)) { continue; } // Validate video and audio contents. MediaType media_type = last_created_media_description->type(); bool is_rtp = media_type == MediaType::AUDIO || media_type == MediaType::VIDEO; if (!is_rtp) { // The checks that follow only apply for RTP-based contents. continue; } if (media_type == MediaType::VIDEO) { type = DetermineVideoSdpMungingType(last_created_media_description, media_description_to_set); if (type != SdpMungingType::kNoModification) { return type; } } else if (media_type == MediaType::AUDIO) { type = DetermineAudioSdpMungingType(last_created_media_description, media_description_to_set); if (type != SdpMungingType::kNoModification) { return type; } } // rtcp-mux. if (last_created_media_description->rtcp_mux() != media_description_to_set->rtcp_mux()) { RTC_LOG(LS_WARNING) << "SDP munging: rtcp-mux modified."; return SdpMungingType::kRtcpMux; } // rtcp-rsize. if (last_created_media_description->rtcp_reduced_size() != media_description_to_set->rtcp_reduced_size()) { RTC_LOG(LS_WARNING) << "SDP munging: rtcp-rsize modified."; return media_type == MediaType::AUDIO ? SdpMungingType::kAudioCodecsRtcpReducedSize : SdpMungingType::kVideoCodecsRtcpReducedSize; } // Validate codecs. We should have bailed out earlier if codecs were added // or removed. auto last_created_codecs = last_created_media_description->codecs(); auto codecs_to_set = media_description_to_set->codecs(); if (last_created_codecs.size() == codecs_to_set.size()) { for (size_t i = 0; i < last_created_codecs.size(); i++) { if (last_created_codecs[i] == codecs_to_set[i]) { continue; } // Codec position swapped. for (size_t j = i + 1; j < last_created_codecs.size(); j++) { if (last_created_codecs[i] == codecs_to_set[j]) { return media_type == MediaType::AUDIO ? SdpMungingType::kAudioCodecsReordered : SdpMungingType::kVideoCodecsReordered; } } // Same codec but id changed. if (last_created_codecs[i].name == codecs_to_set[i].name && last_created_codecs[i].id != codecs_to_set[i].id) { return SdpMungingType::kPayloadTypes; } if (last_created_codecs[i].params != codecs_to_set[i].params) { return media_type == MediaType::AUDIO ? SdpMungingType::kAudioCodecsFmtp : SdpMungingType::kVideoCodecsFmtp; } if (last_created_codecs[i].feedback_params != codecs_to_set[i].feedback_params) { return media_type == MediaType::AUDIO ? SdpMungingType::kAudioCodecsRtcpFb : SdpMungingType::kVideoCodecsRtcpFb; } // Nonstandard a=packetization:raw added by munging if (media_type == MediaType::VIDEO && last_created_codecs[i].packetization != codecs_to_set[i].packetization) { return SdpMungingType::kVideoCodecsModifiedWithRawPacketization; } // At this point clockrate or channels changed. This should already be // rejected later in the process so ignore for munging. } } // sendrecv et al. if (last_created_media_description->direction() != media_description_to_set->direction()) { RTC_LOG(LS_WARNING) << "SDP munging: transceiver direction modified."; return SdpMungingType::kDirection; } // Validate media streams. if (last_created_media_description->streams().size() != media_description_to_set->streams().size()) { RTC_LOG(LS_WARNING) << "SDP munging: streams size does not match last " "created description."; return SdpMungingType::kSsrcs; } for (size_t i = 0; i < last_created_media_description->streams().size(); i++) { if (last_created_media_description->streams()[i].ssrcs != media_description_to_set->streams()[i].ssrcs) { RTC_LOG(LS_WARNING) << "SDP munging: SSRCs do not match last created description."; return SdpMungingType::kSsrcs; } } // Validate RTP header extensions. auto last_created_extensions = last_created_media_description->rtp_header_extensions(); auto extensions_to_set = media_description_to_set->rtp_header_extensions(); if (last_created_extensions.size() < extensions_to_set.size()) { RTC_LOG(LS_WARNING) << "SDP munging: RTP header extension added."; return SdpMungingType::kRtpHeaderExtensionAdded; } if (last_created_extensions.size() > extensions_to_set.size()) { RTC_LOG(LS_WARNING) << "SDP munging: RTP header extension removed."; return SdpMungingType::kRtpHeaderExtensionRemoved; } for (size_t i = 0; i < last_created_extensions.size(); i++) { if (!(last_created_extensions[i].id == extensions_to_set[i].id)) { RTC_LOG(LS_WARNING) << "SDP munging: header extension modified."; return SdpMungingType::kRtpHeaderExtensionModified; } } } // Validate transport descriptions. type = DetermineTransportModification( last_created_desc->description()->transport_infos(), sdesc->description()->transport_infos()); if (type != SdpMungingType::kNoModification) { return type; } // TODO: crbug.com/40567530 - this serializes the descriptions back to a SDP // string which is very complex and we not should be be forced to rely on // string equality. std::string serialized_description; std::string serialized_last_description; if (sdesc->ToString(&serialized_description) && last_created_desc->ToString(&serialized_last_description) && serialized_description == serialized_last_description) { return SdpMungingType::kNoModification; } return SdpMungingType::kUnknownModification; } // Similar to DetermineSdpMungingType, but only checks whether the ICE ufrag or // pwd of the SDP has been modified between createOffer and setLocalDescription. bool HasUfragSdpMunging(const SessionDescriptionInterface* sdesc, const SessionDescriptionInterface* last_created_desc) { if (!sdesc || !sdesc->description()) { RTC_LOG(LS_WARNING) << "SDP munging: Failed to parse session description."; return false; } if (!last_created_desc || !last_created_desc->description()) { RTC_LOG(LS_WARNING) << "SDP munging: SetLocalDescription called without " "CreateOffer or CreateAnswer."; return false; } TransportInfos last_created_transport_infos = last_created_desc->description()->transport_infos(); TransportInfos transport_infos_to_set = sdesc->description()->transport_infos(); for (size_t i = 0; i < std::min(last_created_transport_infos.size(), transport_infos_to_set.size()); i++) { if (last_created_transport_infos[i].description.ice_ufrag != transport_infos_to_set[i].description.ice_ufrag) { return true; } if (last_created_transport_infos[i].description.ice_pwd != transport_infos_to_set[i].description.ice_pwd) { return true; } } return false; } bool IsSdpMungingAllowed(SdpMungingType sdp_munging_type, const FieldTrialsView& trials) { if (sdp_munging_type == SdpMungingType::kNoModification) { return true; } std::string type_as_string = std::to_string(static_cast(sdp_munging_type)); std::string trial; // NoSdpMangleReject is for rollout, disallowing specific types of munging // via Finch. It is a comma-separated list of SdpMungingTypes if (trials.IsEnabled("WebRTC-NoSdpMangleReject")) { trial = trials.Lookup("WebRTC-NoSdpMangleReject"); const std::vector rejected_types = absl::StrSplit(trial, ','); return absl::c_find(rejected_types, type_as_string) == rejected_types.end(); } // NoSdpMangleAllowForTesting is for running E2E tests which should reject // by default with a test-supplied list of exceptions as a comma-separated // list. if (!trials.IsEnabled("WebRTC-NoSdpMangleAllowForTesting")) { return true; } trial = trials.Lookup("WebRTC-NoSdpMangleAllowForTesting"); const std::vector allowed_types = absl::StrSplit(trial, ','); return absl::c_find(allowed_types, type_as_string) != allowed_types.end(); } } // namespace webrtc