From 7e6dc91095c32e5b87de3b0aa4158428758221db Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Sun, 27 Sep 2026 13:32:34 +0800 Subject: [PATCH 01/11] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=20V4L2=20=E7=BC=96=E7=A0=81=E5=8D=8F=E5=95=86?= =?UTF-8?q?=E5=B0=BA=E5=AF=B8=EF=BC=8C=E9=81=BF=E5=85=8D=E9=80=81=E5=B8=A7?= =?UTF-8?q?=E8=B6=8A=E7=95=8C=E5=B4=A9=E6=BA=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build/cross/Dockerfile.arm64 | 5 +++ build/cross/Dockerfile.armv7 | 5 +++ ...m2m-reject-undersized-encoder-format.patch | 36 +++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 build/cross/patches/0001-v4l2-m2m-reject-undersized-encoder-format.patch diff --git a/build/cross/Dockerfile.arm64 b/build/cross/Dockerfile.arm64 index dbf059b7..2aa20c2d 100644 --- a/build/cross/Dockerfile.arm64 +++ b/build/cross/Dockerfile.arm64 @@ -60,6 +60,7 @@ RUN dpkg --add-architecture arm64 # Install cross-compiler and native build tools RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ + patch \ pkg-config \ cmake \ nasm \ @@ -250,6 +251,9 @@ RUN github_prefix="" \ && make install \ && rm -rf /tmp/opus /tmp/opus.tar.gz +# Reject raw formats smaller than the requested frame before any V4L2 upload. +COPY build/cross/patches/0001-v4l2-m2m-reject-undersized-encoder-format.patch /tmp/ffmpeg-v4l2-size.patch + # Download and build FFmpeg with RKMPP support RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \ && github_prefix="" \ @@ -261,6 +265,7 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \ && git remote add origin "${github_prefix}https://github.com/nyanmisaka/ffmpeg-rockchip.git" \ && git fetch --depth 1 origin ${FFMPEG_ROCKCHIP_REV} \ && git checkout --detach FETCH_HEAD \ + && patch -p1 < /tmp/ffmpeg-v4l2-size.patch \ && cd .. \ # Build RKMPP && mkdir -p rkmpp/build && cd rkmpp/build \ diff --git a/build/cross/Dockerfile.armv7 b/build/cross/Dockerfile.armv7 index b1c52ef5..3e1931e4 100644 --- a/build/cross/Dockerfile.armv7 +++ b/build/cross/Dockerfile.armv7 @@ -60,6 +60,7 @@ RUN dpkg --add-architecture armhf # Install cross-compiler and native build tools RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ + patch \ pkg-config \ cmake \ nasm \ @@ -239,6 +240,9 @@ RUN github_prefix="" \ && make install \ && rm -rf /tmp/opus /tmp/opus.tar.gz +# Reject raw formats smaller than the requested frame before any V4L2 upload. +COPY build/cross/patches/0001-v4l2-m2m-reject-undersized-encoder-format.patch /tmp/ffmpeg-v4l2-size.patch + # Download and build FFmpeg with RKMPP support RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \ && github_prefix="" \ @@ -250,6 +254,7 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \ && git remote add origin "${github_prefix}https://github.com/nyanmisaka/ffmpeg-rockchip.git" \ && git fetch --depth 1 origin ${FFMPEG_ROCKCHIP_REV} \ && git checkout --detach FETCH_HEAD \ + && patch -p1 < /tmp/ffmpeg-v4l2-size.patch \ && cd .. \ # Build RKMPP && mkdir -p rkmpp/build && cd rkmpp/build \ diff --git a/build/cross/patches/0001-v4l2-m2m-reject-undersized-encoder-format.patch b/build/cross/patches/0001-v4l2-m2m-reject-undersized-encoder-format.patch new file mode 100644 index 00000000..1688b34b --- /dev/null +++ b/build/cross/patches/0001-v4l2-m2m-reject-undersized-encoder-format.patch @@ -0,0 +1,36 @@ +--- a/libavcodec/v4l2_m2m_enc.c ++++ b/libavcodec/v4l2_m2m_enc.c +@@ -338,6 +338,7 @@ + V4L2m2mPriv *priv = avctx->priv_data; + enum AVPixelFormat pix_fmt_output; + uint32_t v4l2_fmt_output; ++ unsigned int output_width, output_height; + int ret; + + ret = ff_v4l2_m2m_create_context(priv, &s); +@@ -370,6 +371,25 @@ + v4l2_fmt_output = output->format.fmt.pix_mp.pixelformat; + else + v4l2_fmt_output = output->format.fmt.pix.pixelformat; ++ ++ /* Drivers may clamp the requested raw format to their supported size. ++ * The software-frame upload still uses avctx/frame dimensions, so a ++ * smaller negotiated format would overrun the mapped output buffers. ++ * Allow upward alignment, but reject frames that do not fit. ++ */ ++ if (V4L2_TYPE_IS_MULTIPLANAR(output->type)) { ++ output_width = output->format.fmt.pix_mp.width; ++ output_height = output->format.fmt.pix_mp.height; ++ } else { ++ output_width = output->format.fmt.pix.width; ++ output_height = output->format.fmt.pix.height; ++ } ++ if (output_width < avctx->width || output_height < avctx->height) { ++ av_log(avctx, AV_LOG_ERROR, ++ "Encoder cannot fit requested %dx%d frame in negotiated %ux%u format\n", ++ avctx->width, avctx->height, output_width, output_height); ++ return AVERROR(EINVAL); ++ } + + pix_fmt_output = ff_v4l2_format_v4l2_to_avfmt(v4l2_fmt_output, AV_CODEC_ID_RAWVIDEO); + if (pix_fmt_output != avctx->pix_fmt) { From 1e0b0407d38eb143cf266c27c60b555d17edba0a Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Tue, 29 Sep 2026 20:32:29 +0800 Subject: [PATCH 02/11] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E9=80=82?= =?UTF-8?q?=E9=85=8D=20CH9329F=2071=20=E5=AD=97=E8=8A=82=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E5=93=8D=E5=BA=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hid/ch9329_proto.rs | 44 ++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/hid/ch9329_proto.rs b/src/hid/ch9329_proto.rs index 28eb9202..9fbf7bd0 100644 --- a/src/hid/ch9329_proto.rs +++ b/src/hid/ch9329_proto.rs @@ -9,8 +9,11 @@ pub const RESPONSE_ERROR_MASK: u8 = 0xC0; pub const DEFAULT_ADDR: u8 = 0x00; pub const DEFAULT_BAUD_RATE: u32 = 9600; pub const MAX_DATA_LEN: usize = 64; -pub const MAX_PACKET_SIZE: usize = 70; -const EXTENDED_PARAMETER_RESPONSE_SIZES: [usize; 2] = [72, 88]; +// Includes the 71-byte CH9329F GET_PARA_CFG response. +pub const MAX_PACKET_SIZE: usize = 71; +// This CH9329F firmware keeps LEN=50 but appends 15 reserved bytes before the +// checksum, making the complete response 71 bytes. +const EXTENDED_PARAMETER_RESPONSE_SIZE: usize = 71; pub mod cmd { pub const GET_INFO: u8 = 0x01; @@ -243,14 +246,10 @@ pub fn try_extract_response(buffer: &[u8]) -> Option<(Response, usize)> { && matches!(buffer[data_start], 0x00..=0x03 | 0x80..=0x83) && matches!(buffer[data_start + 1], 0x00..=0x02 | 0x80..=0x82); if parameter_payload_is_plausible { - for extended_size in EXTENDED_PARAMETER_RESPONSE_SIZES { - let extended_end = offset + extended_size; - if buffer.len() >= extended_end { - let checksum_index = extended_end - 1; - if calculate_checksum(&buffer[offset..checksum_index]) != buffer[checksum_index] - { - continue; - } + let extended_end = offset + EXTENDED_PARAMETER_RESPONSE_SIZE; + if buffer.len() >= extended_end { + let checksum_index = extended_end - 1; + if calculate_checksum(&buffer[offset..checksum_index]) == buffer[checksum_index] { return Some(( Response { cmd, @@ -263,7 +262,7 @@ pub fn try_extract_response(buffer: &[u8]) -> Option<(Response, usize)> { } } - if buffer.len() < offset + EXTENDED_PARAMETER_RESPONSE_SIZES[1] { + if buffer.len() < extended_end { return None; } } @@ -295,19 +294,18 @@ mod tests { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]; - for reserved_len in [16, 32] { - let mut frame = vec![0x57, 0xAB, DEFAULT_ADDR, 0x88, 50]; - frame.extend_from_slice(&payload); - frame.extend_from_slice(&vec![0; reserved_len]); - frame.push(calculate_checksum(&frame)); + let mut frame = vec![0x57, 0xAB, DEFAULT_ADDR, 0x88, 50]; + frame.extend_from_slice(&payload); + frame.extend_from_slice(&[0; 15]); + frame.push(calculate_checksum(&frame)); - assert!(Response::parse(&frame[..56]).is_none()); - let (response, consumed) = try_extract_response(&frame).expect("extended response"); - assert_eq!(response.cmd, 0x88); - assert_eq!(response.data, payload); - assert!(!response.is_error); - assert_eq!(consumed, frame.len()); - } + assert_eq!(frame.len(), 71); + assert!(Response::parse(&frame[..56]).is_none()); + let (response, consumed) = try_extract_response(&frame).expect("CH9329F response"); + assert_eq!(response.cmd, 0x88); + assert_eq!(response.data, payload); + assert!(!response.is_error); + assert_eq!(consumed, frame.len()); } #[test] From 99da8a3c61cacaa386e189601e344a63ba0547fa Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Tue, 29 Sep 2026 20:32:29 +0800 Subject: [PATCH 03/11] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E6=8C=89=20PT?= =?UTF-8?q?S=20=E7=94=9F=E6=88=90=20H264=20RTP=20=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E6=88=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/webrtc/universal_session.rs | 3 +- src/webrtc/video_track.rs | 156 ++++++++++++++++++++++++++------ 2 files changed, 129 insertions(+), 30 deletions(-) diff --git a/src/webrtc/universal_session.rs b/src/webrtc/universal_session.rs index 6b4a8059..26cd245b 100644 --- a/src/webrtc/universal_session.rs +++ b/src/webrtc/universal_session.rs @@ -671,9 +671,10 @@ impl UniversalSession { } let send_result = video_track - .write_frame_bytes( + .write_frame_bytes_at( encoded_frame.data.clone(), encoded_frame.is_keyframe, + Some(encoded_frame.pts_ms), ) .await; match send_result { diff --git a/src/webrtc/video_track.rs b/src/webrtc/video_track.rs index ceacf7fb..088dd5cc 100644 --- a/src/webrtc/video_track.rs +++ b/src/webrtc/video_track.rs @@ -1,6 +1,8 @@ -//! Multiplex H264/VP8/VP9 (`TrackLocalStaticSample`) vs H265 (`TrackLocalStaticRTP` + [`H265Payloader`]). +//! Send H.26x as RTP and VP8/VP9 through sample tracks. use bytes::Bytes; +use rtp::codecs::h264::H264Payloader; +use rtp::packetizer::Payloader; use std::sync::Arc; use std::time::Duration; use tokio::sync::Mutex; @@ -155,8 +157,38 @@ struct H265RtpState { timestamp_increment: u32, } -#[derive(Default)] +struct H264RtpClock { + timestamp: u32, + last_pts_ms: Option, + increment: u32, + started: bool, +} + +impl H264RtpClock { + fn next(&mut self, pts_ms: Option) -> u32 { + if self.started { + let ticks = match (self.last_pts_ms, pts_ms) { + (Some(previous), Some(current)) => { + current.saturating_sub(previous).saturating_mul(90).max(1) as u32 + } + _ => self.increment, + }; + self.timestamp = self.timestamp.wrapping_add(ticks); + } + self.started = true; + if let Some(pts) = pts_ms { + self.last_pts_ms = Some(self.last_pts_ms.map_or(pts, |last| last.max(pts))); + } else { + self.last_pts_ms = None; + } + self.timestamp + } +} + struct H264TrackState { + payloader: H264Payloader, + sequence_number: u16, + clock: H264RtpClock, sps: Option>, pps: Option>, } @@ -182,7 +214,7 @@ impl UniversalVideoTrack { rtcp_feedback: vec![], }; - let (track, h265_state) = if config.codec == VideoCodec::H265 { + let (track, h265_state) = if matches!(config.codec, VideoCodec::H264 | VideoCodec::H265) { let rtp_track = Arc::new(TrackLocalStaticRTP::new( codec_capability, config.track_id.clone(), @@ -196,7 +228,10 @@ impl UniversalVideoTrack { timestamp_increment: 90000 / config.fps.max(1), }; - (TrackType::Rtp(rtp_track), Some(Mutex::new(h265_state))) + ( + TrackType::Rtp(rtp_track), + (config.codec == VideoCodec::H265).then(|| Mutex::new(h265_state)), + ) } else { let sample_track = Arc::new(TrackLocalStaticSample::new( codec_capability, @@ -207,12 +242,24 @@ impl UniversalVideoTrack { (TrackType::Sample(sample_track), None) }; + let h264_state = H264TrackState { + sps: None, + pps: None, + payloader: H264Payloader::default(), + sequence_number: rand::random(), + clock: H264RtpClock { + timestamp: rand::random(), + last_pts_ms: None, + increment: 90000 / config.fps.max(1), + started: false, + }, + }; Self { track, codec: config.codec, config, h265_state, - h264_state: Mutex::new(H264TrackState::default()), + h264_state: Mutex::new(h264_state), } } @@ -232,12 +279,23 @@ impl UniversalVideoTrack { } pub async fn write_frame_bytes(&self, data: Bytes, is_keyframe: bool) -> Result<()> { + self.write_frame_bytes_at(data, is_keyframe, None).await + } + + /// Use the encoder's monotonic presentation time for H.264. Counting frames + /// compresses the RTP timeline whenever capture/encoding misses a frame. + pub async fn write_frame_bytes_at( + &self, + data: Bytes, + is_keyframe: bool, + pts_ms: Option, + ) -> Result<()> { if data.is_empty() { return Ok(()); } match self.codec { - VideoCodec::H264 => self.write_h264_frame(data, is_keyframe).await, + VideoCodec::H264 => self.write_h264_frame(data, pts_ms).await, VideoCodec::H265 => self.write_h265_frame(data, is_keyframe).await, VideoCodec::VP8 => self.write_vp8_frame(data, is_keyframe).await, VideoCodec::VP9 => self.write_vp9_frame(data, is_keyframe).await, @@ -249,16 +307,16 @@ impl UniversalVideoTrack { .await } - /// One Annex-B AU per sample so the stack can STAP/FU internally. - async fn write_h264_frame(&self, data: Bytes, _is_keyframe: bool) -> Result<()> { + /// Keep the stack's H.264 STAP/FU payloader, with explicit RTP timestamps. + async fn write_h264_frame(&self, data: Bytes, pts_ms: Option) -> Result<()> { let normalized = h264_bitstream::normalize_for_webrtc(data.as_ref()); let mut data = Bytes::from(normalized); let idr = h264_bitstream::is_keyframe(data.as_ref()); let has_parameter_sets = h264_bitstream::has_sps_pps(data.as_ref()); + let mut state = self.h264_state.lock().await; { - let mut state = self.h264_state.lock().await; let (sps, pps) = h264_bitstream::extract_sps_pps(data.as_ref()); if let Some(sps) = sps { state.sps = Some(sps); @@ -281,28 +339,37 @@ impl UniversalVideoTrack { } } - let frame_duration = Duration::from_micros(1_000_000 / self.config.fps.max(1) as u64); - let sample = Sample { - data, - duration: frame_duration, - ..Default::default() + let TrackType::Rtp(track) = &self.track else { + return Err(AppError::WebRtcError("H264 RTP track missing".to_owned())); }; - - match &self.track { - TrackType::Sample(track) => { - if let Err(e) = track.write_sample(&sample).await { - debug!("H264 write_sample failed: {}", e); - return Err(AppError::WebRtcError(format!( - "H264 write_sample failed: {}", - e - ))); - } - } - TrackType::Rtp(_) => { - warn!("H264 should not use RTP track"); - } + let payloads = state + .payloader + .payload(RTP_MTU - 12, &data) + .map_err(|e| AppError::WebRtcError(format!("H264 packetization failed: {e}")))?; + if payloads.is_empty() { + return Ok(()); + } + let timestamp = state.clock.next(pts_ms); + let count = payloads.len(); + // Serialize complete access units, including their sequence allocation. + for (index, payload) in payloads.into_iter().enumerate() { + let sequence_number = state.sequence_number; + state.sequence_number = state.sequence_number.wrapping_add(1); + let packet = rtp::packet::Packet { + header: rtp::header::Header { + version: 2, + marker: index + 1 == count, + sequence_number, + timestamp, + ..Default::default() + }, + payload, + }; + track + .write_rtp(&packet) + .await + .map_err(|e| AppError::WebRtcError(format!("H264 RTP write failed: {e}")))?; } - Ok(()) } @@ -435,6 +502,37 @@ impl UniversalVideoTrack { mod tests { use super::*; + fn test_clock(timestamp: u32) -> H264RtpClock { + H264RtpClock { + timestamp, + last_pts_ms: None, + increment: 3000, + started: false, + } + } + + #[test] + fn h264_timestamps_preserve_variable_frame_intervals() { + let mut clock = test_clock(1000); + assert_eq!(clock.next(Some(100)), 1000); + assert_eq!(clock.next(Some(133)), 3970); + // Missing input frames must leave a gap in media time too. + assert_eq!(clock.next(Some(300)), 19000); + assert_eq!(clock.next(Some(333)), 21970); + } + + #[test] + fn h264_timestamps_wrap_and_handle_legacy_or_repeated_pts() { + let mut clock = test_clock(u32::MAX - 89); + assert_eq!(clock.next(Some(0)), u32::MAX - 89); + assert_eq!(clock.next(Some(1)), 0); + assert_eq!(clock.next(Some(1)), 1); + assert_eq!(clock.next(Some(0)), 2); + assert_eq!(clock.next(Some(2)), 92); + assert_eq!(clock.next(None), 3092); + assert_eq!(clock.next(None), 6092); + } + #[test] fn test_video_codec_properties() { assert_eq!(VideoCodec::H264.mime_type(), "video/H264"); From 48cd06c7a4585b2d464de62f5a7b88ba65e48244 Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Tue, 29 Sep 2026 20:34:00 +0800 Subject: [PATCH 04/11] =?UTF-8?q?fix(web):=20=E7=A7=BB=E9=99=A4=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=E5=8F=B0=E8=A7=86=E9=A2=91=E5=8C=BA=E5=A4=96=E5=9B=B4?= =?UTF-8?q?=E7=95=99=E7=99=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/views/ConsoleView.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/views/ConsoleView.vue b/web/src/views/ConsoleView.vue index 953b5fa7..b7962dc0 100644 --- a/web/src/views/ConsoleView.vue +++ b/web/src/views/ConsoleView.vue @@ -3392,7 +3392,7 @@ onUnmounted(() => { :class="consoleLayout === 'sidebar' && 'pl-14 sm:pl-16'" >
-
+
Date: Thu, 1 Oct 2026 18:34:21 +0800 Subject: [PATCH 05/11] =?UTF-8?q?perf(video):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E8=A7=86=E9=A2=91=E6=B5=81=E6=B0=B4=E7=BA=BF=E5=B9=B6=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E4=BD=BF=E7=94=A8=20vendored=20OpenSSL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 使用固定采集、解码和编码线程及同步最新帧槽,减少同步异步跨界和无效 MJPEG 解码。 编码前预留消费者容量,以完整批次投递编码结果并隔离慢客户端,避免丢失 H.264 参考帧;保留 PTS 修正,不增加 IDR 节流。 复用 Annex-B 缓冲并增加低频发送分项统计,默认启用 vendored OpenSSL SRTP AES-CM 后端。 HTTP 客户端、HTTPS 服务端和本地 DTLS 证书验证迁移至 OpenSSL,移除 rustls TLS 引擎依赖,保留证书验证、媒体加密和原有协议协商。 --- Cargo.toml | 14 +- src/computer_use/openai.rs | 1 - src/main.rs | 14 +- src/utils/mod.rs | 1 + src/utils/tls.rs | 119 + src/video/codec/h264_bitstream.rs | 150 +- src/video/pipeline/delivery.rs | 100 + src/video/pipeline/dmabuf.rs | 135 +- src/video/pipeline/encoder_state.rs | 9 + src/video/pipeline/frame_mailbox.rs | 178 ++ src/video/pipeline/mod.rs | 3 + src/video/pipeline/shared.rs | 822 +++++- src/video/stream_manager.rs | 4 +- src/web/handlers/config/types.rs | 2 +- src/web/handlers/config/web.rs | 15 +- src/webrtc/universal_session.rs | 9 +- src/webrtc/video_track.rs | 202 +- vendor/dtls/Cargo.toml | 155 ++ vendor/dtls/LICENSE-APACHE | 201 ++ vendor/dtls/LICENSE-MIT | 21 + vendor/dtls/README.md | 20 + vendor/dtls/src/alert/alert_test.rs | 50 + vendor/dtls/src/alert/mod.rs | 189 ++ vendor/dtls/src/application_data.rs | 41 + vendor/dtls/src/certificate_verifier.rs | 378 +++ .../change_cipher_spec_test.rs | 37 + vendor/dtls/src/change_cipher_spec/mod.rs | 46 + .../cipher_suite/cipher_suite_aes_128_ccm.rs | 112 + .../cipher_suite_aes_128_gcm_sha256.rs | 107 + .../cipher_suite_aes_256_cbc_sha.rs | 107 + .../cipher_suite_chacha20_poly1305_sha256.rs | 107 + ..._suite_tls_ecdhe_ecdsa_with_aes_128_ccm.rs | 12 + ...suite_tls_ecdhe_ecdsa_with_aes_128_ccm8.rs | 12 + .../cipher_suite_tls_psk_with_aes_128_ccm.rs | 12 + .../cipher_suite_tls_psk_with_aes_128_ccm8.rs | 12 + ...r_suite_tls_psk_with_aes_128_gcm_sha256.rs | 90 + vendor/dtls/src/cipher_suite/mod.rs | 287 ++ vendor/dtls/src/client_certificate_type.rs | 16 + vendor/dtls/src/compression_methods.rs | 60 + vendor/dtls/src/config.rs | 193 ++ vendor/dtls/src/conn/conn_test.rs | 2464 +++++++++++++++++ vendor/dtls/src/conn/mod.rs | 1212 ++++++++ vendor/dtls/src/content.rs | 85 + vendor/dtls/src/crypto/crypto_cbc.rs | 136 + vendor/dtls/src/crypto/crypto_ccm.rs | 188 ++ vendor/dtls/src/crypto/crypto_chacha20.rs | 123 + vendor/dtls/src/crypto/crypto_gcm.rs | 119 + vendor/dtls/src/crypto/crypto_test.rs | 221 ++ vendor/dtls/src/crypto/mod.rs | 514 ++++ vendor/dtls/src/crypto/padding.rs | 122 + vendor/dtls/src/curve/mod.rs | 17 + vendor/dtls/src/curve/named_curve.rs | 83 + vendor/dtls/src/error.rs | 223 ++ .../src/extension/extension_server_name.rs | 56 + .../extension_server_name_test.rs | 26 + .../extension_supported_elliptic_curves.rs | 50 + ...xtension_supported_elliptic_curves_test.rs | 32 + .../extension_supported_point_formats.rs | 53 + .../extension_supported_point_formats_test.rs | 33 + ...xtension_supported_signature_algorithms.rs | 54 + ...ion_supported_signature_algorithms_test.rs | 48 + .../extension_use_extended_master_secret.rs | 39 + ...tension_use_extended_master_secret_test.rs | 32 + .../dtls/src/extension/extension_use_srtp.rs | 88 + .../extension_use_srtp_test.rs | 32 + vendor/dtls/src/extension/mod.rs | 130 + .../dtls/src/extension/renegotiation_info.rs | 53 + .../renegotiation_info_test.rs | 26 + vendor/dtls/src/flight/flight0.rs | 209 ++ vendor/dtls/src/flight/flight1.rs | 193 ++ vendor/dtls/src/flight/flight2.rs | 131 + vendor/dtls/src/flight/flight3.rs | 470 ++++ vendor/dtls/src/flight/flight4.rs | 853 ++++++ vendor/dtls/src/flight/flight5.rs | 779 ++++++ vendor/dtls/src/flight/flight6.rs | 190 ++ vendor/dtls/src/flight/mod.rs | 87 + .../fragment_buffer/fragment_buffer_test.rs | 174 ++ vendor/dtls/src/fragment_buffer/mod.rs | 160 ++ vendor/dtls/src/handshake/handshake_cache.rs | 241 ++ .../handshake_cache/handshake_cache_test.rs | 658 +++++ vendor/dtls/src/handshake/handshake_header.rs | 50 + .../handshake_message_certificate.rs | 70 + .../handshake_message_certificate_test.rs | 79 + .../handshake_message_certificate_request.rs | 77 + ...dshake_message_certificate_request_test.rs | 64 + .../handshake_message_certificate_verify.rs | 52 + ...ndshake_message_certificate_verify_test.rs | 40 + .../handshake_message_client_hello.rs | 196 ++ .../handshake_message_client_hello_test.rs | 75 + .../handshake_message_client_key_exchange.rs | 70 + ...dshake_message_client_key_exchange_test.rs | 35 + .../handshake/handshake_message_finished.rs | 34 + .../handshake_message_finished_test.rs | 32 + .../handshake_message_hello_verify_request.rs | 75 + ...shake_message_hello_verify_request_test.rs | 40 + .../handshake_message_server_hello.rs | 151 + .../handshake_message_server_hello_test.rs | 56 + .../handshake_message_server_hello_done.rs | 27 + ...andshake_message_server_hello_done_test.rs | 28 + .../handshake_message_server_key_exchange.rs | 133 + ...dshake_message_server_key_exchange_test.rs | 50 + vendor/dtls/src/handshake/handshake_random.rs | 71 + vendor/dtls/src/handshake/handshake_test.rs | 70 + vendor/dtls/src/handshake/mod.rs | 242 ++ vendor/dtls/src/handshaker.rs | 425 +++ vendor/dtls/src/lib.rs | 58 + vendor/dtls/src/listener.rs | 95 + vendor/dtls/src/prf/mod.rs | 315 +++ vendor/dtls/src/prf/prf_test.rs | 261 ++ vendor/dtls/src/record_layer/mod.rs | 111 + .../src/record_layer/record_layer_header.rs | 94 + .../src/record_layer/record_layer_test.rs | 118 + .../dtls/src/signature_hash_algorithm/mod.rs | 215 ++ .../signature_hash_algorithm_test.rs | 141 + vendor/dtls/src/state.rs | 336 +++ 115 files changed, 18160 insertions(+), 273 deletions(-) create mode 100644 src/utils/tls.rs create mode 100644 src/video/pipeline/delivery.rs create mode 100644 src/video/pipeline/frame_mailbox.rs create mode 100644 vendor/dtls/Cargo.toml create mode 100644 vendor/dtls/LICENSE-APACHE create mode 100644 vendor/dtls/LICENSE-MIT create mode 100644 vendor/dtls/README.md create mode 100644 vendor/dtls/src/alert/alert_test.rs create mode 100644 vendor/dtls/src/alert/mod.rs create mode 100644 vendor/dtls/src/application_data.rs create mode 100644 vendor/dtls/src/certificate_verifier.rs create mode 100644 vendor/dtls/src/change_cipher_spec/change_cipher_spec_test.rs create mode 100644 vendor/dtls/src/change_cipher_spec/mod.rs create mode 100644 vendor/dtls/src/cipher_suite/cipher_suite_aes_128_ccm.rs create mode 100644 vendor/dtls/src/cipher_suite/cipher_suite_aes_128_gcm_sha256.rs create mode 100644 vendor/dtls/src/cipher_suite/cipher_suite_aes_256_cbc_sha.rs create mode 100644 vendor/dtls/src/cipher_suite/cipher_suite_chacha20_poly1305_sha256.rs create mode 100644 vendor/dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm.rs create mode 100644 vendor/dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8.rs create mode 100644 vendor/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm.rs create mode 100644 vendor/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm8.rs create mode 100644 vendor/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_gcm_sha256.rs create mode 100644 vendor/dtls/src/cipher_suite/mod.rs create mode 100644 vendor/dtls/src/client_certificate_type.rs create mode 100644 vendor/dtls/src/compression_methods.rs create mode 100644 vendor/dtls/src/config.rs create mode 100644 vendor/dtls/src/conn/conn_test.rs create mode 100644 vendor/dtls/src/conn/mod.rs create mode 100644 vendor/dtls/src/content.rs create mode 100644 vendor/dtls/src/crypto/crypto_cbc.rs create mode 100644 vendor/dtls/src/crypto/crypto_ccm.rs create mode 100644 vendor/dtls/src/crypto/crypto_chacha20.rs create mode 100644 vendor/dtls/src/crypto/crypto_gcm.rs create mode 100644 vendor/dtls/src/crypto/crypto_test.rs create mode 100644 vendor/dtls/src/crypto/mod.rs create mode 100644 vendor/dtls/src/crypto/padding.rs create mode 100644 vendor/dtls/src/curve/mod.rs create mode 100644 vendor/dtls/src/curve/named_curve.rs create mode 100644 vendor/dtls/src/error.rs create mode 100644 vendor/dtls/src/extension/extension_server_name.rs create mode 100644 vendor/dtls/src/extension/extension_server_name/extension_server_name_test.rs create mode 100644 vendor/dtls/src/extension/extension_supported_elliptic_curves.rs create mode 100644 vendor/dtls/src/extension/extension_supported_elliptic_curves/extension_supported_elliptic_curves_test.rs create mode 100644 vendor/dtls/src/extension/extension_supported_point_formats.rs create mode 100644 vendor/dtls/src/extension/extension_supported_point_formats/extension_supported_point_formats_test.rs create mode 100644 vendor/dtls/src/extension/extension_supported_signature_algorithms.rs create mode 100644 vendor/dtls/src/extension/extension_supported_signature_algorithms/extension_supported_signature_algorithms_test.rs create mode 100644 vendor/dtls/src/extension/extension_use_extended_master_secret.rs create mode 100644 vendor/dtls/src/extension/extension_use_extended_master_secret/extension_use_extended_master_secret_test.rs create mode 100644 vendor/dtls/src/extension/extension_use_srtp.rs create mode 100644 vendor/dtls/src/extension/extension_use_srtp/extension_use_srtp_test.rs create mode 100644 vendor/dtls/src/extension/mod.rs create mode 100644 vendor/dtls/src/extension/renegotiation_info.rs create mode 100644 vendor/dtls/src/extension/renegotiation_info/renegotiation_info_test.rs create mode 100644 vendor/dtls/src/flight/flight0.rs create mode 100644 vendor/dtls/src/flight/flight1.rs create mode 100644 vendor/dtls/src/flight/flight2.rs create mode 100644 vendor/dtls/src/flight/flight3.rs create mode 100644 vendor/dtls/src/flight/flight4.rs create mode 100644 vendor/dtls/src/flight/flight5.rs create mode 100644 vendor/dtls/src/flight/flight6.rs create mode 100644 vendor/dtls/src/flight/mod.rs create mode 100644 vendor/dtls/src/fragment_buffer/fragment_buffer_test.rs create mode 100644 vendor/dtls/src/fragment_buffer/mod.rs create mode 100644 vendor/dtls/src/handshake/handshake_cache.rs create mode 100644 vendor/dtls/src/handshake/handshake_cache/handshake_cache_test.rs create mode 100644 vendor/dtls/src/handshake/handshake_header.rs create mode 100644 vendor/dtls/src/handshake/handshake_message_certificate.rs create mode 100644 vendor/dtls/src/handshake/handshake_message_certificate/handshake_message_certificate_test.rs create mode 100644 vendor/dtls/src/handshake/handshake_message_certificate_request.rs create mode 100644 vendor/dtls/src/handshake/handshake_message_certificate_request/handshake_message_certificate_request_test.rs create mode 100644 vendor/dtls/src/handshake/handshake_message_certificate_verify.rs create mode 100644 vendor/dtls/src/handshake/handshake_message_certificate_verify/handshake_message_certificate_verify_test.rs create mode 100644 vendor/dtls/src/handshake/handshake_message_client_hello.rs create mode 100644 vendor/dtls/src/handshake/handshake_message_client_hello/handshake_message_client_hello_test.rs create mode 100644 vendor/dtls/src/handshake/handshake_message_client_key_exchange.rs create mode 100644 vendor/dtls/src/handshake/handshake_message_client_key_exchange/handshake_message_client_key_exchange_test.rs create mode 100644 vendor/dtls/src/handshake/handshake_message_finished.rs create mode 100644 vendor/dtls/src/handshake/handshake_message_finished/handshake_message_finished_test.rs create mode 100644 vendor/dtls/src/handshake/handshake_message_hello_verify_request.rs create mode 100644 vendor/dtls/src/handshake/handshake_message_hello_verify_request/handshake_message_hello_verify_request_test.rs create mode 100644 vendor/dtls/src/handshake/handshake_message_server_hello.rs create mode 100644 vendor/dtls/src/handshake/handshake_message_server_hello/handshake_message_server_hello_test.rs create mode 100644 vendor/dtls/src/handshake/handshake_message_server_hello_done.rs create mode 100644 vendor/dtls/src/handshake/handshake_message_server_hello_done/handshake_message_server_hello_done_test.rs create mode 100644 vendor/dtls/src/handshake/handshake_message_server_key_exchange.rs create mode 100644 vendor/dtls/src/handshake/handshake_message_server_key_exchange/handshake_message_server_key_exchange_test.rs create mode 100644 vendor/dtls/src/handshake/handshake_random.rs create mode 100644 vendor/dtls/src/handshake/handshake_test.rs create mode 100644 vendor/dtls/src/handshake/mod.rs create mode 100644 vendor/dtls/src/handshaker.rs create mode 100644 vendor/dtls/src/lib.rs create mode 100644 vendor/dtls/src/listener.rs create mode 100644 vendor/dtls/src/prf/mod.rs create mode 100644 vendor/dtls/src/prf/prf_test.rs create mode 100644 vendor/dtls/src/record_layer/mod.rs create mode 100644 vendor/dtls/src/record_layer/record_layer_header.rs create mode 100644 vendor/dtls/src/record_layer/record_layer_test.rs create mode 100644 vendor/dtls/src/signature_hash_algorithm/mod.rs create mode 100644 vendor/dtls/src/signature_hash_algorithm/signature_hash_algorithm_test.rs create mode 100644 vendor/dtls/src/state.rs diff --git a/Cargo.toml b/Cargo.toml index c133dfae..f8b5e0bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ desktop = [ "dep:urlencoding", "dep:rust-embed", "dep:mime_guess", - "dep:rustls", + "dep:openssl", "dep:rcgen", "dep:axum-server", "dep:clap", @@ -110,8 +110,7 @@ base64 = { version = "0.22", optional = true } tempfile = { version = "3", optional = true } # HTTP client (for URL downloads) -# Use rustls by default, but allow native-tls for systems with older GLIBC -reqwest = { version = "0.13", features = ["stream", "rustls-no-provider", "json"], default-features = false, optional = true } +reqwest = { version = "0.13", features = ["stream", "native-tls-vendored", "json"], default-features = false, optional = true } urlencoding = { version = "2", optional = true } # Static file embedding @@ -119,9 +118,9 @@ rust-embed = { version = "8", features = ["compression", "debug-embed"], optiona mime_guess = { version = "2", optional = true } # TLS/HTTPS -rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"], optional = true } +openssl = { version = "0.10", features = ["vendored"], optional = true } rcgen = { version = "0.14", optional = true } -axum-server = { version = "0.8", features = ["tls-rustls-no-provider"], optional = true } +axum-server = { version = "0.8", features = ["tls-openssl"], optional = true } # CLI argument parsing clap = { version = "4", features = ["derive"], optional = true } @@ -148,7 +147,7 @@ parking_lot = { version = "0.12", optional = true } arc-swap = { version = "1.8", optional = true } # WebRTC -webrtc = { version = "0.17", optional = true } +webrtc = { version = "0.17", features = ["vendored-openssl"], optional = true } rtp = { version = "0.17", optional = true } rtsp-types = { version = "0.1", optional = true } sdp-types = { version = "0.1", optional = true } @@ -206,6 +205,9 @@ windows-sys = { version = "0.61", features = [ [build-dependencies] protobuf-codegen = "3.7" +[patch.crates-io] +dtls = { path = "vendor/dtls" } + [profile.release] opt-level = 3 lto = true diff --git a/src/computer_use/openai.rs b/src/computer_use/openai.rs index 4f4ec0c1..a6866c82 100644 --- a/src/computer_use/openai.rs +++ b/src/computer_use/openai.rs @@ -995,7 +995,6 @@ mod tests { use super::*; fn test_provider(endpoint: &str) -> OpenAiComputerProvider { - let _ = rustls::crypto::ring::default_provider().install_default(); OpenAiComputerProvider::new( "secret".to_string(), endpoint.to_string(), diff --git a/src/main.rs b/src/main.rs index 897b9f3d..42193d73 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,10 +4,9 @@ use std::io::Write; use std::net::{IpAddr, SocketAddr}; use std::path::PathBuf; -use axum_server::tls_rustls::RustlsConfig; +use axum_server::tls_openssl::OpenSSLAcceptor; use clap::{Args, Parser, Subcommand, ValueEnum}; use futures::{stream::FuturesUnordered, StreamExt}; -use rustls::crypto::{ring, CryptoProvider}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use one_kvm::auth::{SessionStore, TwoFactorService, UserStore}; @@ -17,6 +16,7 @@ use one_kvm::platform::PlatformCapabilities; use one_kvm::runtime::{RuntimeBuilder, WebConfigOverrides}; use one_kvm::state::ShutdownAction; use one_kvm::utils::bind_tcp_listener; +use one_kvm::utils::tls::server_config_from_pem_file; #[derive(Debug, Clone, Copy, Default, ValueEnum)] enum LogLevel { @@ -100,9 +100,6 @@ async fn main() -> anyhow::Result<()> { init_logging(args.log_level); - CryptoProvider::install_default(ring::default_provider()) - .expect("Failed to install rustls crypto provider"); - tracing::info!("Starting One-KVM v{}", env!("CARGO_PKG_VERSION")); let platform = PlatformCapabilities::current(); tracing::info!( @@ -187,7 +184,7 @@ async fn main() -> anyhow::Result<()> { let tls_config = if let (Some(cert_path), Some(key_path)) = (&config.web.ssl_cert_path, &config.web.ssl_key_path) { - RustlsConfig::from_pem_file(cert_path, key_path).await? + server_config_from_pem_file(cert_path, key_path).await? } else { let cert_dir = data_dir.join("certs"); let cert_path = cert_dir.join("server.crt"); @@ -203,7 +200,7 @@ async fn main() -> anyhow::Result<()> { tracing::info!("Using existing TLS certificate from {}", cert_dir.display()); } - RustlsConfig::from_pem_file(&cert_path, &key_path).await? + server_config_from_pem_file(&cert_path, &key_path).await? }; let servers = FuturesUnordered::new(); @@ -211,7 +208,8 @@ async fn main() -> anyhow::Result<()> { let local_addr = listener.local_addr()?; tracing::info!("Starting HTTPS server on {}", local_addr); - let server = axum_server::from_tcp_rustls(listener, tls_config.clone())? + let server = axum_server::from_tcp(listener)? + .acceptor(OpenSSLAcceptor::new(tls_config.clone())) .serve(app.clone().into_make_service()); servers.push(server); } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index dbd54519..1cdbb1c3 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -9,6 +9,7 @@ pub mod net; pub mod net; pub mod serial; pub mod throttle; +pub mod tls; pub use fs::{list_dir_names, read_trimmed}; pub use host::{hostname_from_etc, hostname_uname}; diff --git a/src/utils/tls.rs b/src/utils/tls.rs new file mode 100644 index 00000000..cdbd9636 --- /dev/null +++ b/src/utils/tls.rs @@ -0,0 +1,119 @@ +use axum_server::tls_openssl::OpenSSLConfig; +use openssl::pkey::PKey; +use openssl::ssl::{SslAcceptor, SslMethod, SslVersion}; +use openssl::x509::X509; +use std::io; +use std::path::Path; + +pub fn server_config_from_pem(cert_pem: &[u8], key_pem: &[u8]) -> io::Result { + let mut certificates = X509::stack_from_pem(cert_pem) + .map_err(io::Error::other)? + .into_iter(); + let certificate = certificates.next().ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidData, "TLS certificate chain is empty") + })?; + let private_key = PKey::private_key_from_pem(key_pem).map_err(io::Error::other)?; + let mut acceptor = + SslAcceptor::mozilla_intermediate_v5(SslMethod::tls_server()).map_err(io::Error::other)?; + acceptor + .set_min_proto_version(Some(SslVersion::TLS1_2)) + .map_err(io::Error::other)?; + acceptor + .set_certificate(&certificate) + .map_err(io::Error::other)?; + acceptor + .set_private_key(&private_key) + .map_err(io::Error::other)?; + for certificate in certificates { + acceptor + .add_extra_chain_cert(certificate) + .map_err(io::Error::other)?; + } + OpenSSLConfig::try_from(acceptor).map_err(io::Error::other) +} + +pub async fn server_config_from_pem_file( + cert_path: impl AsRef, + key_path: impl AsRef, +) -> io::Result { + let cert_pem = tokio::fs::read(cert_path).await?; + let key_pem = tokio::fs::read(key_path).await?; + server_config_from_pem(&cert_pem, &key_pem) +} + +#[cfg(test)] +mod tests { + use super::*; + use axum_server::tls_openssl::OpenSSLAcceptor; + + #[test] + fn preserves_certificate_chain() { + let leaf = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); + let intermediate = rcgen::generate_simple_self_signed(vec!["issuer".into()]).unwrap(); + let chain = format!("{}{}", leaf.cert.pem(), intermediate.cert.pem()); + let config = server_config_from_pem( + chain.as_bytes(), + leaf.signing_key.serialize_pem().as_bytes(), + ) + .unwrap(); + assert_eq!(config.get_inner().context().extra_chain_certs().len(), 1); + } + + #[test] + fn rejects_empty_invalid_and_mismatched_credentials() { + let first = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); + let second = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); + let cert = first.cert.pem(); + let key = first.signing_key.serialize_pem(); + assert!(server_config_from_pem(b"", key.as_bytes()).is_err()); + assert!(server_config_from_pem(b"invalid", key.as_bytes()).is_err()); + assert!(server_config_from_pem(cert.as_bytes(), b"invalid").is_err()); + assert!(server_config_from_pem( + cert.as_bytes(), + second.signing_key.serialize_pem().as_bytes(), + ) + .is_err()); + } + + #[tokio::test] + async fn https_client_verifies_certificates_and_supports_tls12() { + let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); + let cert_pem = cert.cert.pem(); + let config = server_config_from_pem( + cert_pem.as_bytes(), + cert.signing_key.serialize_pem().as_bytes(), + ) + .unwrap(); + let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + listener.set_nonblocking(true).unwrap(); + let address = listener.local_addr().unwrap(); + let handle = axum_server::Handle::new(); + let app = axum::Router::new().route("/", axum::routing::get(|| async { "tls-ok" })); + let server = axum_server::from_tcp(listener) + .unwrap() + .acceptor(OpenSSLAcceptor::new(config)) + .handle(handle.clone()) + .serve(app.into_make_service()); + let task = tokio::spawn(server); + handle.listening().await.unwrap(); + let url = format!("https://localhost:{}/", address.port()); + let root = reqwest::Certificate::from_pem(cert_pem.as_bytes()).unwrap(); + for tls12_only in [false, true] { + let mut client = reqwest::Client::builder() + .add_root_certificate(root.clone()) + .timeout(std::time::Duration::from_secs(5)); + if tls12_only { + client = client.max_tls_version(reqwest::tls::Version::TLS_1_2); + } + let response = client.build().unwrap().get(&url).send().await.unwrap(); + assert_eq!(response.text().await.unwrap(), "tls-ok"); + } + let untrusted = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(5)) + .build() + .unwrap(); + assert!(untrusted.get(&url).send().await.is_err()); + handle.shutdown(); + task.await.unwrap().unwrap(); + } +} diff --git a/src/video/codec/h264_bitstream.rs b/src/video/codec/h264_bitstream.rs index d47357e5..a6dddab5 100644 --- a/src/video/codec/h264_bitstream.rs +++ b/src/video/codec/h264_bitstream.rs @@ -69,63 +69,54 @@ pub fn strip_aud_nal_units(data: &[u8]) -> Vec { result } -pub fn extract_sps_pps(data: &[u8]) -> (Option>, Option>) { - let mut sps: Option> = None; - let mut pps: Option> = None; - let mut i = 0; +#[derive(Debug, Default)] +pub struct H264AccessUnit<'data> { + pub sps: Option<&'data [u8]>, + pub pps: Option<&'data [u8]>, + pub is_idr: bool, +} - while i < data.len() { - let start_code_len = if i + 4 <= data.len() - && data[i] == 0 - && data[i + 1] == 0 - && data[i + 2] == 0 - && data[i + 3] == 1 - { - 4 - } else if i + 3 <= data.len() && data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 { - 3 - } else { - i += 1; - continue; - }; - - let nal_start = i + start_code_len; - if nal_start >= data.len() { - break; - } - - let nal_type = data[nal_start] & 0x1F; - - let mut nal_end = data.len(); - let mut j = nal_start + 1; - while j + 3 <= data.len() { - if (data[j] == 0 && data[j + 1] == 0 && data[j + 2] == 1) - || (j + 4 <= data.len() - && data[j] == 0 - && data[j + 1] == 0 - && data[j + 2] == 0 - && data[j + 3] == 1) - { - nal_end = j; - break; +fn next_start_code(data: &[u8], mut offset: usize) -> Option<(usize, usize)> { + while offset + 3 <= data.len() { + if data[offset] == 0 && data[offset + 1] == 0 { + if data[offset + 2] == 1 { + return Some((offset, 3)); } - j += 1; - } - - match nal_type { - 7 => { - sps = Some(data[nal_start..nal_end].to_vec()); + if offset + 4 <= data.len() && data[offset + 2] == 0 && data[offset + 3] == 1 { + return Some((offset, 4)); } - 8 => { - pps = Some(data[nal_start..nal_end].to_vec()); - } - _ => {} } - - i = nal_end; + offset += 1; } + None +} - (sps, pps) +pub fn inspect_annex_b(data: &[u8]) -> H264AccessUnit<'_> { + let mut inspection = H264AccessUnit::default(); + let mut start_code = next_start_code(data, 0); + while let Some((offset, prefix_length)) = start_code { + let nal_start = offset + prefix_length; + let next = next_start_code(data, nal_start); + let nal_end = next.map_or(data.len(), |(next_offset, _)| next_offset); + if nal_start < nal_end { + match data[nal_start] & 0x1f { + 5 => inspection.is_idr = true, + 7 => inspection.sps = Some(&data[nal_start..nal_end]), + 8 => inspection.pps = Some(&data[nal_start..nal_end]), + _ => {} + } + } + start_code = next; + } + inspection +} + +pub fn extract_sps_pps(data: &[u8]) -> (Option>, Option>) { + let inspection = inspect_annex_b(data); + ( + inspection.sps.map(<[u8]>::to_vec), + inspection.pps.map(<[u8]>::to_vec), + ) } pub fn has_sps_pps(data: &[u8]) -> bool { @@ -215,8 +206,9 @@ pub fn parse_profile_level_id_from_sps(sps: &[u8]) -> Option { } pub fn extract_profile_level_id(data: &[u8]) -> Option { - let (sps, _) = extract_sps_pps(data); - sps.and_then(|sps_data| parse_profile_level_id_from_sps(&sps_data)) + inspect_annex_b(data) + .sps + .and_then(parse_profile_level_id_from_sps) } pub fn is_annex_b(data: &[u8]) -> bool { @@ -292,6 +284,58 @@ pub fn normalize_for_webrtc(data: &[u8]) -> Vec { mod tests { use super::*; + #[test] + fn inspects_parameter_sets_and_idr_in_one_pass() { + let data = [ + 0, 0, 0, 1, 0x67, 0x42, 0xe0, 0x1f, 0, 0, 1, 0x68, 0xce, 0, 0, 0, 1, 0x65, 0x88, + ]; + let inspection = inspect_annex_b(&data); + assert_eq!(inspection.sps, Some(&data[4..8])); + assert_eq!(inspection.pps, Some(&data[11..13])); + assert!(inspection.is_idr); + assert!(std::ptr::eq( + inspection.sps.unwrap().as_ptr(), + data[4..].as_ptr() + )); + assert_eq!(extract_profile_level_id(&data).as_deref(), Some("42e01f")); + } + + #[test] + fn predicted_packet_has_no_parameter_sets_or_idr() { + let data = [0, 0, 0, 1, 0x41, 0xc0]; + let inspection = inspect_annex_b(&data); + assert!(inspection.sps.is_none()); + assert!(inspection.pps.is_none()); + assert!(!inspection.is_idr); + } + + #[test] + fn empty_and_truncated_nal_units_are_ignored() { + for data in [ + &[][..], + &[0][..], + &[0, 0][..], + &[0, 0, 1][..], + &[0, 0, 0, 1][..], + &[0, 0, 1, 0, 0, 0, 1][..], + ] { + let inspection = inspect_annex_b(data); + assert!(inspection.sps.is_none()); + assert!(inspection.pps.is_none()); + assert!(!inspection.is_idr); + } + } + + #[test] + fn inspection_keeps_last_parameter_sets() { + let data = [ + 0, 0, 1, 0x67, 0x42, 0xc0, 0x1e, 0, 0, 0, 1, 0x67, 0x42, 0xe0, 0x1f, + ]; + let inspection = inspect_annex_b(&data); + assert_eq!(inspection.sps, Some(&data[11..15])); + assert_eq!(extract_profile_level_id(&data).as_deref(), Some("42e01f")); + } + #[test] fn detects_h264_keyframes() { let idr_frame = vec![0x00, 0x00, 0x00, 0x01, 0x65]; diff --git a/src/video/pipeline/delivery.rs b/src/video/pipeline/delivery.rs new file mode 100644 index 00000000..7296a8a6 --- /dev/null +++ b/src/video/pipeline/delivery.rs @@ -0,0 +1,100 @@ +use std::collections::VecDeque; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::Arc; +use std::time::Duration; + +use parking_lot::Mutex; +use tokio::sync::mpsc; + +use super::frame_mailbox::FrameMailbox; +use super::EncodedVideoFrame; +use crate::video::frame::VideoFrame; + +pub(super) type EncodedBatch = Arc<[Arc]>; +pub(super) type FrameMailboxHandle = Arc>>>>>; + +#[derive(Default)] +pub(super) struct DeliveryStats { + pub sent_frames: AtomicU64, + pub send_time_ns: AtomicU64, + pub send_errors: AtomicU64, +} + +pub(super) struct VideoSubscriber { + pub sender: mpsc::Sender, + pub needs_keyframe: AtomicBool, + pub recovery_requested: AtomicBool, +} + +pub(super) struct VideoFrameReservation { + pub subscriber: Arc, + pub permit: mpsc::OwnedPermit, +} + +pub struct EncodedVideoFrameReceiver { + receiver: mpsc::Receiver, + pending: VecDeque>, + mailbox: FrameMailboxHandle, + stats: Arc, +} + +impl EncodedVideoFrameReceiver { + pub(super) fn new( + receiver: mpsc::Receiver, + mailbox: FrameMailboxHandle, + stats: Arc, + ) -> Self { + Self { + receiver, + pending: VecDeque::new(), + mailbox, + stats, + } + } + + fn notify_capacity(&self) { + if let Some(mailbox) = self.mailbox.lock().as_ref() { + mailbox.notify(); + } + } + + pub async fn recv(&mut self) -> Option> { + loop { + if let Some(frame) = self.pending.pop_front() { + return Some(frame); + } + let batch = self.receiver.recv().await?; + self.pending.extend(batch.iter().cloned()); + self.notify_capacity(); + } + } + + pub fn try_recv(&mut self) -> Result, mpsc::error::TryRecvError> { + loop { + if let Some(frame) = self.pending.pop_front() { + return Ok(frame); + } + let batch = self.receiver.try_recv()?; + self.pending.extend(batch.iter().cloned()); + self.notify_capacity(); + } + } + + pub(crate) fn record_send(&self, elapsed: Duration, succeeded: bool) { + self.stats + .send_time_ns + .fetch_add(elapsed.as_nanos() as u64, Ordering::Relaxed); + if succeeded { + self.stats.sent_frames.fetch_add(1, Ordering::Relaxed); + } else { + self.stats.send_errors.fetch_add(1, Ordering::Relaxed); + } + } +} + +impl Drop for EncodedVideoFrameReceiver { + fn drop(&mut self) { + self.receiver.close(); + self.notify_capacity(); + } +} diff --git a/src/video/pipeline/dmabuf.rs b/src/video/pipeline/dmabuf.rs index 0fc9fba7..7fda10ae 100644 --- a/src/video/pipeline/dmabuf.rs +++ b/src/video/pipeline/dmabuf.rs @@ -256,60 +256,80 @@ fn run( } let capture = active.as_mut().expect("opened capture"); - let result = match capture.encoder.as_mut().expect("active encoder") { - CaptureEncoder::Dma(encoder) => { - let pts = pipeline.pts_ms(); - capture + let reservations = pipeline.reserve_outputs(); + let result = if reservations.as_ref().is_none_or(Vec::is_empty) { + match capture.encoder.as_mut().expect("active encoder") { + CaptureEncoder::Dma(_) => capture .stream - .with_next_dmabuf(|index, bytes_used, fresh_fd| { - let keyframe = pipeline.keyframe_requested.swap(false, Ordering::AcqRel); - // The callback holds the dequeue lease until native encode - // completes (or destroys MPP on error), before QBUF. - unsafe { encoder.encode(index, bytes_used, fresh_fd, pts, keyframe) } - }) - .map(|(_, packet)| { - packet - .map(|packet| { - let data = Bytes::from(packet); - let is_keyframe = match config.output_codec { - VideoEncoderType::H264 => h264_bitstream::is_keyframe(&data), - VideoEncoderType::H265 => h265_bitstream::is_keyframe(&data), - _ => false, - }; - let (data, is_keyframe) = pipeline.inspect_and_parameterize_packet( - config.output_codec, - data, - is_keyframe, - ); - if config.output_codec == VideoEncoderType::H264 { - pipeline.update_h264_profile_level_id(&data); - } - vec![EncodedVideoFrame { - data, - pts_ms: pts, - is_keyframe, - sequence: pipeline.sequence.fetch_add(1, Ordering::Relaxed) + 1, - duration: Duration::from_micros( - 1_000_000 / config.fps.max(1) as u64, - ), - codec: config.output_codec, - }] - }) - .map_err(AppError::VideoError) - }) + .with_next_dmabuf(|_, _, _| ()) + .map(|_| Ok(Vec::new())), + CaptureEncoder::Copy(_) => { + let mut raw = buffer_pool.take(0); + let result = capture.stream.next_into(&mut raw).map(|_| Ok(Vec::new())); + buffer_pool.put(raw); + result + } } - CaptureEncoder::Copy(encoder) => { - let mut raw = buffer_pool.take(0); - capture.stream.next_into(&mut raw).map(|meta| { - let frame = VideoFrame::from_pooled( - Arc::new(FrameBuffer::new(raw, Some(buffer_pool.clone()))), - config.resolution, - config.input_format, - capture.stream.stride(), - meta.sequence, - ); - pipeline.encode_frame_sync(encoder, &frame) - }) + } else { + match capture.encoder.as_mut().expect("active encoder") { + CaptureEncoder::Dma(encoder) => { + let pts = pipeline.pts_ms(); + capture + .stream + .with_next_dmabuf(|index, bytes_used, fresh_fd| { + let keyframe = + pipeline.keyframe_requested.swap(false, Ordering::AcqRel); + // The callback holds the dequeue lease until native encode + // completes (or destroys MPP on error), before QBUF. + unsafe { encoder.encode(index, bytes_used, fresh_fd, pts, keyframe) } + }) + .map(|(_, packet)| { + packet + .map(|packet| { + let data = Bytes::from(packet); + let is_keyframe = match config.output_codec { + VideoEncoderType::H264 => { + h264_bitstream::is_keyframe(&data) + } + VideoEncoderType::H265 => { + h265_bitstream::is_keyframe(&data) + } + _ => false, + }; + let (data, is_keyframe) = pipeline + .inspect_and_parameterize_packet( + config.output_codec, + data, + is_keyframe, + ); + vec![EncodedVideoFrame { + data, + pts_ms: pts, + is_keyframe, + sequence: pipeline.sequence.fetch_add(1, Ordering::Relaxed) + + 1, + duration: Duration::from_micros( + 1_000_000 / config.fps.max(1) as u64, + ), + codec: config.output_codec, + }] + }) + .map_err(AppError::VideoError) + }) + } + CaptureEncoder::Copy(encoder) => { + let mut raw = buffer_pool.take(0); + capture.stream.next_into(&mut raw).map(|meta| { + let frame = VideoFrame::from_pooled( + Arc::new(FrameBuffer::new(raw, Some(buffer_pool.clone()))), + config.resolution, + config.input_format, + capture.stream.stride(), + meta.sequence, + ); + pipeline.encode_frame_sync(encoder, &frame) + }) + } } }; @@ -321,10 +341,11 @@ fn run( config.input_format, config.fps, )); - for frame in frames { - pipeline.broadcast_encoded(Arc::new(frame)); - fps_frames += 1; - } + fps_frames += frames.len() as u32; + pipeline.broadcast_encoded_batch( + frames.into_iter().map(Arc::new).collect::>().into(), + reservations.unwrap_or_default(), + ); } Ok(Err(error)) => { if matches!(capture.encoder, Some(CaptureEncoder::Dma(_))) { @@ -380,7 +401,7 @@ fn run( } } if fps_start.elapsed() >= Duration::from_secs(1) { - pipeline.stats.blocking_lock().current_fps = + pipeline.stats.lock().current_fps = fps_frames as f32 / fps_start.elapsed().as_secs_f32(); fps_frames = 0; fps_start = Instant::now(); diff --git a/src/video/pipeline/encoder_state.rs b/src/video/pipeline/encoder_state.rs index 08b1cc3c..346365b8 100644 --- a/src/video/pipeline/encoder_state.rs +++ b/src/video/pipeline/encoder_state.rs @@ -41,6 +41,11 @@ pub(super) trait VideoEncoderTrait: Send { pub(super) struct EncodedFrame { pub(super) data: Bytes, pub(super) key: i32, + /// Packet pts in milliseconds, stamped by the encoder when the input + /// frame was accepted. With a pipelined encoder a packet can be handed + /// back one call later than its input frame, so consumers must use this + /// instead of the wall clock time of the current call. + pub(super) pts: i64, } struct H264EncoderWrapper(H264Encoder); @@ -53,6 +58,7 @@ impl VideoEncoderTrait for H264EncoderWrapper { .map(|f| EncodedFrame { data: f.data, key: f.key, + pts: f.pts, }) .collect()) } @@ -99,6 +105,7 @@ impl VideoEncoderTrait for H265EncoderWrapper { .map(|f| EncodedFrame { data: f.data, key: f.key, + pts: f.pts, }) .collect()) } @@ -126,6 +133,7 @@ impl VideoEncoderTrait for VP8EncoderWrapper { .map(|f| EncodedFrame { data: f.data.into(), key: f.key, + pts: f.pts, }) .collect()) } @@ -151,6 +159,7 @@ impl VideoEncoderTrait for VP9EncoderWrapper { .map(|f| EncodedFrame { data: f.data.into(), key: f.key, + pts: f.pts, }) .collect()) } diff --git a/src/video/pipeline/frame_mailbox.rs b/src/video/pipeline/frame_mailbox.rs new file mode 100644 index 00000000..2e0fa2c8 --- /dev/null +++ b/src/video/pipeline/frame_mailbox.rs @@ -0,0 +1,178 @@ +use std::sync::{Condvar, Mutex}; + +#[derive(Debug, PartialEq, Eq)] +pub(super) enum PublishResult { + Published, + Replaced, + Rejected, +} + +struct FrameState { + pending: Option, + last_sequence: Option, + closed: bool, +} + +pub(super) struct FrameMailbox { + state: Mutex>, + available: Condvar, +} + +impl FrameMailbox { + pub(super) fn new() -> Self { + Self { + state: Mutex::new(FrameState { + pending: None, + last_sequence: None, + closed: false, + }), + available: Condvar::new(), + } + } + + pub(super) fn publish(&self, sequence: u64, frame: Frame) -> PublishResult { + let mut state = self.state.lock().expect("frame mailbox lock poisoned"); + if state.closed || state.last_sequence.is_some_and(|last| sequence <= last) { + return PublishResult::Rejected; + } + state.last_sequence = Some(sequence); + let outcome = if state.pending.replace(frame).is_some() { + PublishResult::Replaced + } else { + PublishResult::Published + }; + self.available.notify_one(); + outcome + } + + #[cfg(test)] + pub(super) fn receive(&self) -> Option { + self.receive_when(|| Some(())).map(|(frame, ())| frame) + } + + pub(super) fn receive_when( + &self, + mut ready: impl FnMut() -> Option, + ) -> Option<(Frame, Ready)> { + let mut state = self.state.lock().expect("frame mailbox lock poisoned"); + loop { + if state.closed { + return None; + } + if state.pending.is_some() { + if let Some(ready) = ready() { + return state.pending.take().map(|frame| (frame, ready)); + } + } + state = self + .available + .wait(state) + .expect("frame mailbox lock poisoned"); + } + } + + pub(super) fn notify(&self) { + let _state = self.state.lock().expect("frame mailbox lock poisoned"); + self.available.notify_one(); + } + + pub(super) fn has_pending(&self) -> bool { + self.state + .lock() + .expect("frame mailbox lock poisoned") + .pending + .is_some() + } + + pub(super) fn close(&self) { + let mut state = self.state.lock().expect("frame mailbox lock poisoned"); + state.closed = true; + state.pending.take(); + self.available.notify_all(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::{mpsc, Arc}; + use std::time::Duration; + + #[test] + fn pending_frame_is_replaced_by_latest() { + let mailbox = FrameMailbox::new(); + assert_eq!(mailbox.publish(1, "first"), PublishResult::Published); + assert!(mailbox.has_pending()); + assert_eq!(mailbox.publish(2, "latest"), PublishResult::Replaced); + assert_eq!(mailbox.receive(), Some("latest")); + assert!(!mailbox.has_pending()); + } + + #[test] + fn consumed_sequence_still_rejects_old_and_duplicate_frames() { + let mailbox = FrameMailbox::new(); + assert_eq!(mailbox.publish(10, "current"), PublishResult::Published); + assert_eq!(mailbox.receive(), Some("current")); + assert_eq!(mailbox.publish(9, "old"), PublishResult::Rejected); + assert_eq!(mailbox.publish(10, "duplicate"), PublishResult::Rejected); + assert!(!mailbox.has_pending()); + assert_eq!(mailbox.publish(11, "next"), PublishResult::Published); + assert_eq!(mailbox.receive(), Some("next")); + } + + #[test] + fn close_discards_pending_and_rejects_future_frames() { + let mailbox = FrameMailbox::new(); + mailbox.publish(1, "pending"); + mailbox.close(); + assert!(!mailbox.has_pending()); + assert_eq!(mailbox.receive(), None); + assert_eq!(mailbox.publish(2, "late"), PublishResult::Rejected); + } + + #[test] + fn receiver_is_woken_by_publish() { + for sequence in 0..64 { + let mailbox = Arc::new(FrameMailbox::new()); + let receiver = mailbox.clone(); + let (ready_sender, ready_receiver) = mpsc::channel(); + let (result_sender, result_receiver) = mpsc::channel(); + let worker = std::thread::spawn(move || { + ready_sender.send(()).unwrap(); + result_sender.send(receiver.receive()).unwrap(); + }); + ready_receiver.recv().unwrap(); + mailbox.publish(sequence, sequence); + assert_eq!( + result_receiver + .recv_timeout(Duration::from_secs(2)) + .unwrap(), + Some(sequence) + ); + worker.join().unwrap(); + } + } + + #[test] + fn close_wakes_idle_receiver() { + for _ in 0..64 { + let mailbox = Arc::new(FrameMailbox::::new()); + let receiver = mailbox.clone(); + let (ready_sender, ready_receiver) = mpsc::channel(); + let (result_sender, result_receiver) = mpsc::channel(); + let worker = std::thread::spawn(move || { + ready_sender.send(()).unwrap(); + result_sender.send(receiver.receive()).unwrap(); + }); + ready_receiver.recv().unwrap(); + mailbox.close(); + assert_eq!( + result_receiver + .recv_timeout(Duration::from_secs(2)) + .unwrap(), + None + ); + worker.join().unwrap(); + } + } +} diff --git a/src/video/pipeline/mod.rs b/src/video/pipeline/mod.rs index e12e6e51..cbd0f99d 100644 --- a/src/video/pipeline/mod.rs +++ b/src/video/pipeline/mod.rs @@ -1,8 +1,11 @@ //! Video processing pipelines. +mod delivery; mod encoder_state; +mod frame_mailbox; mod shared; +pub use delivery::EncodedVideoFrameReceiver; pub use shared::{ EncodedVideoFrame, PipelineAppliedConfig, PipelineLifecycle, PipelineStateNotification, SharedVideoPipeline, SharedVideoPipelineConfig, SharedVideoPipelineStats, diff --git a/src/video/pipeline/shared.rs b/src/video/pipeline/shared.rs index 1269b43d..75b5f96e 100644 --- a/src/video/pipeline/shared.rs +++ b/src/video/pipeline/shared.rs @@ -24,10 +24,15 @@ use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering}; use std::sync::mpsc::{sync_channel, SyncSender, TrySendError}; use std::sync::Arc; use std::time::{Duration, Instant}; -use tokio::sync::{mpsc, watch, Mutex, RwLock}; +use tokio::sync::{mpsc, watch, RwLock}; use tracing::{debug, error, info, trace, warn}; +use super::delivery::{ + DeliveryStats, EncodedBatch, EncodedVideoFrameReceiver, FrameMailboxHandle, + VideoFrameReservation, VideoSubscriber, +}; use super::encoder_state::{build_encoder_state, should_parallel_decode_mjpeg, EncoderThreadState}; +use super::frame_mailbox::{FrameMailbox, PublishResult}; #[cfg(all(target_os = "linux", any(target_arch = "aarch64", target_arch = "arm")))] #[path = "dmabuf.rs"] @@ -151,6 +156,17 @@ use crate::video::recovery::{wait_for_source_change, CaptureRecoveryPolicy}; use crate::video::signal::SignalStatus; const MIN_CAPTURE_FRAME_SIZE: usize = 128; +type VideoFrameMailbox = FrameMailbox>; + +fn spawn_video_worker( + name: &'static str, + worker: impl FnOnce() + Send + 'static, +) -> std::io::Result> { + std::thread::Builder::new() + .name(name.to_string()) + .spawn(worker) +} + struct MjpegDecodeJob { data: Vec, sequence: u64, @@ -162,8 +178,7 @@ fn mjpeg_decode_worker_count(available_parallelism: usize) -> usize { fn spawn_mjpeg_decode_workers( pipeline: &Arc, - latest_frame: &Arc>>>, - frame_seq_tx: &watch::Sender, + frame_mailbox: &Arc, buffer_pool: &Arc, resolution: Resolution, ) -> Vec> { @@ -179,8 +194,7 @@ fn spawn_mjpeg_decode_workers( // latency behind stale frames. let (tx, rx) = sync_channel::(0); let worker_pipeline = pipeline.clone(); - let worker_latest_frame = latest_frame.clone(); - let worker_frame_seq_tx = frame_seq_tx.clone(); + let worker_frame_mailbox = frame_mailbox.clone(); let worker_buffer_pool = buffer_pool.clone(); let thread_name = format!("mjpeg-decoder-{worker_id}"); let spawn_result = std::thread::Builder::new() @@ -188,8 +202,20 @@ fn spawn_mjpeg_decode_workers( .spawn(move || { let mut decoder = MjpegToNv12Decoder::new(resolution); while let Ok(job) = rx.recv() { + if !worker_pipeline.running_flag.load(Ordering::Acquire) { + worker_buffer_pool.put(job.data); + break; + } + if worker_frame_mailbox.has_pending() { + worker_pipeline + .mjpeg_dropped_before_decode + .fetch_add(1, Ordering::Relaxed); + worker_buffer_pool.put(job.data); + continue; + } let nv12_size = resolution.width as usize * resolution.height as usize * 3 / 2; let mut nv12 = worker_buffer_pool.take(nv12_size); + let decode_started = Instant::now(); let decode_result = decoder.decode_into(&job.data, &mut nv12); worker_buffer_pool.put(job.data); @@ -198,7 +224,17 @@ fn spawn_mjpeg_decode_workers( warn!("Dropping undecodable MJPEG frame: {}", error); continue; } + worker_pipeline.mjpeg_decode_time_ns.fetch_add( + decode_started.elapsed().as_nanos() as u64, + Ordering::Relaxed, + ); + worker_pipeline + .mjpeg_decoded_frames + .fetch_add(1, Ordering::Relaxed); if !worker_pipeline.running_flag.load(Ordering::Acquire) { + worker_pipeline + .decoded_frames_not_encoded + .fetch_add(1, Ordering::Relaxed); worker_buffer_pool.put(nv12); break; } @@ -210,20 +246,11 @@ fn spawn_mjpeg_decode_workers( resolution.width, job.sequence, )); - let published = { - let mut latest = worker_latest_frame.write(); - if latest - .as_ref() - .is_some_and(|current| current.sequence >= job.sequence) - { - false - } else { - *latest = Some(frame); - true - } - }; - if published { - let _ = worker_frame_seq_tx.send(job.sequence.wrapping_add(1)); + if worker_frame_mailbox.publish(job.sequence, frame) != PublishResult::Published + { + worker_pipeline + .decoded_frames_not_encoded + .fetch_add(1, Ordering::Relaxed); } } }); @@ -461,10 +488,59 @@ fn log_encoding_error( } } +fn log_pipeline_performance( + stats: &SharedVideoPipelineStats, + previous: &SharedVideoPipelineStats, + elapsed: f64, +) { + let delta = |current: u64, previous: u64| current.saturating_sub(previous); + let decoded = delta(stats.mjpeg_decoded_frames, previous.mjpeg_decoded_frames); + let encoded_inputs = delta(stats.encoded_input_frames, previous.encoded_input_frames); + let encoded = delta(stats.encoded_frames, previous.encoded_frames); + let sent = delta(stats.sent_frames, previous.sent_frames); + let send_errors = delta(stats.send_errors, previous.send_errors); + let average_ms = |time_ns: u64, count: u64| { + if count == 0 { + 0.0 + } else { + time_ns as f64 / count as f64 / 1_000_000.0 + } + }; + info!( + "[VideoPerf] window_s={:.2} decoded_fps={:.2} encoded_fps={:.2} sent_fps={:.2} decode_ms={:.2} encode_ms={:.2} send_ms={:.2} encoded_mbps={:.2} dropped_before_decode={} decoded_unused={} backpressure_events={} slow_subscriber_skips={} send_errors={}", + elapsed, + decoded as f64 / elapsed, + encoded as f64 / elapsed, + sent as f64 / elapsed, + average_ms(delta(stats.mjpeg_decode_time_ns, previous.mjpeg_decode_time_ns), decoded), + average_ms(delta(stats.encode_time_ns, previous.encode_time_ns), encoded_inputs), + average_ms(delta(stats.send_time_ns, previous.send_time_ns), sent + send_errors), + delta(stats.encoded_bytes, previous.encoded_bytes) as f64 * 8.0 / elapsed / 1_000_000.0, + delta(stats.mjpeg_dropped_before_decode, previous.mjpeg_dropped_before_decode), + delta(stats.decoded_frames_not_encoded, previous.decoded_frames_not_encoded), + delta(stats.output_backpressure_events, previous.output_backpressure_events), + delta(stats.encoded_frames_skipped_for_slow_subscribers, previous.encoded_frames_skipped_for_slow_subscribers), + send_errors, + ); +} + /// Pipeline statistics #[derive(Debug, Clone, Default)] pub struct SharedVideoPipelineStats { pub current_fps: f32, + pub mjpeg_decoded_frames: u64, + pub mjpeg_dropped_before_decode: u64, + pub decoded_frames_not_encoded: u64, + pub mjpeg_decode_time_ns: u64, + pub encoded_input_frames: u64, + pub encoded_frames: u64, + pub encoded_bytes: u64, + pub encode_time_ns: u64, + pub output_backpressure_events: u64, + pub encoded_frames_skipped_for_slow_subscribers: u64, + pub sent_frames: u64, + pub send_time_ns: u64, + pub send_errors: u64, } #[derive(Default)] @@ -479,8 +555,21 @@ struct CachedH26xParameterSets { /// Universal shared video pipeline pub struct SharedVideoPipeline { config: RwLock, - subscribers: ParkingRwLock>>>, - stats: Mutex, + subscribers: ParkingRwLock>>, + stats: ParkingMutex, + frame_mailbox: FrameMailboxHandle, + mjpeg_decoded_frames: AtomicU64, + mjpeg_dropped_before_decode: AtomicU64, + decoded_frames_not_encoded: AtomicU64, + mjpeg_decode_time_ns: AtomicU64, + encoded_input_frames: AtomicU64, + encoded_frames: AtomicU64, + encoded_bytes: AtomicU64, + encode_time_ns: AtomicU64, + output_backpressure_events: AtomicU64, + output_backpressured: AtomicBool, + encoded_frames_skipped_for_slow_subscribers: AtomicU64, + delivery_stats: Arc, running: watch::Sender, running_rx: watch::Receiver, /// Becomes true only after the synchronous encoder worker has exited and @@ -527,7 +616,20 @@ impl SharedVideoPipeline { let pipeline = Arc::new(Self { config: RwLock::new(config), subscribers: ParkingRwLock::new(Vec::new()), - stats: Mutex::new(SharedVideoPipelineStats::default()), + stats: ParkingMutex::new(SharedVideoPipelineStats::default()), + frame_mailbox: Arc::new(ParkingMutex::new(None)), + mjpeg_decoded_frames: AtomicU64::new(0), + mjpeg_dropped_before_decode: AtomicU64::new(0), + decoded_frames_not_encoded: AtomicU64::new(0), + mjpeg_decode_time_ns: AtomicU64::new(0), + encoded_input_frames: AtomicU64::new(0), + encoded_frames: AtomicU64::new(0), + encoded_bytes: AtomicU64::new(0), + encode_time_ns: AtomicU64::new(0), + output_backpressure_events: AtomicU64::new(0), + output_backpressured: AtomicBool::new(false), + encoded_frames_skipped_for_slow_subscribers: AtomicU64::new(0), + delivery_stats: Arc::new(DeliveryStats::default()), running: running_tx, running_rx, encoder_done: encoder_done_tx, @@ -593,16 +695,21 @@ impl SharedVideoPipeline { } /// Subscribe to encoded frames - pub fn subscribe(&self) -> mpsc::Receiver> { - // A queued video frame is already stale when the next frame is ready. - // Keep at most one pending frame so a slow WebRTC writer cannot make - // the encoder wait or accumulate seconds of latency. + pub fn subscribe(&self) -> EncodedVideoFrameReceiver { let (tx, rx) = mpsc::channel(1); if let Some(frame) = self.bootstrap_frame.read().clone() { - let _ = tx.try_send(frame); + let batch: EncodedBatch = vec![frame].into(); + let _ = tx.try_send(batch); } - self.subscribers.write().push(tx); - rx + self.subscribers.write().push(Arc::new(VideoSubscriber { + sender: tx, + needs_keyframe: AtomicBool::new(true), + recovery_requested: AtomicBool::new(false), + })); + if let Some(mailbox) = self.frame_mailbox.lock().as_ref() { + mailbox.notify(); + } + EncodedVideoFrameReceiver::new(rx, self.frame_mailbox.clone(), self.delivery_stats.clone()) } /// Get subscriber count @@ -610,7 +717,7 @@ impl SharedVideoPipeline { self.subscribers .read() .iter() - .filter(|tx| !tx.is_closed()) + .filter(|subscriber| !subscriber.sender.is_closed()) .count() } @@ -671,7 +778,28 @@ impl SharedVideoPipeline { /// Get current stats pub async fn stats(&self) -> SharedVideoPipelineStats { - self.stats.lock().await.clone() + self.stats_snapshot() + } + + fn stats_snapshot(&self) -> SharedVideoPipelineStats { + let mut stats = self.stats.lock().clone(); + stats.mjpeg_decoded_frames = self.mjpeg_decoded_frames.load(Ordering::Relaxed); + stats.mjpeg_dropped_before_decode = + self.mjpeg_dropped_before_decode.load(Ordering::Relaxed); + stats.decoded_frames_not_encoded = self.decoded_frames_not_encoded.load(Ordering::Relaxed); + stats.mjpeg_decode_time_ns = self.mjpeg_decode_time_ns.load(Ordering::Relaxed); + stats.encoded_input_frames = self.encoded_input_frames.load(Ordering::Relaxed); + stats.encoded_frames = self.encoded_frames.load(Ordering::Relaxed); + stats.encoded_bytes = self.encoded_bytes.load(Ordering::Relaxed); + stats.encode_time_ns = self.encode_time_ns.load(Ordering::Relaxed); + stats.output_backpressure_events = self.output_backpressure_events.load(Ordering::Relaxed); + stats.encoded_frames_skipped_for_slow_subscribers = self + .encoded_frames_skipped_for_slow_subscribers + .load(Ordering::Relaxed); + stats.sent_frames = self.delivery_stats.sent_frames.load(Ordering::Relaxed); + stats.send_time_ns = self.delivery_stats.send_time_ns.load(Ordering::Relaxed); + stats.send_errors = self.delivery_stats.send_errors.load(Ordering::Relaxed); + stats } /// Check if running @@ -704,8 +832,8 @@ impl SharedVideoPipeline { self.h264_profile_level_id_rx.clone() } - fn update_h264_profile_level_id(&self, data: &[u8]) { - let Some(profile_level_id) = h264_bitstream::extract_profile_level_id(data) else { + fn update_h264_profile_level_id(&self, sps: &[u8]) { + let Some(profile_level_id) = h264_bitstream::parse_profile_level_id_from_sps(sps) else { return; }; if self.h264_profile_level_id.borrow().as_deref() == Some(profile_level_id.as_str()) { @@ -727,16 +855,19 @@ impl SharedVideoPipeline { if !was_annex_b && h264_bitstream::is_annex_b(data.as_ref()) { debug!("[Pipeline] Converted length-prefixed H264 packet to Annex-B"); } - let (sps, pps) = h264_bitstream::extract_sps_pps(data.as_ref()); + let inspection = h264_bitstream::inspect_annex_b(data.as_ref()); + let sps = inspection.sps; + let pps = inspection.pps; // Require metadata and payload to agree before advertising a // decoder bootstrap frame. - let is_idr = ffmpeg_keyframe && h264_bitstream::is_keyframe(data.as_ref()); + let is_idr = ffmpeg_keyframe && inspection.is_idr; let mut cache = self.parameter_sets.lock(); - if let Some(sps) = sps.as_ref() { - cache.h264_sps = Some(sps.clone()); + if let Some(sps) = sps { + self.update_h264_profile_level_id(sps); + cache.h264_sps = Some(sps.to_vec()); } - if let Some(pps) = pps.as_ref() { - cache.h264_pps = Some(pps.clone()); + if let Some(pps) = pps { + cache.h264_pps = Some(pps.to_vec()); } if !is_idr { @@ -808,30 +939,103 @@ impl SharedVideoPipeline { } } - fn broadcast_encoded(&self, frame: Arc) { - if frame.is_keyframe { + fn reserve_outputs(&self) -> Option> { + let subscribers = self.subscribers.read(); + let mut active_subscribers = 0; + let mut reservations = Vec::with_capacity(subscribers.len()); + for subscriber in subscribers.iter() { + if subscriber.sender.is_closed() { + continue; + } + active_subscribers += 1; + if let Ok(permit) = subscriber.sender.clone().try_reserve_owned() { + if subscriber.needs_keyframe.load(Ordering::Acquire) + && !subscriber.recovery_requested.swap(true, Ordering::AcqRel) + { + self.keyframe_requested.store(true, Ordering::Release); + } + reservations.push(VideoFrameReservation { + subscriber: subscriber.clone(), + permit, + }); + } + } + if active_subscribers > 0 && reservations.is_empty() { + if !self.output_backpressured.swap(true, Ordering::AcqRel) { + self.output_backpressure_events + .fetch_add(1, Ordering::Relaxed); + } + None + } else { + self.output_backpressured.store(false, Ordering::Release); + Some(reservations) + } + } + + fn broadcast_encoded_batch( + &self, + frames: EncodedBatch, + reservations: Vec, + ) { + if frames.is_empty() { + return; + } + self.encoded_frames + .fetch_add(frames.len() as u64, Ordering::Relaxed); + self.encoded_bytes.fetch_add( + frames.iter().map(|frame| frame.data.len() as u64).sum(), + Ordering::Relaxed, + ); + if let Some(frame) = frames.iter().rev().find(|frame| frame.is_keyframe) { *self.bootstrap_frame.write() = Some(frame.clone()); } - - let subscribers = { - let guard = self.subscribers.read(); - if guard.is_empty() { - return; + { + let mut subscribers = self.subscribers.write(); + subscribers.retain(|subscriber| !subscriber.sender.is_closed()); + for subscriber in subscribers.iter() { + if !reservations + .iter() + .any(|reservation| Arc::ptr_eq(&reservation.subscriber, subscriber)) + { + subscriber.needs_keyframe.store(true, Ordering::Release); + subscriber + .recovery_requested + .store(false, Ordering::Release); + self.encoded_frames_skipped_for_slow_subscribers + .fetch_add(frames.len() as u64, Ordering::Relaxed); + } } - guard.iter().cloned().collect::>() - }; - - for tx in &subscribers { - // Never await a consumer. A full one-slot queue means the - // consumer is behind; dropping this frame preserves bounded - // latency and the receiver's sequence-gap logic requests a fresh - // keyframe when necessary. - let _ = tx.try_send(frame.clone()); } - - if subscribers.iter().any(|tx| tx.is_closed()) { - let mut guard = self.subscribers.write(); - guard.retain(|tx| !tx.is_closed()); + for reservation in reservations { + let batch = if reservation + .subscriber + .needs_keyframe + .load(Ordering::Acquire) + { + let Some(first_keyframe) = frames.iter().position(|frame| frame.is_keyframe) else { + self.encoded_frames_skipped_for_slow_subscribers + .fetch_add(frames.len() as u64, Ordering::Relaxed); + continue; + }; + reservation + .subscriber + .needs_keyframe + .store(false, Ordering::Release); + reservation + .subscriber + .recovery_requested + .store(false, Ordering::Release); + self.encoded_frames_skipped_for_slow_subscribers + .fetch_add(first_keyframe as u64, Ordering::Relaxed); + if first_keyframe == 0 { + frames.clone() + } else { + frames[first_keyframe..].to_vec().into() + } + } else { + frames.clone() + }; + reservation.permit.send(batch); } } @@ -951,14 +1155,34 @@ impl SharedVideoPipeline { info!("Using parallel libyuv MJPEG decode with hardware encoding"); } let mut encoder_state = build_encoder_state(&encoder_config)?; + let frame_mailbox = Arc::new(VideoFrameMailbox::new()); + *self.frame_mailbox.lock() = Some(frame_mailbox.clone()); + self.mjpeg_decoded_frames.store(0, Ordering::Relaxed); + self.mjpeg_dropped_before_decode.store(0, Ordering::Relaxed); + self.decoded_frames_not_encoded.store(0, Ordering::Relaxed); + self.mjpeg_decode_time_ns.store(0, Ordering::Relaxed); + self.encoded_input_frames.store(0, Ordering::Relaxed); + self.encoded_frames.store(0, Ordering::Relaxed); + self.encoded_bytes.store(0, Ordering::Relaxed); + self.encode_time_ns.store(0, Ordering::Relaxed); + self.output_backpressure_events.store(0, Ordering::Relaxed); + self.output_backpressured.store(false, Ordering::Release); + self.encoded_frames_skipped_for_slow_subscribers + .store(0, Ordering::Relaxed); + self.delivery_stats.sent_frames.store(0, Ordering::Relaxed); + self.delivery_stats.send_time_ns.store(0, Ordering::Relaxed); + self.delivery_stats.send_errors.store(0, Ordering::Relaxed); + for subscriber in self.subscribers.read().iter() { + subscriber.needs_keyframe.store(true, Ordering::Release); + subscriber + .recovery_requested + .store(false, Ordering::Release); + } let _ = self.running.send(true); let _ = self.encoder_done.send(false); self.running_flag.store(true, Ordering::Release); let pipeline = self.clone(); - let latest_frame: Arc>>> = - Arc::new(ParkingRwLock::new(None)); - let (frame_seq_tx, mut frame_seq_rx) = watch::channel(0u64); let buffer_pool = Arc::new(FrameBufferPool::new(buffer_count.max(4) as usize)); let (cmd_tx, mut cmd_rx) = tokio::sync::mpsc::unbounded_channel(); { @@ -969,32 +1193,34 @@ impl SharedVideoPipeline { // Encoder loop uses a dedicated OS thread because FFmpeg work is synchronous. { let pipeline = pipeline.clone(); - let latest_frame = latest_frame.clone(); - let handle = tokio::runtime::Handle::current(); - std::thread::spawn(move || { + let frame_mailbox = frame_mailbox.clone(); + let encoder_worker = spawn_video_worker("video-encoder", move || { let mut input_frame_count: u64 = 0; let mut encoded_frame_count: u64 = 0; let mut last_fps_time = Instant::now(); let mut fps_frame_count: u64 = 0; - let mut last_seq = *frame_seq_rx.borrow(); + let perf_logging = std::env::var("ONE_KVM_VIDEO_STATS").as_deref() == Ok("1"); + let mut last_perf_time = Instant::now(); + let mut last_perf_stats = pipeline.stats_snapshot(); let encode_error_throttler = LogThrottler::with_secs(ENCODE_ERROR_THROTTLE_SECS); let mut suppressed_encode_errors: HashMap = HashMap::new(); while pipeline.running_flag.load(Ordering::Acquire) { - if handle.block_on(frame_seq_rx.changed()).is_err() { + let Some((frame, reservations)) = + frame_mailbox.receive_when(|| pipeline.reserve_outputs()) + else { break; - } + }; if !pipeline.running_flag.load(Ordering::Acquire) { break; } - let seq = *frame_seq_rx.borrow(); - if seq == last_seq { - continue; - } - last_seq = seq; - - if pipeline.subscriber_count() == 0 { + if reservations.is_empty() { + if parallel_mjpeg_decode { + pipeline + .decoded_frames_not_encoded + .fetch_add(1, Ordering::Relaxed); + } continue; } @@ -1004,26 +1230,28 @@ impl SharedVideoPipeline { } } - let frame = { - let guard = latest_frame.read(); - guard.clone() - }; - let frame = match frame { - Some(f) => f, - None => continue, - }; - input_frame_count = input_frame_count.wrapping_add(1); + pipeline + .encoded_input_frames + .fetch_add(1, Ordering::Relaxed); + let encode_started = Instant::now(); + let encode_result = pipeline.encode_frame_sync(&mut encoder_state, &frame); + pipeline.encode_time_ns.fetch_add( + encode_started.elapsed().as_nanos() as u64, + Ordering::Relaxed, + ); - match pipeline.encode_frame_sync(&mut encoder_state, &frame) { + match encode_result { Ok(encoded_frames) => { - for encoded_frame in encoded_frames { - let encoded_arc = Arc::new(encoded_frame); - pipeline.broadcast_encoded(encoded_arc); - - encoded_frame_count = encoded_frame_count.wrapping_add(1); - fps_frame_count += 1; - } + encoded_frame_count = + encoded_frame_count.wrapping_add(encoded_frames.len() as u64); + fps_frame_count += encoded_frames.len() as u64; + let frames: EncodedBatch = encoded_frames + .into_iter() + .map(Arc::new) + .collect::>() + .into(); + pipeline.broadcast_encoded_batch(frames, reservations); } Err(e) => { log_encoding_error( @@ -1040,16 +1268,24 @@ impl SharedVideoPipeline { fps_frame_count = 0; last_fps_time = Instant::now(); - handle.block_on(async { - let mut s = pipeline.stats.lock().await; - s.current_fps = current_fps; - }); + pipeline.stats.lock().current_fps = current_fps; trace!( - "Shared pipeline processed {} input frames, emitted {} encoded frames", + "Shared pipeline processed {} input frames, emitted {} encoded frames; MJPEG decoded={}, dropped before decode={}, decoded but not encoded={}", input_frame_count, - encoded_frame_count + encoded_frame_count, + pipeline.mjpeg_decoded_frames.load(Ordering::Relaxed), + pipeline.mjpeg_dropped_before_decode.load(Ordering::Relaxed), + pipeline.decoded_frames_not_encoded.load(Ordering::Relaxed), ); } + + if perf_logging && last_perf_time.elapsed() >= Duration::from_secs(5) { + let elapsed = last_perf_time.elapsed().as_secs_f64(); + let stats = pipeline.stats_snapshot(); + log_pipeline_performance(&stats, &last_perf_stats, elapsed); + last_perf_stats = stats; + last_perf_time = Instant::now(); + } } pipeline.clear_cmd_tx(); @@ -1057,17 +1293,24 @@ impl SharedVideoPipeline { drop(encoder_state); let _ = pipeline.encoder_done.send(true); }); + if let Err(error) = encoder_worker { + self.stop(); + let _ = self.running.send(false); + let _ = self.encoder_done.send(true); + return Err(AppError::VideoError(format!( + "Failed to start encoder worker: {error}" + ))); + } } // Capture loop (runs on thread, updates latest frame) { let pipeline = pipeline.clone(); - let latest_frame = latest_frame.clone(); - let frame_seq_tx = frame_seq_tx.clone(); + let frame_mailbox = frame_mailbox.clone(); let buffer_pool = buffer_pool.clone(); let bridge_ctx = BridgeContext::from_parts(subdev_path, parse_bridge_kind(bridge_kind.as_deref())); - std::thread::spawn(move || { + let capture_worker = spawn_video_worker("video-capture", move || { let mut stream: Option = None; let mut initial_geometry: Option<(Resolution, PixelFormat)> = None; let mut resolution = config.resolution; @@ -1078,8 +1321,7 @@ impl SharedVideoPipeline { .then(|| { spawn_mjpeg_decode_workers( &pipeline, - &latest_frame, - &frame_seq_tx, + &frame_mailbox, &buffer_pool, config.resolution, ) @@ -1140,8 +1382,8 @@ impl SharedVideoPipeline { } let mut no_subscribers_since: Option = None; + let mut capture_sequence = 0u64; let grace_period = Duration::from_secs(AUTO_STOP_GRACE_PERIOD_SECS); - let mut sequence: u64 = 0; let mut consecutive_timeouts: u32 = 0; let recovery_policy = CaptureRecoveryPolicy::new(config.control_mode); let capture_error_throttler = LogThrottler::with_secs(5); @@ -1430,6 +1672,18 @@ impl SharedVideoPipeline { } owned.truncate(frame_size); + capture_sequence = capture_sequence.wrapping_add(1); + + if parallel_mjpeg_decode + && pixel_format == PixelFormat::Mjpeg + && frame_mailbox.has_pending() + { + pipeline + .mjpeg_dropped_before_decode + .fetch_add(1, Ordering::Relaxed); + buffer_pool.put(owned); + continue; + } // Notify streaming only after the short-frame guard passes. pipeline.notify_state(PipelineStateNotification::streaming( @@ -1441,7 +1695,7 @@ impl SharedVideoPipeline { if let Some(senders) = mjpeg_decode_senders.as_mut() { let mut pending = Some(MjpegDecodeJob { data: owned, - sequence: meta.sequence, + sequence: capture_sequence, }); for offset in 0..senders.len() { let index = (next_mjpeg_decoder + offset) % senders.len(); @@ -1458,6 +1712,9 @@ impl SharedVideoPipeline { } } if let Some(job) = pending { + pipeline + .mjpeg_dropped_before_decode + .fetch_add(1, Ordering::Relaxed); buffer_pool.put(job.data); } continue; @@ -1468,6 +1725,7 @@ impl SharedVideoPipeline { let nv12_size = resolution.width as usize * resolution.height as usize * 3 / 2; let mut nv12 = buffer_pool.take(nv12_size); + let decode_started = Instant::now(); if let Err(error) = decoder.decode_into(&owned, &mut nv12) { buffer_pool.put(owned); buffer_pool.put(nv12); @@ -1478,6 +1736,13 @@ impl SharedVideoPipeline { continue; } buffer_pool.put(owned); + pipeline.mjpeg_decode_time_ns.fetch_add( + decode_started.elapsed().as_nanos() as u64, + Ordering::Relaxed, + ); + pipeline + .mjpeg_decoded_frames + .fetch_add(1, Ordering::Relaxed); (nv12, PixelFormat::Nv12, resolution.width) } else { (owned, pixel_format, stride) @@ -1487,15 +1752,15 @@ impl SharedVideoPipeline { resolution, frame_format, frame_stride, - meta.sequence, + capture_sequence, )); - sequence = meta.sequence.wrapping_add(1); - + if frame_mailbox.publish(capture_sequence, frame) != PublishResult::Published + && mjpeg_decoder.is_some() { - let mut guard = latest_frame.write(); - *guard = Some(frame); + pipeline + .decoded_frames_not_encoded + .fetch_add(1, Ordering::Relaxed); } - let _ = frame_seq_tx.send(sequence); } // `running` represents completed lifecycle state, not a stop request. @@ -1503,10 +1768,17 @@ impl SharedVideoPipeline { // have all completed before another consumer is told the device is free. drop(stream); pipeline.running_flag.store(false, Ordering::Release); - let _ = frame_seq_tx.send(sequence.wrapping_add(1)); + frame_mailbox.close(); let _ = pipeline.running.send(false); info!("Video pipeline stopped and capture device released"); }); + if let Err(error) = capture_worker { + self.stop(); + let _ = self.running.send(false); + return Err(AppError::VideoError(format!( + "Failed to start capture worker: {error}" + ))); + } } Ok(()) @@ -1638,13 +1910,9 @@ impl SharedVideoPipeline { let (data, is_keyframe) = self.inspect_and_parameterize_packet(codec, encoded.data, encoded.key == 1); let sequence = self.sequence.fetch_add(1, Ordering::Relaxed) + 1; - if codec == VideoEncoderType::H264 { - self.update_h264_profile_level_id(&data); - } - encoded_frames.push(EncodedVideoFrame { data, - pts_ms, + pts_ms: encoded.pts, is_keyframe, sequence, duration: Duration::from_millis(1000 / fps as u64), @@ -1682,7 +1950,11 @@ impl SharedVideoPipeline { /// Stop the pipeline (non-blocking, does not wait for capture thread to exit) pub fn stop(&self) { - if self.running_flag.swap(false, Ordering::AcqRel) { + let was_running = self.running_flag.swap(false, Ordering::AcqRel); + if let Some(frame_mailbox) = self.frame_mailbox.lock().as_ref() { + frame_mailbox.close(); + } + if was_running { self.clear_cmd_tx(); info!("Stopping video pipeline"); } @@ -2058,6 +2330,101 @@ mod tests { assert!(h264_bitstream::is_keyframe(bootstrap.as_ref())); } + #[test] + fn h264_profile_is_updated_from_parameter_only_packet() { + let pipeline = SharedVideoPipeline::new(SharedVideoPipelineConfig::h264( + Resolution::HD720, + BitratePreset::Balanced, + )) + .unwrap(); + let packet = + Bytes::from_static(&[0, 0, 0, 1, 0x67, 0x42, 0xe0, 0x1f, 0, 0, 0, 1, 0x68, 0xce]); + let (_, keyframe) = + pipeline.inspect_and_parameterize_packet(VideoEncoderType::H264, packet, false); + assert!(!keyframe); + assert_eq!( + pipeline.h264_profile_level_id_watch().borrow().as_deref(), + Some("42e01f") + ); + } + + #[tokio::test] + async fn decoder_statistics_report_work_and_drops() { + let pipeline = SharedVideoPipeline::new(SharedVideoPipelineConfig::h264( + Resolution::HD720, + BitratePreset::Balanced, + )) + .unwrap(); + pipeline.mjpeg_decoded_frames.store(30, Ordering::Relaxed); + pipeline + .mjpeg_dropped_before_decode + .store(10, Ordering::Relaxed); + pipeline + .decoded_frames_not_encoded + .store(5, Ordering::Relaxed); + let stats = pipeline.stats().await; + assert_eq!(stats.mjpeg_decoded_frames, 30); + assert_eq!(stats.mjpeg_dropped_before_decode, 10); + assert_eq!(stats.decoded_frames_not_encoded, 5); + } + + #[test] + fn encoder_packet_pts_is_preserved() { + use super::super::encoder_state::{EncodedFrame, VideoEncoderTrait}; + + struct TestEncoder; + + impl VideoEncoderTrait for TestEncoder { + fn encode_raw(&mut self, _data: &[u8], _pts_ms: i64) -> Result> { + Ok(vec![EncodedFrame { + data: Bytes::from_static(&[0, 0, 0, 1, 0x41, 0xc0]), + key: 0, + pts: 321, + }]) + } + + fn set_bitrate(&mut self, _bitrate_kbps: u32) -> Result<()> { + Ok(()) + } + + fn codec_name(&self) -> &str { + "test-h264" + } + + fn request_keyframe(&mut self) {} + } + + let pipeline = SharedVideoPipeline::new(SharedVideoPipelineConfig::h264( + Resolution::HD720, + BitratePreset::Balanced, + )) + .unwrap(); + let mut state = EncoderThreadState { + encoder: Some(Box::new(TestEncoder)), + mjpeg_decoder: None, + nv12_converter: None, + yuv420p_converter: None, + encoder_needs_yuv420p: false, + #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] + ffmpeg_hw_pipeline: None, + #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] + ffmpeg_hw_enabled: false, + fps: 30, + codec: VideoEncoderType::H264, + input_format: PixelFormat::Nv12, + }; + let frame = VideoFrame::from_pooled( + Arc::new(FrameBuffer::new(vec![0; 1280 * 720 * 3 / 2], None)), + Resolution::HD720, + PixelFormat::Nv12, + 1280, + 1, + ); + let encoded = pipeline.encode_frame_sync(&mut state, &frame).unwrap(); + assert_eq!(encoded.len(), 1); + assert_eq!(encoded[0].pts_ms, 321); + } + #[test] fn h265_keyframe_requires_irap_and_parameter_sets() { let pipeline = SharedVideoPipeline::new(SharedVideoPipelineConfig::h265( @@ -2135,7 +2502,7 @@ mod tests { codec: VideoEncoderType::H264, }); - pipeline.broadcast_encoded(bootstrap.clone()); + pipeline.broadcast_encoded_batch(vec![bootstrap.clone()].into(), Vec::new()); let mut subscriber = pipeline.subscribe(); let received = subscriber .try_recv() @@ -2143,6 +2510,205 @@ mod tests { assert!(Arc::ptr_eq(&received, &bootstrap)); } + fn delivery_test_pipeline() -> Arc { + SharedVideoPipeline::new(SharedVideoPipelineConfig::h264( + Resolution::HD720, + BitratePreset::Balanced, + )) + .unwrap() + } + + fn delivery_test_frame(sequence: u64, is_keyframe: bool) -> Arc { + Arc::new(EncodedVideoFrame { + data: Bytes::from_static(&[0, 0, 0, 1, 0x41, 0xc0]), + pts_ms: sequence as i64 * 33, + is_keyframe, + sequence, + duration: Duration::from_millis(33), + codec: VideoEncoderType::H264, + }) + } + + fn delivery_test_raw_frame(sequence: u64) -> Arc { + Arc::new(VideoFrame::from_pooled( + Arc::new(FrameBuffer::new(vec![0; 6], None)), + Resolution { + width: 2, + height: 2, + }, + PixelFormat::Nv12, + 2, + sequence, + )) + } + + #[test] + fn encoded_batch_delivers_all_frames_in_order() { + let pipeline = delivery_test_pipeline(); + let mut receiver = pipeline.subscribe(); + let reservations = pipeline.reserve_outputs().unwrap(); + pipeline.broadcast_encoded_batch( + vec![ + delivery_test_frame(1, true), + delivery_test_frame(2, false), + delivery_test_frame(3, false), + ] + .into(), + reservations, + ); + assert_eq!(receiver.try_recv().unwrap().sequence, 1); + assert_eq!(receiver.try_recv().unwrap().sequence, 2); + assert_eq!(receiver.try_recv().unwrap().sequence, 3); + assert!(matches!( + receiver.try_recv(), + Err(mpsc::error::TryRecvError::Empty) + )); + } + + #[test] + fn single_subscriber_backpressure_preserves_reference_chain() { + let pipeline = delivery_test_pipeline(); + let mut receiver = pipeline.subscribe(); + let reservations = pipeline.reserve_outputs().unwrap(); + pipeline.broadcast_encoded_batch(vec![delivery_test_frame(1, true)].into(), reservations); + assert!(pipeline.reserve_outputs().is_none()); + assert!(pipeline.reserve_outputs().is_none()); + assert_eq!(pipeline.stats_snapshot().output_backpressure_events, 1); + assert_eq!(receiver.try_recv().unwrap().sequence, 1); + let reservations = pipeline.reserve_outputs().unwrap(); + pipeline.broadcast_encoded_batch(vec![delivery_test_frame(2, false)].into(), reservations); + assert_eq!(receiver.try_recv().unwrap().sequence, 2); + let stats = pipeline.stats_snapshot(); + assert_eq!(stats.encoded_frames, 2); + assert_eq!(stats.encoded_frames_skipped_for_slow_subscribers, 0); + } + + #[test] + fn output_capacity_wakes_encoder_with_latest_raw_frame() { + use std::sync::mpsc as sync_mpsc; + + let pipeline = delivery_test_pipeline(); + let mailbox = Arc::new(VideoFrameMailbox::new()); + *pipeline.frame_mailbox.lock() = Some(mailbox.clone()); + let mut receiver = pipeline.subscribe(); + let reservations = pipeline.reserve_outputs().unwrap(); + pipeline.broadcast_encoded_batch(vec![delivery_test_frame(1, true)].into(), reservations); + mailbox.publish(1, delivery_test_raw_frame(1)); + mailbox.publish(2, delivery_test_raw_frame(2)); + let (waiting_sender, waiting_receiver) = sync_mpsc::sync_channel(1); + let (result_sender, result_receiver) = sync_mpsc::channel(); + let worker = std::thread::spawn(move || { + let result = mailbox.receive_when(|| { + let reservations = pipeline.reserve_outputs(); + if reservations.is_none() { + let _ = waiting_sender.try_send(()); + } + reservations + }); + let (frame, reservations) = result.unwrap(); + result_sender + .send((frame.sequence, reservations.len())) + .unwrap(); + }); + waiting_receiver + .recv_timeout(Duration::from_secs(2)) + .unwrap(); + assert_eq!(receiver.try_recv().unwrap().sequence, 1); + assert_eq!( + result_receiver + .recv_timeout(Duration::from_secs(2)) + .unwrap(), + (2, 1) + ); + worker.join().unwrap(); + } + + #[test] + fn receiver_drop_wakes_encoder_waiting_for_capacity() { + use std::sync::mpsc as sync_mpsc; + + let pipeline = delivery_test_pipeline(); + let mailbox = Arc::new(VideoFrameMailbox::new()); + *pipeline.frame_mailbox.lock() = Some(mailbox.clone()); + let receiver = pipeline.subscribe(); + let reservations = pipeline.reserve_outputs().unwrap(); + pipeline.broadcast_encoded_batch(vec![delivery_test_frame(1, true)].into(), reservations); + mailbox.publish(1, delivery_test_raw_frame(1)); + let (waiting_sender, waiting_receiver) = sync_mpsc::sync_channel(1); + let (result_sender, result_receiver) = sync_mpsc::channel(); + let worker = std::thread::spawn(move || { + let result = mailbox.receive_when(|| { + let reservations = pipeline.reserve_outputs(); + if reservations.is_none() { + let _ = waiting_sender.try_send(()); + } + reservations + }); + result_sender.send(result.unwrap().1.is_empty()).unwrap(); + }); + waiting_receiver + .recv_timeout(Duration::from_secs(2)) + .unwrap(); + drop(receiver); + assert!(result_receiver + .recv_timeout(Duration::from_secs(2)) + .unwrap()); + worker.join().unwrap(); + } + + #[test] + fn slow_subscriber_is_isolated_and_resumes_at_keyframe() { + let pipeline = delivery_test_pipeline(); + let mut fast_receiver = pipeline.subscribe(); + let mut slow_receiver = pipeline.subscribe(); + let reservations = pipeline.reserve_outputs().unwrap(); + pipeline.broadcast_encoded_batch(vec![delivery_test_frame(1, true)].into(), reservations); + pipeline.keyframe_requested.store(false, Ordering::Release); + assert_eq!(fast_receiver.try_recv().unwrap().sequence, 1); + let reservations = pipeline.reserve_outputs().unwrap(); + assert_eq!(reservations.len(), 1); + pipeline.broadcast_encoded_batch(vec![delivery_test_frame(2, false)].into(), reservations); + assert_eq!(fast_receiver.try_recv().unwrap().sequence, 2); + assert!(!pipeline.keyframe_requested.load(Ordering::Acquire)); + assert_eq!(slow_receiver.try_recv().unwrap().sequence, 1); + let reservations = pipeline.reserve_outputs().unwrap(); + assert_eq!(reservations.len(), 2); + assert!(pipeline.keyframe_requested.swap(false, Ordering::AcqRel)); + pipeline.broadcast_encoded_batch(vec![delivery_test_frame(3, false)].into(), reservations); + assert_eq!(fast_receiver.try_recv().unwrap().sequence, 3); + assert!(matches!( + slow_receiver.try_recv(), + Err(mpsc::error::TryRecvError::Empty) + )); + let reservations = pipeline.reserve_outputs().unwrap(); + assert!(!pipeline.keyframe_requested.load(Ordering::Acquire)); + pipeline.broadcast_encoded_batch(vec![delivery_test_frame(4, true)].into(), reservations); + assert_eq!(fast_receiver.try_recv().unwrap().sequence, 4); + assert_eq!(slow_receiver.try_recv().unwrap().sequence, 4); + let reservations = pipeline.reserve_outputs().unwrap(); + pipeline.broadcast_encoded_batch(vec![delivery_test_frame(5, false)].into(), reservations); + assert_eq!(fast_receiver.try_recv().unwrap().sequence, 5); + assert_eq!(slow_receiver.try_recv().unwrap().sequence, 5); + assert_eq!( + pipeline + .stats_snapshot() + .encoded_frames_skipped_for_slow_subscribers, + 2 + ); + } + + #[test] + fn send_statistics_include_successes_errors_and_duration() { + let pipeline = delivery_test_pipeline(); + let receiver = pipeline.subscribe(); + receiver.record_send(Duration::from_micros(100), true); + receiver.record_send(Duration::from_micros(200), false); + let stats = pipeline.stats_snapshot(); + assert_eq!(stats.sent_frames, 1); + assert_eq!(stats.send_errors, 1); + assert_eq!(stats.send_time_ns, 300_000); + } + #[test] fn mjpeg_workers_match_available_cpu_count() { assert_eq!(mjpeg_decode_worker_count(1), 1); @@ -2151,6 +2717,24 @@ mod tests { assert_eq!(mjpeg_decode_worker_count(64), 64); } + #[test] + fn stop_wakes_idle_encoder_even_after_capture_requested_stop() { + let pipeline = SharedVideoPipeline::new(SharedVideoPipelineConfig::h264( + Resolution::HD720, + BitratePreset::Balanced, + )) + .unwrap(); + let mailbox = Arc::new(VideoFrameMailbox::new()); + *pipeline.frame_mailbox.lock() = Some(mailbox.clone()); + let (sender, receiver) = std::sync::mpsc::channel(); + let worker = std::thread::spawn(move || { + sender.send(mailbox.receive().is_none()).unwrap(); + }); + pipeline.stop(); + assert!(receiver.recv_timeout(Duration::from_secs(2)).unwrap()); + worker.join().unwrap(); + } + #[test] fn stop_request_does_not_publish_worker_exit() { let pipeline = SharedVideoPipeline::new(SharedVideoPipelineConfig::h264( diff --git a/src/video/stream_manager.rs b/src/video/stream_manager.rs index 761a90c6..3ae2bbcb 100644 --- a/src/video/stream_manager.rs +++ b/src/video/stream_manager.rs @@ -697,9 +697,7 @@ impl VideoStreamManager { /// Returns None if video capture cannot be started or pipeline creation fails. pub async fn subscribe_encoded_frames( &self, - ) -> Option< - tokio::sync::mpsc::Receiver>, - > { + ) -> Option { // 1. Ensure video capture is initialized (for config discovery) if self.streamer.state().await == StreamerState::Uninitialized { tracing::info!("Initializing video capture for encoded frame subscription"); diff --git a/src/web/handlers/config/types.rs b/src/web/handlers/config/types.rs index 9e843236..59275182 100644 --- a/src/web/handlers/config/types.rs +++ b/src/web/handlers/config/types.rs @@ -1328,7 +1328,7 @@ impl WebConfigUpdate { } } // Cert and key must be provided together (cryptographic validity is checked in the - // handler via `RustlsConfig::from_pem`, same stack as the running HTTPS server). + // handler via `server_config_from_pem`, same stack as the running HTTPS server). match (&self.ssl_cert_pem, &self.ssl_key_pem) { (Some(_cert), Some(_key)) => {} (Some(_), None) => { diff --git a/src/web/handlers/config/web.rs b/src/web/handlers/config/web.rs index 890f24d4..39e44ce0 100644 --- a/src/web/handlers/config/web.rs +++ b/src/web/handlers/config/web.rs @@ -1,9 +1,9 @@ use axum::{extract::State, Json}; -use axum_server::tls_rustls::RustlsConfig; use std::sync::Arc; use crate::error::{AppError, Result}; use crate::state::AppState; +use crate::utils::tls::server_config_from_pem; use super::types::{WebConfigResponse, WebConfigUpdate}; @@ -27,8 +27,7 @@ pub async fn update_web_config( ) = (&req.ssl_cert_pem, &req.ssl_key_pem) { - RustlsConfig::from_pem(cert_pem.as_bytes().to_vec(), key_pem.as_bytes().to_vec()) - .await + server_config_from_pem(cert_pem.as_bytes(), key_pem.as_bytes()) .map_err(|e| { AppError::BadRequest( format!( @@ -88,16 +87,12 @@ pub async fn update_web_config( #[cfg(test)] mod tests { use super::*; - use rustls::crypto::{ring, CryptoProvider}; - #[tokio::test] - async fn rustls_accepts_rcgen_self_signed_pem() { - let _ = CryptoProvider::install_default(ring::default_provider()); + #[test] + fn openssl_accepts_rcgen_self_signed_pem() { let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); let cert_pem = cert.cert.pem(); let key_pem = cert.signing_key.serialize_pem(); - RustlsConfig::from_pem(cert_pem.into_bytes(), key_pem.into_bytes()) - .await - .unwrap(); + server_config_from_pem(cert_pem.as_bytes(), key_pem.as_bytes()).unwrap(); } } diff --git a/src/webrtc/universal_session.rs b/src/webrtc/universal_session.rs index 26cd245b..656aed20 100644 --- a/src/webrtc/universal_session.rs +++ b/src/webrtc/universal_session.rs @@ -37,9 +37,8 @@ use crate::error::{AppError, Result}; use crate::hid::datachannel::{parse_hid_message, HidChannelEvent}; use crate::hid::HidController; use crate::video::codec::{h264_bitstream, h265_bitstream}; -use crate::video::types::{ - BitratePreset, EncodedVideoFrame, PixelFormat, Resolution, VideoEncoderType, -}; +use crate::video::pipeline::EncodedVideoFrameReceiver; +use crate::video::types::{BitratePreset, PixelFormat, Resolution, VideoEncoderType}; const MIME_TYPE_H265: &str = "video/H265"; const KEYFRAME_RETRY_LIMIT: u8 = 3; @@ -527,7 +526,7 @@ impl UniversalSession { /// `on_connected` runs once ICE is up (e.g. request a keyframe). pub async fn start_from_video_pipeline( &self, - mut frame_rx: tokio::sync::mpsc::Receiver>, + mut frame_rx: EncodedVideoFrameReceiver, request_keyframe: Arc, ) { if let Some(handle) = self.video_receiver_handle.lock().await.take() { @@ -670,6 +669,7 @@ impl UniversalSession { } } + let send_started = Instant::now(); let send_result = video_track .write_frame_bytes_at( encoded_frame.data.clone(), @@ -677,6 +677,7 @@ impl UniversalSession { Some(encoded_frame.pts_ms), ) .await; + frame_rx.record_send(send_started.elapsed(), send_result.is_ok()); match send_result { Ok(()) => { frames_sent += 1; diff --git a/src/webrtc/video_track.rs b/src/webrtc/video_track.rs index 088dd5cc..4367e616 100644 --- a/src/webrtc/video_track.rs +++ b/src/webrtc/video_track.rs @@ -4,9 +4,9 @@ use bytes::Bytes; use rtp::codecs::h264::H264Payloader; use rtp::packetizer::Payloader; use std::sync::Arc; -use std::time::Duration; +use std::time::{Duration, Instant}; use tokio::sync::Mutex; -use tracing::{debug, trace, warn}; +use tracing::{debug, info, trace, warn}; use webrtc::media::Sample; use webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecCapability; use webrtc::track::track_local::track_local_static_rtp::TrackLocalStaticRTP; @@ -191,6 +191,63 @@ struct H264TrackState { clock: H264RtpClock, sps: Option>, pps: Option>, + performance: H264SendPerformance, +} + +#[derive(Default)] +struct H264SendTiming { + prepare: Duration, + lock: Duration, + packetize: Duration, + write: Duration, +} + +struct H264SendPerformance { + last_report: Instant, + frames: u64, + packets: u64, + bytes: u64, + timing: H264SendTiming, +} + +impl H264SendPerformance { + fn new() -> Self { + Self { + last_report: Instant::now(), + frames: 0, + packets: 0, + bytes: 0, + timing: H264SendTiming::default(), + } + } + + fn record(&mut self, bytes: usize, packets: usize, timing: H264SendTiming) { + self.frames += 1; + self.packets += packets as u64; + self.bytes += bytes as u64; + self.timing.prepare += timing.prepare; + self.timing.lock += timing.lock; + self.timing.packetize += timing.packetize; + self.timing.write += timing.write; + let elapsed = self.last_report.elapsed(); + if elapsed < Duration::from_secs(5) { + return; + } + let average_ms = |duration: Duration| duration.as_secs_f64() * 1000.0 / self.frames as f64; + info!( + "[VideoSendPerf] window_s={:.2} frames={} packets_per_frame={:.2} frame_kib={:.2} prepare_ms={:.3} lock_ms={:.3} packetize_ms={:.3} write_ms={:.3} write_us_per_packet={:.3}", + elapsed.as_secs_f64(), + self.frames, + self.packets as f64 / self.frames as f64, + self.bytes as f64 / self.frames as f64 / 1024.0, + average_ms(self.timing.prepare), + average_ms(self.timing.lock), + average_ms(self.timing.packetize), + average_ms(self.timing.write), + self.timing.write.as_secs_f64() * 1_000_000.0 / self.packets.max(1) as f64, + ); + *self = Self::new(); + } } pub struct UniversalVideoTrack { @@ -199,6 +256,7 @@ pub struct UniversalVideoTrack { config: UniversalVideoTrackConfig, h265_state: Option>, h264_state: Mutex, + h264_perf_enabled: bool, } impl UniversalVideoTrack { @@ -253,6 +311,7 @@ impl UniversalVideoTrack { increment: 90000 / config.fps.max(1), started: false, }, + performance: H264SendPerformance::new(), }; Self { track, @@ -260,6 +319,7 @@ impl UniversalVideoTrack { config, h265_state, h264_state: Mutex::new(h264_state), + h264_perf_enabled: std::env::var("ONE_KVM_VIDEO_STATS").as_deref() == Ok("1"), } } @@ -309,20 +369,24 @@ impl UniversalVideoTrack { /// Keep the stack's H.264 STAP/FU payloader, with explicit RTP timestamps. async fn write_h264_frame(&self, data: Bytes, pts_ms: Option) -> Result<()> { - let normalized = h264_bitstream::normalize_for_webrtc(data.as_ref()); - let mut data = Bytes::from(normalized); + let prepare_started = self.h264_perf_enabled.then(Instant::now); + let mut data = h264_bitstream::normalize_annex_b(data); - let idr = h264_bitstream::is_keyframe(data.as_ref()); - let has_parameter_sets = h264_bitstream::has_sps_pps(data.as_ref()); + let inspection = h264_bitstream::inspect_annex_b(data.as_ref()); + let idr = inspection.is_idr; + let has_parameter_sets = inspection.sps.is_some() && inspection.pps.is_some(); + let prepare_time = prepare_started.map(|started| started.elapsed()); + let lock_started = self.h264_perf_enabled.then(Instant::now); let mut state = self.h264_state.lock().await; + let lock_time = lock_started.map(|started| started.elapsed()); + let packetize_started = self.h264_perf_enabled.then(Instant::now); { - let (sps, pps) = h264_bitstream::extract_sps_pps(data.as_ref()); - if let Some(sps) = sps { - state.sps = Some(sps); + if let Some(sps) = inspection.sps { + state.sps = Some(sps.to_vec()); } - if let Some(pps) = pps { - state.pps = Some(pps); + if let Some(pps) = inspection.pps { + state.pps = Some(pps.to_vec()); } if idr && !has_parameter_sets { @@ -351,6 +415,8 @@ impl UniversalVideoTrack { } let timestamp = state.clock.next(pts_ms); let count = payloads.len(); + let packetize_time = packetize_started.map(|started| started.elapsed()); + let write_started = self.h264_perf_enabled.then(Instant::now); // Serialize complete access units, including their sequence allocation. for (index, payload) in payloads.into_iter().enumerate() { let sequence_number = state.sequence_number; @@ -370,6 +436,18 @@ impl UniversalVideoTrack { .await .map_err(|e| AppError::WebRtcError(format!("H264 RTP write failed: {e}")))?; } + if let Some(started) = write_started { + state.performance.record( + data.len(), + count, + H264SendTiming { + prepare: prepare_time.unwrap_or_default(), + lock: lock_time.unwrap_or_default(), + packetize: packetize_time.unwrap_or_default(), + write: started.elapsed(), + }, + ); + } Ok(()) } @@ -533,6 +611,108 @@ mod tests { assert_eq!(clock.next(None), 6092); } + #[test] + fn h264_zero_copy_input_preserves_rtp_payloads() { + let mut annex_b = vec![ + 0, 0, 0, 1, 0x09, 0xf0, 0, 0, 1, 0x67, 0x42, 0xe0, 0x1f, 0, 0, 0, 1, 0x68, 0xce, 0, 0, + 1, 0x0c, 0xff, 0, 0, 0, 1, 0x65, + ]; + annex_b.extend_from_slice(&vec![0x55; 2500]); + let mut avcc = vec![0, 0, 0, 2, 0x09, 0xf0, 0, 0, 1, 1, 0x65]; + avcc.extend_from_slice(&vec![0x55; 256]); + let inputs = vec![ + annex_b, + avcc, + vec![0, 0, 1, 0x41, 0x88, 0x55], + vec![0x41, 0x88, 0x55], + vec![0, 0, 0, 1, 0x09, 0xf0, 0, 0, 1, 0x0c, 0xff], + Vec::new(), + ]; + for input in inputs { + let previous = Bytes::from(h264_bitstream::normalize_for_webrtc(&input)); + let current = h264_bitstream::normalize_annex_b(Bytes::from(input)); + let mut previous_payloader = H264Payloader::default(); + let mut current_payloader = H264Payloader::default(); + assert_eq!( + previous_payloader.payload(RTP_MTU - 12, &previous).unwrap(), + current_payloader.payload(RTP_MTU - 12, ¤t).unwrap(), + ); + } + } + + #[test] + fn h264_zero_copy_input_preserves_cached_parameter_sets() { + let mut previous_payloader = H264Payloader::default(); + let mut current_payloader = H264Payloader::default(); + for input in [ + Bytes::from_static(&[0, 0, 0, 1, 0x67, 0x42, 0xe0, 0x1f]), + Bytes::from_static(&[0, 0, 0, 1, 0x09, 0xf0, 0, 0, 1, 0x68, 0xce]), + Bytes::from_static(&[0, 0, 0, 1, 0x65, 0x88, 0x55]), + ] { + let previous = Bytes::from(h264_bitstream::normalize_for_webrtc(&input)); + let current = h264_bitstream::normalize_annex_b(input.clone()); + assert_eq!(current.as_ptr(), input.as_ptr()); + assert_eq!( + previous_payloader.payload(RTP_MTU - 12, &previous).unwrap(), + current_payloader.payload(RTP_MTU - 12, ¤t).unwrap(), + ); + } + } + + #[test] + fn h264_send_performance_accumulates_stage_timings() { + let mut performance = H264SendPerformance::new(); + for _ in 0..2 { + performance.record( + 1200, + 2, + H264SendTiming { + prepare: Duration::from_micros(100), + lock: Duration::from_micros(20), + packetize: Duration::from_micros(200), + write: Duration::from_micros(300), + }, + ); + } + assert_eq!(performance.frames, 2); + assert_eq!(performance.bytes, 2400); + assert_eq!(performance.packets, 4); + assert_eq!(performance.timing.prepare, Duration::from_micros(200)); + assert_eq!(performance.timing.lock, Duration::from_micros(40)); + assert_eq!(performance.timing.packetize, Duration::from_micros(400)); + assert_eq!(performance.timing.write, Duration::from_micros(600)); + } + + #[test] + fn srtp_backend_preserves_authentication_and_sequence_rollover() { + use webrtc::srtp::context::Context; + use webrtc::srtp::protection_profile::ProtectionProfile; + + for profile in [ + ProtectionProfile::Aes128CmHmacSha1_80, + ProtectionProfile::AeadAes128Gcm, + ] { + let key = vec![42; profile.key_len()]; + let salt = vec![13; profile.salt_len()]; + let mut sender = Context::new(&key, &salt, profile, None, None).unwrap(); + let mut receiver = Context::new(&key, &salt, profile, None, None).unwrap(); + for sequence in [u16::MAX - 1, u16::MAX, 0, 1] { + for ssrc in [17u32, 18] { + let mut packet = vec![0x55; 1200]; + packet[..12].copy_from_slice(&[0x80, 108, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + packet[2..4].copy_from_slice(&sequence.to_be_bytes()); + packet[8..12].copy_from_slice(&ssrc.to_be_bytes()); + let encrypted = sender.encrypt_rtp(&packet).unwrap(); + assert_ne!(&encrypted[12..1200], &packet[12..]); + let mut corrupted = encrypted.to_vec(); + corrupted[13] ^= 1; + assert!(receiver.decrypt_rtp(&corrupted).is_err()); + assert_eq!(receiver.decrypt_rtp(&encrypted).unwrap().as_ref(), packet); + } + } + } + } + #[test] fn test_video_codec_properties() { assert_eq!(VideoCodec::H264.mime_type(), "video/H264"); diff --git a/vendor/dtls/Cargo.toml b/vendor/dtls/Cargo.toml new file mode 100644 index 00000000..49212d1e --- /dev/null +++ b/vendor/dtls/Cargo.toml @@ -0,0 +1,155 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +name = "dtls" +version = "0.17.2" +authors = ["Rain Liu "] +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "A pure Rust implementation of DTLS" +homepage = "https://webrtc.rs" +documentation = "https://docs.rs/dtls" +readme = "README.md" +license = "MIT/Apache-2.0" +repository = "https://github.com/webrtc-rs/webrtc/tree/master/dtls" + +[features] +pem = ["dep:pem"] + +[lib] +name = "dtls" +path = "src/lib.rs" + +[dependencies.aes] +version = "0.8" + +[dependencies.aes-gcm] +version = "0.10" + +[dependencies.async-trait] +version = "0.1" + +[dependencies.bytecheck] +version = "0.8" + +[dependencies.byteorder] +version = "1" + +[dependencies.cbc] +version = "0.1" +features = [ + "block-padding", + "alloc", +] + +[dependencies.ccm] +version = "0.5" + +[dependencies.chacha20poly1305] +version = "0.10.1" + +[dependencies.der-parser] +version = "9.0" + +[dependencies.hmac] +version = "0.12" + +[dependencies.log] +version = "0.4" + +[dependencies.p256] +version = "0.13" +features = [ + "default", + "ecdh", + "ecdsa", +] + +[dependencies.p384] +version = "0.13" + +[dependencies.pem] +version = "3" +optional = true + +[dependencies.portable-atomic] +version = "1.6" + +[dependencies.rand] +version = "0.9" + +[dependencies.rand_core] +version = "0.6" + +[dependencies.rcgen] +version = "0.13" + +[dependencies.ring] +version = "0.17.14" + +[dependencies.rkyv] +version = "0.8" + +[dependencies.openssl] +version = "0.10" +features = ["vendored"] + +[dependencies.rustls-pki-types] +version = "1" + +[dependencies.sec1] +version = "0.7" +features = ["std"] + +[dependencies.sha1] +version = "0.10" + +[dependencies.sha2] +version = "0.10" + +[dependencies.thiserror] +version = "1" + +[dependencies.tokio] +version = "1.32.0" +features = [ + "fs", + "io-util", + "io-std", + "macros", + "net", + "parking_lot", + "rt", + "rt-multi-thread", + "sync", + "time", +] + +[dependencies.util] +version = "0.17.1" +features = ["conn"] +default-features = false +package = "webrtc-util" + +[dependencies.x25519-dalek] +version = "2" +features = ["static_secrets"] + +[dependencies.x509-parser] +version = "0.16" + +[dev-dependencies] diff --git a/vendor/dtls/LICENSE-APACHE b/vendor/dtls/LICENSE-APACHE new file mode 100644 index 00000000..16fe87b0 --- /dev/null +++ b/vendor/dtls/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/vendor/dtls/LICENSE-MIT b/vendor/dtls/LICENSE-MIT new file mode 100644 index 00000000..e11d93be --- /dev/null +++ b/vendor/dtls/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 WebRTC.rs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/dtls/README.md b/vendor/dtls/README.md new file mode 100644 index 00000000..daa8002c --- /dev/null +++ b/vendor/dtls/README.md @@ -0,0 +1,20 @@ +# DTLS certificate verification backend + +This directory contains `dtls` 0.17.2 from the crates.io release of webrtc-rs. +The original MIT and Apache-2.0 licenses are preserved. + +One-KVM patches only certificate-chain verification and its configuration to +use vendored OpenSSL instead of rustls/WebPKI. DTLS flights, record protection, +certificate signatures, SRTP negotiation and the application fingerprint check +remain unchanged. `rustls-pki-types` remains a certificate-data representation, +not a TLS implementation. + +`certificate_verifier.rs` implements trust-anchor, purpose, certificate-validity +and DNS/IP identity checks. Server names do not fall back to the certificate +subject, and partial-label wildcards are rejected. No trust checks are skipped +by this backend. The upstream application-controlled `insecure_skip_verify` +policy is preserved, including WebRTC's separate certificate fingerprint check. +Authentication security level 2 rejects weak RSA keys and SHA-1 leaf signatures. + +Run `cargo test --manifest-path vendor/dtls/Cargo.toml --lib` after updating this +fork, and verify the application dependency tree contains no `rustls` engine. diff --git a/vendor/dtls/src/alert/alert_test.rs b/vendor/dtls/src/alert/alert_test.rs new file mode 100644 index 00000000..ad9fff9f --- /dev/null +++ b/vendor/dtls/src/alert/alert_test.rs @@ -0,0 +1,50 @@ +use std::io::{BufReader, BufWriter}; + +use super::*; +use crate::error::Error; + +#[test] +fn test_alert() -> Result<()> { + let tests = vec![ + ( + "Valid Alert", + vec![0x02, 0x0A], + Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::UnexpectedMessage, + }, + None, + ), + ( + "Invalid alert length", + vec![0x00], + Alert { + alert_level: AlertLevel::Invalid, + alert_description: AlertDescription::Invalid, + }, + Some(Error::Other("io".to_owned())), + ), + ]; + + for (name, data, wanted, unmarshal_error) in tests { + let mut reader = BufReader::new(data.as_slice()); + let result = Alert::unmarshal(&mut reader); + + if let Some(err) = unmarshal_error { + assert!(result.is_err(), "{name} expected error: {err}"); + } else if let Ok(alert) = result { + assert_eq!(wanted, alert, "{name} expected {wanted}, but got {alert}"); + + let mut data2: Vec = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(data2.as_mut()); + alert.marshal(&mut writer)?; + } + assert_eq!(data, data2, "{name} expected {data:?}, but got {data2:?}"); + } else { + assert!(result.is_ok(), "{name} expected Ok, but has error"); + } + } + + Ok(()) +} diff --git a/vendor/dtls/src/alert/mod.rs b/vendor/dtls/src/alert/mod.rs new file mode 100644 index 00000000..8c58210b --- /dev/null +++ b/vendor/dtls/src/alert/mod.rs @@ -0,0 +1,189 @@ +#[cfg(test)] +mod alert_test; + +use std::fmt; +use std::io::{Read, Write}; + +use byteorder::{ReadBytesExt, WriteBytesExt}; + +use super::content::*; +use crate::error::Result; + +#[derive(Copy, Clone, PartialEq, Debug)] +pub(crate) enum AlertLevel { + Warning = 1, + Fatal = 2, + Invalid, +} + +impl fmt::Display for AlertLevel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + AlertLevel::Warning => write!(f, "LevelWarning"), + AlertLevel::Fatal => write!(f, "LevelFatal"), + _ => write!(f, "Invalid alert level"), + } + } +} + +impl From for AlertLevel { + fn from(val: u8) -> Self { + match val { + 1 => AlertLevel::Warning, + 2 => AlertLevel::Fatal, + _ => AlertLevel::Invalid, + } + } +} + +#[derive(Copy, Clone, PartialEq, Debug)] +pub(crate) enum AlertDescription { + CloseNotify = 0, + UnexpectedMessage = 10, + BadRecordMac = 20, + DecryptionFailed = 21, + RecordOverflow = 22, + DecompressionFailure = 30, + HandshakeFailure = 40, + NoCertificate = 41, + BadCertificate = 42, + UnsupportedCertificate = 43, + CertificateRevoked = 44, + CertificateExpired = 45, + CertificateUnknown = 46, + IllegalParameter = 47, + UnknownCa = 48, + AccessDenied = 49, + DecodeError = 50, + DecryptError = 51, + ExportRestriction = 60, + ProtocolVersion = 70, + InsufficientSecurity = 71, + InternalError = 80, + UserCanceled = 90, + NoRenegotiation = 100, + UnsupportedExtension = 110, + UnknownPskIdentity = 115, + Invalid, +} + +impl fmt::Display for AlertDescription { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + AlertDescription::CloseNotify => write!(f, "CloseNotify"), + AlertDescription::UnexpectedMessage => write!(f, "UnexpectedMessage"), + AlertDescription::BadRecordMac => write!(f, "BadRecordMac"), + AlertDescription::DecryptionFailed => write!(f, "DecryptionFailed"), + AlertDescription::RecordOverflow => write!(f, "RecordOverflow"), + AlertDescription::DecompressionFailure => write!(f, "DecompressionFailure"), + AlertDescription::HandshakeFailure => write!(f, "HandshakeFailure"), + AlertDescription::NoCertificate => write!(f, "NoCertificate"), + AlertDescription::BadCertificate => write!(f, "BadCertificate"), + AlertDescription::UnsupportedCertificate => write!(f, "UnsupportedCertificate"), + AlertDescription::CertificateRevoked => write!(f, "CertificateRevoked"), + AlertDescription::CertificateExpired => write!(f, "CertificateExpired"), + AlertDescription::CertificateUnknown => write!(f, "CertificateUnknown"), + AlertDescription::IllegalParameter => write!(f, "IllegalParameter"), + AlertDescription::UnknownCa => write!(f, "UnknownCA"), + AlertDescription::AccessDenied => write!(f, "AccessDenied"), + AlertDescription::DecodeError => write!(f, "DecodeError"), + AlertDescription::DecryptError => write!(f, "DecryptError"), + AlertDescription::ExportRestriction => write!(f, "ExportRestriction"), + AlertDescription::ProtocolVersion => write!(f, "ProtocolVersion"), + AlertDescription::InsufficientSecurity => write!(f, "InsufficientSecurity"), + AlertDescription::InternalError => write!(f, "InternalError"), + AlertDescription::UserCanceled => write!(f, "UserCanceled"), + AlertDescription::NoRenegotiation => write!(f, "NoRenegotiation"), + AlertDescription::UnsupportedExtension => write!(f, "UnsupportedExtension"), + AlertDescription::UnknownPskIdentity => write!(f, "UnknownPskIdentity"), + _ => write!(f, "Invalid alert description"), + } + } +} + +impl From for AlertDescription { + fn from(val: u8) -> Self { + match val { + 0 => AlertDescription::CloseNotify, + 10 => AlertDescription::UnexpectedMessage, + 20 => AlertDescription::BadRecordMac, + 21 => AlertDescription::DecryptionFailed, + 22 => AlertDescription::RecordOverflow, + 30 => AlertDescription::DecompressionFailure, + 40 => AlertDescription::HandshakeFailure, + 41 => AlertDescription::NoCertificate, + 42 => AlertDescription::BadCertificate, + 43 => AlertDescription::UnsupportedCertificate, + 44 => AlertDescription::CertificateRevoked, + 45 => AlertDescription::CertificateExpired, + 46 => AlertDescription::CertificateUnknown, + 47 => AlertDescription::IllegalParameter, + 48 => AlertDescription::UnknownCa, + 49 => AlertDescription::AccessDenied, + 50 => AlertDescription::DecodeError, + 51 => AlertDescription::DecryptError, + 60 => AlertDescription::ExportRestriction, + 70 => AlertDescription::ProtocolVersion, + 71 => AlertDescription::InsufficientSecurity, + 80 => AlertDescription::InternalError, + 90 => AlertDescription::UserCanceled, + 100 => AlertDescription::NoRenegotiation, + 110 => AlertDescription::UnsupportedExtension, + 115 => AlertDescription::UnknownPskIdentity, + _ => AlertDescription::Invalid, + } + } +} + +// One of the content types supported by the TLS record layer is the +// alert type. Alert messages convey the severity of the message +// (warning or fatal) and a description of the alert. Alert messages +// with a level of fatal result in the immediate termination of the +// connection. In this case, other connections corresponding to the +// session may continue, but the session identifier MUST be invalidated, +// preventing the failed session from being used to establish new +// connections. Like other messages, alert messages are encrypted and +// compressed, as specified by the current connection state. +/// ## Specifications +/// +/// * [RFC 5246 §7.2] +/// +/// [RFC 5246 §7.2]: https://tools.ietf.org/html/rfc5246#section-7.2 +#[derive(Copy, Clone, PartialEq, Debug)] +pub struct Alert { + pub(crate) alert_level: AlertLevel, + pub(crate) alert_description: AlertDescription, +} + +impl fmt::Display for Alert { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Alert {}: {}", self.alert_level, self.alert_description) + } +} + +impl Alert { + pub fn content_type(&self) -> ContentType { + ContentType::Alert + } + + pub fn size(&self) -> usize { + 2 + } + + pub fn marshal(&self, writer: &mut W) -> Result<()> { + writer.write_u8(self.alert_level as u8)?; + writer.write_u8(self.alert_description as u8)?; + + Ok(writer.flush()?) + } + + pub fn unmarshal(reader: &mut R) -> Result { + let alert_level = reader.read_u8()?.into(); + let alert_description = reader.read_u8()?.into(); + + Ok(Alert { + alert_level, + alert_description, + }) + } +} diff --git a/vendor/dtls/src/application_data.rs b/vendor/dtls/src/application_data.rs new file mode 100644 index 00000000..4cea4df8 --- /dev/null +++ b/vendor/dtls/src/application_data.rs @@ -0,0 +1,41 @@ +use std::io::{Read, Write}; + +use super::content::*; +use crate::error::Result; + +// Application data messages are carried by the record layer and are +// fragmented, compressed, and encrypted based on the current connection +// state. The messages are treated as transparent data to the record +// layer. +/// ## Specifications +/// +/// * [RFC 5246 §10] +/// +/// [RFC 5246 §10]: https://tools.ietf.org/html/rfc5246#section-10 +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct ApplicationData { + pub data: Vec, +} + +impl ApplicationData { + pub fn content_type(&self) -> ContentType { + ContentType::ApplicationData + } + + pub fn size(&self) -> usize { + self.data.len() + } + + pub fn marshal(&self, writer: &mut W) -> Result<()> { + writer.write_all(&self.data)?; + + Ok(writer.flush()?) + } + + pub fn unmarshal(reader: &mut R) -> Result { + let mut data: Vec = vec![]; + reader.read_to_end(&mut data)?; + + Ok(ApplicationData { data }) + } +} diff --git a/vendor/dtls/src/certificate_verifier.rs b/vendor/dtls/src/certificate_verifier.rs new file mode 100644 index 00000000..6d37797c --- /dev/null +++ b/vendor/dtls/src/certificate_verifier.rs @@ -0,0 +1,378 @@ +use openssl::stack::Stack; +use openssl::x509::store::X509StoreBuilder; +use openssl::x509::verify::{X509CheckFlags, X509VerifyParam}; +use openssl::x509::{X509PurposeId, X509StoreContext, X509}; +use rustls_pki_types::{CertificateDer, ServerName}; + +use crate::error::{Error, Result}; + +#[derive(Clone, Debug, Default)] +pub struct RootCertStore { + certificates: Vec, +} + +impl RootCertStore { + pub fn empty() -> Self { + Self::default() + } + + pub fn add(&mut self, certificate: CertificateDer<'_>) -> Result<()> { + self.certificates + .push(X509::from_der(certificate.as_ref()).map_err(openssl_error)?); + Ok(()) + } +} + +pub(crate) struct CertificateVerifier { + roots: RootCertStore, +} + +impl CertificateVerifier { + pub(crate) fn new(roots: RootCertStore) -> Self { + Self { roots } + } + + pub(crate) fn verify_client_cert( + &self, + end_entity: &CertificateDer<'_>, + intermediates: &[CertificateDer<'_>], + ) -> Result<()> { + self.verify(end_entity, intermediates, X509PurposeId::SSL_CLIENT, None) + } + + pub(crate) fn verify_server_cert( + &self, + end_entity: &CertificateDer<'_>, + intermediates: &[CertificateDer<'_>], + server_name: &str, + ) -> Result<()> { + ServerName::try_from(server_name).map_err(|error| Error::Other(error.to_string()))?; + self.verify( + end_entity, + intermediates, + X509PurposeId::SSL_SERVER, + Some(server_name), + ) + } + + fn verify( + &self, + end_entity: &CertificateDer<'_>, + intermediates: &[CertificateDer<'_>], + purpose: X509PurposeId, + server_name: Option<&str>, + ) -> Result<()> { + let leaf = X509::from_der(end_entity.as_ref()).map_err(openssl_error)?; + let mut chain = Stack::new().map_err(openssl_error)?; + for certificate in intermediates { + chain + .push(X509::from_der(certificate.as_ref()).map_err(openssl_error)?) + .map_err(openssl_error)?; + } + let mut parameters = X509VerifyParam::new().map_err(openssl_error)?; + parameters.set_auth_level(2); + parameters.set_purpose(purpose).map_err(openssl_error)?; + parameters.set_hostflags( + X509CheckFlags::NO_PARTIAL_WILDCARDS | X509CheckFlags::NEVER_CHECK_SUBJECT, + ); + if let Some(server_name) = server_name { + if let Ok(address) = server_name.parse() { + parameters.set_ip(address).map_err(openssl_error)?; + } else { + parameters.set_host(server_name).map_err(openssl_error)?; + } + } + let mut store = X509StoreBuilder::new().map_err(openssl_error)?; + store.set_param(¶meters).map_err(openssl_error)?; + for certificate in &self.roots.certificates { + store.add_cert(certificate.clone()).map_err(openssl_error)?; + } + let store = store.build(); + let mut context = X509StoreContext::new().map_err(openssl_error)?; + context + .init(&store, &leaf, &chain, |context| { + if context.verify_cert()? { + Ok(None) + } else { + Ok(Some(context.error().to_string())) + } + }) + .map_err(openssl_error)? + .map_or(Ok(()), |error| Err(Error::Other(error))) + } +} + +fn openssl_error(error: openssl::error::ErrorStack) -> Error { + Error::Other(error.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + use rcgen::{BasicConstraints, CertificateParams, ExtendedKeyUsagePurpose, IsCa, KeyPair}; + + fn signed_leaf( + parameters: CertificateParams, + ) -> (CertificateVerifier, CertificateDer<'static>) { + let mut issuer_parameters = CertificateParams::new(Vec::::new()).unwrap(); + issuer_parameters + .distinguished_name + .push(rcgen::DnType::CommonName, "test root"); + issuer_parameters.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + let issuer_key = KeyPair::generate().unwrap(); + let issuer = issuer_parameters.self_signed(&issuer_key).unwrap(); + let leaf_key = KeyPair::generate().unwrap(); + let leaf = parameters + .signed_by(&leaf_key, &issuer, &issuer_key) + .unwrap(); + let mut roots = RootCertStore::empty(); + roots.add(issuer.der().clone()).unwrap(); + (CertificateVerifier::new(roots), leaf.der().clone()) + } + + #[test] + fn trusted_server_matches_dns_and_ip_subject_alternative_names() { + let parameters = + CertificateParams::new(vec!["localhost".into(), "127.0.0.1".into()]).unwrap(); + let (verifier, leaf) = signed_leaf(parameters); + verifier + .verify_server_cert(&leaf, &[], "localhost") + .unwrap(); + assert!(verifier.verify_server_cert(&leaf, &[], "127.0.0.1").is_ok()); + assert!(verifier + .verify_server_cert(&leaf, &[], "wrong.local") + .is_err()); + assert!(verifier + .verify_server_cert(&leaf, &[], "127.0.0.2") + .is_err()); + assert!(verifier.verify_server_cert(&leaf, &[], "").is_err()); + } + + #[test] + fn rejects_untrusted_and_tampered_certificates() { + let (verifier, leaf) = + signed_leaf(CertificateParams::new(vec!["localhost".into()]).unwrap()); + let untrusted = CertificateVerifier::new(RootCertStore::empty()); + assert!(untrusted + .verify_server_cert(&leaf, &[], "localhost") + .is_err()); + let mut tampered = leaf.as_ref().to_vec(); + let last = tampered.len() - 1; + tampered[last] ^= 1; + assert!(verifier + .verify_server_cert(&CertificateDer::from(tampered), &[], "localhost") + .is_err()); + assert!(verifier + .verify_server_cert(&CertificateDer::from(vec![0]), &[], "localhost") + .is_err()); + } + + #[test] + fn rejects_expired_certificate_and_wrong_extended_key_usage() { + let mut expired = CertificateParams::new(vec!["localhost".into()]).unwrap(); + expired.not_before = rcgen::date_time_ymd(2019, 1, 1); + expired.not_after = rcgen::date_time_ymd(2020, 1, 1); + let (verifier, leaf) = signed_leaf(expired); + assert!(verifier + .verify_server_cert(&leaf, &[], "localhost") + .is_err()); + let mut server_only = CertificateParams::new(vec!["localhost".into()]).unwrap(); + server_only.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth]; + let (verifier, leaf) = signed_leaf(server_only); + verifier + .verify_server_cert(&leaf, &[], "localhost") + .unwrap(); + assert!(verifier.verify_client_cert(&leaf, &[]).is_err()); + let mut client_only = CertificateParams::new(vec!["localhost".into()]).unwrap(); + client_only.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth]; + let (verifier, leaf) = signed_leaf(client_only); + assert!(verifier.verify_client_cert(&leaf, &[]).is_ok()); + assert!(verifier + .verify_server_cert(&leaf, &[], "localhost") + .is_err()); + } + + #[test] + fn does_not_fall_back_to_common_name_or_partial_wildcard() { + let mut parameters = CertificateParams::new(Vec::::new()).unwrap(); + parameters + .distinguished_name + .push(rcgen::DnType::CommonName, "localhost"); + let (verifier, leaf) = signed_leaf(parameters); + assert!(verifier + .verify_server_cert(&leaf, &[], "localhost") + .is_err()); + let (verifier, leaf) = + signed_leaf(CertificateParams::new(vec!["local*.example".into()]).unwrap()); + assert!(verifier + .verify_server_cert(&leaf, &[], "localhost.example") + .is_err()); + } + + #[test] + fn rejects_weak_rsa_keys_and_sha1_leaf_signatures() { + use openssl::asn1::Asn1Time; + use openssl::hash::MessageDigest; + use openssl::pkey::PKey; + use openssl::rsa::Rsa; + use openssl::x509::extension::{BasicConstraints, SubjectAlternativeName}; + use openssl::x509::X509NameBuilder; + + let issuer_key = PKey::from_rsa(Rsa::generate(2048).unwrap()).unwrap(); + let mut issuer_name = X509NameBuilder::new().unwrap(); + issuer_name + .append_entry_by_text("CN", "strong root") + .unwrap(); + let issuer_name = issuer_name.build(); + let mut issuer = X509::builder().unwrap(); + issuer.set_version(2).unwrap(); + issuer.set_subject_name(&issuer_name).unwrap(); + issuer.set_issuer_name(&issuer_name).unwrap(); + issuer.set_pubkey(&issuer_key).unwrap(); + issuer + .set_not_before(&Asn1Time::days_from_now(0).unwrap()) + .unwrap(); + issuer + .set_not_after(&Asn1Time::days_from_now(1).unwrap()) + .unwrap(); + issuer + .append_extension(BasicConstraints::new().critical().ca().build().unwrap()) + .unwrap(); + issuer.sign(&issuer_key, MessageDigest::sha256()).unwrap(); + let issuer = issuer.build(); + let mut roots = RootCertStore::empty(); + roots + .add(CertificateDer::from(issuer.to_der().unwrap())) + .unwrap(); + let verifier = CertificateVerifier::new(roots); + for (bits, digest, expected) in [ + (2048, MessageDigest::sha256(), true), + (1024, MessageDigest::sha256(), false), + (2048, MessageDigest::sha1(), false), + ] { + let leaf_key = PKey::from_rsa(Rsa::generate(bits).unwrap()).unwrap(); + let mut leaf_name = X509NameBuilder::new().unwrap(); + leaf_name.append_entry_by_text("CN", "localhost").unwrap(); + let leaf_name = leaf_name.build(); + let mut leaf = X509::builder().unwrap(); + leaf.set_version(2).unwrap(); + leaf.set_subject_name(&leaf_name).unwrap(); + leaf.set_issuer_name(issuer.subject_name()).unwrap(); + leaf.set_pubkey(&leaf_key).unwrap(); + leaf.set_not_before(&Asn1Time::days_from_now(0).unwrap()) + .unwrap(); + leaf.set_not_after(&Asn1Time::days_from_now(1).unwrap()) + .unwrap(); + let names = SubjectAlternativeName::new() + .dns("localhost") + .build(&leaf.x509v3_context(Some(&issuer), None)) + .unwrap(); + leaf.append_extension(names).unwrap(); + leaf.sign(&issuer_key, digest).unwrap(); + let leaf = CertificateDer::from(leaf.build().to_der().unwrap()); + assert_eq!( + verifier.verify_server_cert(&leaf, &[], "localhost").is_ok(), + expected + ); + } + } + + #[tokio::test] + async fn dtls_handshake_preserves_mutual_certificate_authentication() { + use crate::config::{ClientAuthType, Config}; + use crate::conn::DTLSConn; + use crate::crypto::Certificate; + use std::sync::Arc; + use std::time::Duration; + + let client_certificate = + Certificate::generate_self_signed(vec!["localhost".into()]).unwrap(); + let server_certificate = + Certificate::generate_self_signed(vec!["localhost".into()]).unwrap(); + let mut client_roots = RootCertStore::empty(); + client_roots + .add(server_certificate.certificate[0].clone()) + .unwrap(); + let mut server_roots = RootCertStore::empty(); + server_roots + .add(client_certificate.certificate[0].clone()) + .unwrap(); + let client_socket = Arc::new(tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap()); + let server_socket = Arc::new(tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap()); + client_socket + .connect(server_socket.local_addr().unwrap()) + .await + .unwrap(); + server_socket + .connect(client_socket.local_addr().unwrap()) + .await + .unwrap(); + let client_config = Config { + certificates: vec![client_certificate], + roots_cas: client_roots, + server_name: "localhost".into(), + ..Default::default() + }; + let server_config = Config { + certificates: vec![server_certificate], + client_cas: server_roots, + client_auth: ClientAuthType::RequireAndVerifyClientCert, + ..Default::default() + }; + let (client, server) = tokio::time::timeout(Duration::from_secs(10), async { + tokio::try_join!( + DTLSConn::new(client_socket, client_config, true, None), + DTLSConn::new(server_socket, server_config, false, None), + ) + }) + .await + .unwrap() + .unwrap(); + client + .write(b"authenticated", Some(Duration::from_secs(5))) + .await + .unwrap(); + let mut buffer = [0; 64]; + let count = server + .read(&mut buffer, Some(Duration::from_secs(5))) + .await + .unwrap(); + assert_eq!(&buffer[..count], b"authenticated"); + client.close().await.unwrap(); + server.close().await.unwrap(); + } + + #[test] + fn verifies_intermediate_chain_without_implicitly_trusting_it() { + let mut root_parameters = CertificateParams::new(Vec::::new()).unwrap(); + root_parameters + .distinguished_name + .push(rcgen::DnType::CommonName, "test root"); + root_parameters.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + let root_key = KeyPair::generate().unwrap(); + let root = root_parameters.self_signed(&root_key).unwrap(); + let mut intermediate_parameters = CertificateParams::new(Vec::::new()).unwrap(); + intermediate_parameters + .distinguished_name + .push(rcgen::DnType::CommonName, "test intermediate"); + intermediate_parameters.is_ca = IsCa::Ca(BasicConstraints::Constrained(0)); + let intermediate_key = KeyPair::generate().unwrap(); + let intermediate = intermediate_parameters + .signed_by(&intermediate_key, &root, &root_key) + .unwrap(); + let leaf_key = KeyPair::generate().unwrap(); + let leaf = CertificateParams::new(vec!["localhost".into()]) + .unwrap() + .signed_by(&leaf_key, &intermediate, &intermediate_key) + .unwrap(); + let mut roots = RootCertStore::empty(); + roots.add(root.der().clone()).unwrap(); + let verifier = CertificateVerifier::new(roots); + assert!(verifier + .verify_server_cert(leaf.der(), &[intermediate.der().clone()], "localhost") + .is_ok()); + assert!(verifier + .verify_server_cert(leaf.der(), &[], "localhost") + .is_err()); + } +} diff --git a/vendor/dtls/src/change_cipher_spec/change_cipher_spec_test.rs b/vendor/dtls/src/change_cipher_spec/change_cipher_spec_test.rs new file mode 100644 index 00000000..868d96df --- /dev/null +++ b/vendor/dtls/src/change_cipher_spec/change_cipher_spec_test.rs @@ -0,0 +1,37 @@ +use std::io::{BufReader, BufWriter}; + +use super::*; + +#[test] +fn test_change_cipher_spec_round_trip() -> Result<()> { + let c = ChangeCipherSpec {}; + let mut raw = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); + c.marshal(&mut writer)?; + } + + let mut reader = BufReader::new(raw.as_slice()); + let cnew = ChangeCipherSpec::unmarshal(&mut reader)?; + assert_eq!( + c, cnew, + "ChangeCipherSpec round trip: got {cnew:?}, want {c:?}" + ); + + Ok(()) +} + +#[test] +fn test_change_cipher_spec_invalid() -> Result<()> { + let data = vec![0x00]; + + let mut reader = BufReader::new(data.as_slice()); + let result = ChangeCipherSpec::unmarshal(&mut reader); + + match result { + Ok(_) => panic!("must be error"), + Err(err) => assert_eq!(err.to_string(), Error::ErrInvalidCipherSpec.to_string()), + }; + + Ok(()) +} diff --git a/vendor/dtls/src/change_cipher_spec/mod.rs b/vendor/dtls/src/change_cipher_spec/mod.rs new file mode 100644 index 00000000..3379d208 --- /dev/null +++ b/vendor/dtls/src/change_cipher_spec/mod.rs @@ -0,0 +1,46 @@ +#[cfg(test)] +mod change_cipher_spec_test; + +use std::io::{Read, Write}; + +use byteorder::{ReadBytesExt, WriteBytesExt}; + +use super::content::*; +use super::error::*; + +// The change cipher spec protocol exists to signal transitions in +// ciphering strategies. The protocol consists of a single message, +// which is encrypted and compressed under the current (not the pending) +// connection state. The message consists of a single byte of value 1. +/// ## Specifications +/// +/// * [RFC 5246 §7.1] +/// +/// [RFC 5246 §7.1]: https://tools.ietf.org/html/rfc5246#section-7.1 +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct ChangeCipherSpec; + +impl ChangeCipherSpec { + pub fn content_type(&self) -> ContentType { + ContentType::ChangeCipherSpec + } + + pub fn size(&self) -> usize { + 1 + } + + pub fn marshal(&self, writer: &mut W) -> Result<()> { + writer.write_u8(0x01)?; + + Ok(writer.flush()?) + } + + pub fn unmarshal(reader: &mut R) -> Result { + let data = reader.read_u8()?; + if data != 0x01 { + return Err(Error::ErrInvalidCipherSpec); + } + + Ok(ChangeCipherSpec {}) + } +} diff --git a/vendor/dtls/src/cipher_suite/cipher_suite_aes_128_ccm.rs b/vendor/dtls/src/cipher_suite/cipher_suite_aes_128_ccm.rs new file mode 100644 index 00000000..3ab6fbdc --- /dev/null +++ b/vendor/dtls/src/cipher_suite/cipher_suite_aes_128_ccm.rs @@ -0,0 +1,112 @@ +use super::*; +use crate::client_certificate_type::ClientCertificateType; +use crate::crypto::crypto_ccm::{CryptoCcm, CryptoCcmTagLen}; +use crate::prf::*; + +#[derive(Clone)] +pub struct CipherSuiteAes128Ccm { + ccm: Option, + client_certificate_type: ClientCertificateType, + id: CipherSuiteId, + psk: bool, + crypto_ccm_tag_len: CryptoCcmTagLen, +} + +impl CipherSuiteAes128Ccm { + const PRF_MAC_LEN: usize = 0; + const PRF_KEY_LEN: usize = 16; + const PRF_IV_LEN: usize = 4; + + pub fn new( + client_certificate_type: ClientCertificateType, + id: CipherSuiteId, + psk: bool, + crypto_ccm_tag_len: CryptoCcmTagLen, + ) -> Self { + CipherSuiteAes128Ccm { + ccm: None, + client_certificate_type, + id, + psk, + crypto_ccm_tag_len, + } + } +} + +impl CipherSuite for CipherSuiteAes128Ccm { + fn to_string(&self) -> String { + format!("{}", self.id) + } + + fn id(&self) -> CipherSuiteId { + self.id + } + + fn certificate_type(&self) -> ClientCertificateType { + self.client_certificate_type + } + + fn hash_func(&self) -> CipherSuiteHash { + CipherSuiteHash::Sha256 + } + + fn is_psk(&self) -> bool { + self.psk + } + + fn is_initialized(&self) -> bool { + self.ccm.is_some() + } + + fn init( + &mut self, + master_secret: &[u8], + client_random: &[u8], + server_random: &[u8], + is_client: bool, + ) -> Result<()> { + let keys = prf_encryption_keys( + master_secret, + client_random, + server_random, + CipherSuiteAes128Ccm::PRF_MAC_LEN, + CipherSuiteAes128Ccm::PRF_KEY_LEN, + CipherSuiteAes128Ccm::PRF_IV_LEN, + self.hash_func(), + )?; + + if is_client { + self.ccm = Some(CryptoCcm::new( + &self.crypto_ccm_tag_len, + &keys.client_write_key, + &keys.client_write_iv, + &keys.server_write_key, + &keys.server_write_iv, + )); + } else { + self.ccm = Some(CryptoCcm::new( + &self.crypto_ccm_tag_len, + &keys.server_write_key, + &keys.server_write_iv, + &keys.client_write_key, + &keys.client_write_iv, + )); + } + + Ok(()) + } + + fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { + let ccm = self.ccm.as_ref().ok_or(Error::Other( + "CipherSuite has not been initialized, unable to encrypt".to_owned(), + ))?; + ccm.encrypt(pkt_rlh, raw) + } + + fn decrypt(&self, input: &[u8]) -> Result> { + let ccm = self.ccm.as_ref().ok_or(Error::Other( + "CipherSuite has not been initialized, unable to decrypt".to_owned(), + ))?; + ccm.decrypt(input) + } +} diff --git a/vendor/dtls/src/cipher_suite/cipher_suite_aes_128_gcm_sha256.rs b/vendor/dtls/src/cipher_suite/cipher_suite_aes_128_gcm_sha256.rs new file mode 100644 index 00000000..0c3d5ffd --- /dev/null +++ b/vendor/dtls/src/cipher_suite/cipher_suite_aes_128_gcm_sha256.rs @@ -0,0 +1,107 @@ +use super::*; +use crate::crypto::crypto_gcm::*; +use crate::prf::*; + +#[derive(Clone)] +pub struct CipherSuiteAes128GcmSha256 { + gcm: Option, + rsa: bool, +} + +impl CipherSuiteAes128GcmSha256 { + const PRF_MAC_LEN: usize = 0; + const PRF_KEY_LEN: usize = 16; + const PRF_IV_LEN: usize = 4; + + pub fn new(rsa: bool) -> Self { + CipherSuiteAes128GcmSha256 { gcm: None, rsa } + } +} + +impl CipherSuite for CipherSuiteAes128GcmSha256 { + fn to_string(&self) -> String { + if self.rsa { + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256".to_owned() + } else { + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256".to_owned() + } + } + + fn id(&self) -> CipherSuiteId { + if self.rsa { + CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256 + } else { + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256 + } + } + + fn certificate_type(&self) -> ClientCertificateType { + if self.rsa { + ClientCertificateType::RsaSign + } else { + ClientCertificateType::EcdsaSign + } + } + + fn hash_func(&self) -> CipherSuiteHash { + CipherSuiteHash::Sha256 + } + + fn is_psk(&self) -> bool { + false + } + + fn is_initialized(&self) -> bool { + self.gcm.is_some() + } + + fn init( + &mut self, + master_secret: &[u8], + client_random: &[u8], + server_random: &[u8], + is_client: bool, + ) -> Result<()> { + let keys = prf_encryption_keys( + master_secret, + client_random, + server_random, + CipherSuiteAes128GcmSha256::PRF_MAC_LEN, + CipherSuiteAes128GcmSha256::PRF_KEY_LEN, + CipherSuiteAes128GcmSha256::PRF_IV_LEN, + self.hash_func(), + )?; + + if is_client { + self.gcm = Some(CryptoGcm::new( + &keys.client_write_key, + &keys.client_write_iv, + &keys.server_write_key, + &keys.server_write_iv, + )); + } else { + self.gcm = Some(CryptoGcm::new( + &keys.server_write_key, + &keys.server_write_iv, + &keys.client_write_key, + &keys.client_write_iv, + )); + } + + Ok(()) + } + + fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { + let cg = self.gcm.as_ref().ok_or(Error::Other( + "CipherSuite has not been initialized, unable to encrypt".to_owned(), + ))?; + cg.encrypt(pkt_rlh, raw) + } + + fn decrypt(&self, input: &[u8]) -> Result> { + let cg = self.gcm.as_ref().ok_or(Error::Other( + "CipherSuite has not been initialized, unable to decrypt".to_owned(), + ))?; + cg.decrypt(input) + } +} diff --git a/vendor/dtls/src/cipher_suite/cipher_suite_aes_256_cbc_sha.rs b/vendor/dtls/src/cipher_suite/cipher_suite_aes_256_cbc_sha.rs new file mode 100644 index 00000000..3654f93f --- /dev/null +++ b/vendor/dtls/src/cipher_suite/cipher_suite_aes_256_cbc_sha.rs @@ -0,0 +1,107 @@ +use super::*; +use crate::crypto::crypto_cbc::*; +use crate::prf::*; + +#[derive(Clone)] +pub struct CipherSuiteAes256CbcSha { + cbc: Option, + rsa: bool, +} + +impl CipherSuiteAes256CbcSha { + const PRF_MAC_LEN: usize = 20; + const PRF_KEY_LEN: usize = 32; + const PRF_IV_LEN: usize = 16; + + pub fn new(rsa: bool) -> Self { + CipherSuiteAes256CbcSha { cbc: None, rsa } + } +} + +impl CipherSuite for CipherSuiteAes256CbcSha { + fn to_string(&self) -> String { + if self.rsa { + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA".to_owned() + } else { + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA".to_owned() + } + } + + fn id(&self) -> CipherSuiteId { + if self.rsa { + CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_256_Cbc_Sha + } else { + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha + } + } + + fn certificate_type(&self) -> ClientCertificateType { + if self.rsa { + ClientCertificateType::RsaSign + } else { + ClientCertificateType::EcdsaSign + } + } + + fn hash_func(&self) -> CipherSuiteHash { + CipherSuiteHash::Sha256 + } + + fn is_psk(&self) -> bool { + false + } + + fn is_initialized(&self) -> bool { + self.cbc.is_some() + } + + fn init( + &mut self, + master_secret: &[u8], + client_random: &[u8], + server_random: &[u8], + is_client: bool, + ) -> Result<()> { + let keys = prf_encryption_keys( + master_secret, + client_random, + server_random, + CipherSuiteAes256CbcSha::PRF_MAC_LEN, + CipherSuiteAes256CbcSha::PRF_KEY_LEN, + CipherSuiteAes256CbcSha::PRF_IV_LEN, + self.hash_func(), + )?; + + if is_client { + self.cbc = Some(CryptoCbc::new( + &keys.client_write_key, + &keys.client_mac_key, + &keys.server_write_key, + &keys.server_mac_key, + )?); + } else { + self.cbc = Some(CryptoCbc::new( + &keys.server_write_key, + &keys.server_mac_key, + &keys.client_write_key, + &keys.client_mac_key, + )?); + } + + Ok(()) + } + + fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { + let cg = self.cbc.as_ref().ok_or(Error::Other( + "CipherSuite has not been initialized, unable to encrypt".to_owned(), + ))?; + cg.encrypt(pkt_rlh, raw) + } + + fn decrypt(&self, input: &[u8]) -> Result> { + let cg = self.cbc.as_ref().ok_or(Error::Other( + "CipherSuite has not been initialized, unable to decrypt".to_owned(), + ))?; + cg.decrypt(input) + } +} diff --git a/vendor/dtls/src/cipher_suite/cipher_suite_chacha20_poly1305_sha256.rs b/vendor/dtls/src/cipher_suite/cipher_suite_chacha20_poly1305_sha256.rs new file mode 100644 index 00000000..7066a4bf --- /dev/null +++ b/vendor/dtls/src/cipher_suite/cipher_suite_chacha20_poly1305_sha256.rs @@ -0,0 +1,107 @@ +use super::*; +use crate::crypto::crypto_chacha20::*; +use crate::prf::*; + +#[derive(Clone)] +pub struct CipherSuiteChaCha20Poly1305Sha256 { + rsa: bool, + cipher: Option, +} + +impl CipherSuiteChaCha20Poly1305Sha256 { + const PRF_MAC_LEN: usize = 0; + const PRF_KEY_LEN: usize = 32; + const PRF_IV_LEN: usize = 12; + + pub fn new(rsa: bool) -> Self { + CipherSuiteChaCha20Poly1305Sha256 { rsa, cipher: None } + } +} + +impl CipherSuite for CipherSuiteChaCha20Poly1305Sha256 { + fn to_string(&self) -> String { + if self.rsa { + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256".to_owned() + } else { + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256".to_owned() + } + } + + fn id(&self) -> CipherSuiteId { + if self.rsa { + CipherSuiteId::Tls_Ecdhe_Rsa_With_ChaCha20_Poly1305_Sha256 + } else { + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_ChaCha20_Poly1305_Sha256 + } + } + + fn certificate_type(&self) -> ClientCertificateType { + if self.rsa { + ClientCertificateType::RsaSign + } else { + ClientCertificateType::EcdsaSign + } + } + + fn hash_func(&self) -> CipherSuiteHash { + CipherSuiteHash::Sha256 + } + + fn is_psk(&self) -> bool { + false + } + + fn is_initialized(&self) -> bool { + self.cipher.is_some() + } + + fn init( + &mut self, + master_secret: &[u8], + client_random: &[u8], + server_random: &[u8], + is_client: bool, + ) -> Result<()> { + let keys = prf_encryption_keys( + master_secret, + client_random, + server_random, + CipherSuiteChaCha20Poly1305Sha256::PRF_MAC_LEN, + CipherSuiteChaCha20Poly1305Sha256::PRF_KEY_LEN, + CipherSuiteChaCha20Poly1305Sha256::PRF_IV_LEN, + self.hash_func(), + )?; + + if is_client { + self.cipher = Some(CryptoChaCha20::new( + &keys.client_write_key, + &keys.client_write_iv, + &keys.server_write_key, + &keys.server_write_iv, + )); + } else { + self.cipher = Some(CryptoChaCha20::new( + &keys.server_write_key, + &keys.server_write_iv, + &keys.client_write_key, + &keys.client_write_iv, + )); + } + + Ok(()) + } + + fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { + let cg = self.cipher.as_ref().ok_or(Error::Other( + "CipherSuite has not been initialized, unable to encrypt".to_owned(), + ))?; + cg.encrypt(pkt_rlh, raw) + } + + fn decrypt(&self, input: &[u8]) -> Result> { + let cg = self.cipher.as_ref().ok_or(Error::Other( + "CipherSuite has not been initialized, unable to decrypt".to_owned(), + ))?; + cg.decrypt(input) + } +} diff --git a/vendor/dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm.rs b/vendor/dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm.rs new file mode 100644 index 00000000..c5bf2dfd --- /dev/null +++ b/vendor/dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm.rs @@ -0,0 +1,12 @@ +use super::*; +use crate::cipher_suite::cipher_suite_aes_128_ccm::CipherSuiteAes128Ccm; +use crate::crypto::crypto_ccm::CryptoCcmTagLen; + +pub fn new_cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm() -> CipherSuiteAes128Ccm { + CipherSuiteAes128Ccm::new( + ClientCertificateType::EcdsaSign, + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm, + false, + CryptoCcmTagLen::CryptoCcmTagLength, + ) +} diff --git a/vendor/dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8.rs b/vendor/dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8.rs new file mode 100644 index 00000000..aa9a92ce --- /dev/null +++ b/vendor/dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8.rs @@ -0,0 +1,12 @@ +use super::*; +use crate::cipher_suite::cipher_suite_aes_128_ccm::CipherSuiteAes128Ccm; +use crate::crypto::crypto_ccm::CryptoCcmTagLen; + +pub fn new_cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8() -> CipherSuiteAes128Ccm { + CipherSuiteAes128Ccm::new( + ClientCertificateType::EcdsaSign, + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8, + false, + CryptoCcmTagLen::CryptoCcm8TagLength, + ) +} diff --git a/vendor/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm.rs b/vendor/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm.rs new file mode 100644 index 00000000..6f506e0e --- /dev/null +++ b/vendor/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm.rs @@ -0,0 +1,12 @@ +use super::*; +use crate::cipher_suite::cipher_suite_aes_128_ccm::CipherSuiteAes128Ccm; +use crate::crypto::crypto_ccm::CryptoCcmTagLen; + +pub fn new_cipher_suite_tls_psk_with_aes_128_ccm() -> CipherSuiteAes128Ccm { + CipherSuiteAes128Ccm::new( + ClientCertificateType::Unsupported, + CipherSuiteId::Tls_Psk_With_Aes_128_Ccm, + true, + CryptoCcmTagLen::CryptoCcmTagLength, + ) +} diff --git a/vendor/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm8.rs b/vendor/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm8.rs new file mode 100644 index 00000000..64b9f1a5 --- /dev/null +++ b/vendor/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm8.rs @@ -0,0 +1,12 @@ +use super::*; +use crate::cipher_suite::cipher_suite_aes_128_ccm::CipherSuiteAes128Ccm; +use crate::crypto::crypto_ccm::CryptoCcmTagLen; + +pub fn new_cipher_suite_tls_psk_with_aes_128_ccm8() -> CipherSuiteAes128Ccm { + CipherSuiteAes128Ccm::new( + ClientCertificateType::Unsupported, + CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8, + true, + CryptoCcmTagLen::CryptoCcm8TagLength, + ) +} diff --git a/vendor/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_gcm_sha256.rs b/vendor/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_gcm_sha256.rs new file mode 100644 index 00000000..cb0f3e51 --- /dev/null +++ b/vendor/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_gcm_sha256.rs @@ -0,0 +1,90 @@ +use super::*; +use crate::crypto::crypto_gcm::*; +use crate::prf::*; + +#[derive(Clone, Default)] +pub struct CipherSuiteTlsPskWithAes128GcmSha256 { + gcm: Option, +} + +impl CipherSuiteTlsPskWithAes128GcmSha256 { + const PRF_MAC_LEN: usize = 0; + const PRF_KEY_LEN: usize = 16; + const PRF_IV_LEN: usize = 4; +} + +impl CipherSuite for CipherSuiteTlsPskWithAes128GcmSha256 { + fn to_string(&self) -> String { + "TLS_PSK_WITH_AES_128_GCM_SHA256".to_owned() + } + + fn id(&self) -> CipherSuiteId { + CipherSuiteId::Tls_Psk_With_Aes_128_Gcm_Sha256 + } + + fn certificate_type(&self) -> ClientCertificateType { + ClientCertificateType::Unsupported + } + + fn hash_func(&self) -> CipherSuiteHash { + CipherSuiteHash::Sha256 + } + + fn is_psk(&self) -> bool { + true + } + + fn is_initialized(&self) -> bool { + self.gcm.is_some() + } + + fn init( + &mut self, + master_secret: &[u8], + client_random: &[u8], + server_random: &[u8], + is_client: bool, + ) -> Result<()> { + let keys = prf_encryption_keys( + master_secret, + client_random, + server_random, + CipherSuiteTlsPskWithAes128GcmSha256::PRF_MAC_LEN, + CipherSuiteTlsPskWithAes128GcmSha256::PRF_KEY_LEN, + CipherSuiteTlsPskWithAes128GcmSha256::PRF_IV_LEN, + self.hash_func(), + )?; + + if is_client { + self.gcm = Some(CryptoGcm::new( + &keys.client_write_key, + &keys.client_write_iv, + &keys.server_write_key, + &keys.server_write_iv, + )); + } else { + self.gcm = Some(CryptoGcm::new( + &keys.server_write_key, + &keys.server_write_iv, + &keys.client_write_key, + &keys.client_write_iv, + )); + } + + Ok(()) + } + + fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { + let cg = self.gcm.as_ref().ok_or(Error::Other( + "CipherSuite has not been initialized, unable to encrypt".to_owned(), + ))?; + cg.encrypt(pkt_rlh, raw) + } + + fn decrypt(&self, input: &[u8]) -> Result> { + let cg = self.gcm.as_ref().ok_or(Error::Other( + "CipherSuite has not been initialized, unable to decrypt".to_owned(), + ))?; + cg.decrypt(input) + } +} diff --git a/vendor/dtls/src/cipher_suite/mod.rs b/vendor/dtls/src/cipher_suite/mod.rs new file mode 100644 index 00000000..f6a72c7e --- /dev/null +++ b/vendor/dtls/src/cipher_suite/mod.rs @@ -0,0 +1,287 @@ +pub mod cipher_suite_aes_128_ccm; +pub mod cipher_suite_aes_128_gcm_sha256; +pub mod cipher_suite_aes_256_cbc_sha; +pub mod cipher_suite_chacha20_poly1305_sha256; +pub mod cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm; +pub mod cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8; +pub mod cipher_suite_tls_psk_with_aes_128_ccm; +pub mod cipher_suite_tls_psk_with_aes_128_ccm8; +pub mod cipher_suite_tls_psk_with_aes_128_gcm_sha256; + +use std::fmt; +use std::marker::{Send, Sync}; + +use cipher_suite_aes_128_gcm_sha256::*; +use cipher_suite_aes_256_cbc_sha::*; +use cipher_suite_chacha20_poly1305_sha256::*; +use cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm::*; +use cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8::*; +use cipher_suite_tls_psk_with_aes_128_ccm::*; +use cipher_suite_tls_psk_with_aes_128_ccm8::*; +use cipher_suite_tls_psk_with_aes_128_gcm_sha256::*; + +use super::client_certificate_type::*; +use super::error::*; +use super::record_layer::record_layer_header::*; + +// CipherSuiteID is an ID for our supported CipherSuites +// Supported Cipher Suites +#[allow(non_camel_case_types)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum CipherSuiteId { + // AES-128-CCM + Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm = 0xc0ac, + Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8 = 0xc0ae, + + // AES-128-GCM-SHA256 + Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256 = 0xc02b, + Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256 = 0xc02f, + + // AES-256-CBC-SHA + Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha = 0xc00a, + Tls_Ecdhe_Rsa_With_Aes_256_Cbc_Sha = 0xc014, + + Tls_Psk_With_Aes_128_Ccm = 0xc0a4, + Tls_Psk_With_Aes_128_Ccm_8 = 0xc0a8, + Tls_Psk_With_Aes_128_Gcm_Sha256 = 0x00a8, + + // CHACHA20_POLY1305_SHA256 + Tls_Ecdhe_Rsa_With_ChaCha20_Poly1305_Sha256 = 0xcca8, + Tls_Ecdhe_Ecdsa_With_ChaCha20_Poly1305_Sha256 = 0xcca9, + + Unsupported, +} + +impl fmt::Display for CipherSuiteId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm => { + write!(f, "TLS_ECDHE_ECDSA_WITH_AES_128_CCM") + } + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8 => { + write!(f, "TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8") + } + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256 => { + write!(f, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256") + } + CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256 => { + write!(f, "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256") + } + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha => { + write!(f, "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA") + } + CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_256_Cbc_Sha => { + write!(f, "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA") + } + CipherSuiteId::Tls_Psk_With_Aes_128_Ccm => write!(f, "TLS_PSK_WITH_AES_128_CCM"), + CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8 => write!(f, "TLS_PSK_WITH_AES_128_CCM_8"), + CipherSuiteId::Tls_Psk_With_Aes_128_Gcm_Sha256 => { + write!(f, "TLS_PSK_WITH_AES_128_GCM_SHA256") + } + CipherSuiteId::Tls_Ecdhe_Rsa_With_ChaCha20_Poly1305_Sha256 => { + write!(f, "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256") + } + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_ChaCha20_Poly1305_Sha256 => { + write!(f, "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256") + } + + _ => write!(f, "Unsupported CipherSuiteID"), + } + } +} + +impl From for CipherSuiteId { + fn from(val: u16) -> Self { + match val { + // AES-128-CCM + 0xc0ac => CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm, + 0xc0ae => CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8, + + // AES-128-GCM-SHA256 + 0xc02b => CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256, + 0xc02f => CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256, + + // AES-256-CBC-SHA + 0xc00a => CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha, + 0xc014 => CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_256_Cbc_Sha, + + 0xc0a4 => CipherSuiteId::Tls_Psk_With_Aes_128_Ccm, + 0xc0a8 => CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8, + 0x00a8 => CipherSuiteId::Tls_Psk_With_Aes_128_Gcm_Sha256, + + // CHACHA20_POLY1305_SHA256 + 0xcca8 => CipherSuiteId::Tls_Ecdhe_Rsa_With_ChaCha20_Poly1305_Sha256, + 0xcca9 => CipherSuiteId::Tls_Ecdhe_Ecdsa_With_ChaCha20_Poly1305_Sha256, + + _ => CipherSuiteId::Unsupported, + } + } +} + +impl From<&str> for CipherSuiteId { + fn from(val: &str) -> Self { + match val { + "TLS_ECDHE_ECDSA_WITH_AES_128_CCM" => CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm, + "TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8" => { + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8 + } + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" => { + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256 + } + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" => { + CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256 + } + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA" => { + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha + } + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA" => { + CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_256_Cbc_Sha + } + "TLS_PSK_WITH_AES_128_CCM" => CipherSuiteId::Tls_Psk_With_Aes_128_Ccm, + "TLS_PSK_WITH_AES_128_CCM_8" => CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8, + "TLS_PSK_WITH_AES_128_GCM_SHA256" => CipherSuiteId::Tls_Psk_With_Aes_128_Gcm_Sha256, + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256" => { + CipherSuiteId::Tls_Ecdhe_Rsa_With_ChaCha20_Poly1305_Sha256 + } + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256" => { + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_ChaCha20_Poly1305_Sha256 + } + _ => CipherSuiteId::Unsupported, + } + } +} + +#[derive(Copy, Clone, Debug)] +pub enum CipherSuiteHash { + Sha256, +} + +impl CipherSuiteHash { + pub(crate) fn size(&self) -> usize { + match *self { + CipherSuiteHash::Sha256 => 32, + } + } +} + +pub trait CipherSuite { + fn to_string(&self) -> String; + fn id(&self) -> CipherSuiteId; + fn certificate_type(&self) -> ClientCertificateType; + fn hash_func(&self) -> CipherSuiteHash; + fn is_psk(&self) -> bool; + fn is_initialized(&self) -> bool; + + // Generate the internal encryption state + fn init( + &mut self, + master_secret: &[u8], + client_random: &[u8], + server_random: &[u8], + is_client: bool, + ) -> Result<()>; + + fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result>; + fn decrypt(&self, input: &[u8]) -> Result>; +} + +// Taken from https://www.iana.org/assignments/tls-parameters/tls-parameters.xml +// A cipher_suite is a specific combination of key agreement, cipher and MAC +// function. +pub fn cipher_suite_for_id(id: CipherSuiteId) -> Result> { + match id { + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm => { + Ok(Box::new(new_cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm())) + } + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8 => Ok(Box::new( + new_cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8(), + )), + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256 => { + Ok(Box::new(CipherSuiteAes128GcmSha256::new(false))) + } + CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256 => { + Ok(Box::new(CipherSuiteAes128GcmSha256::new(true))) + } + CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_256_Cbc_Sha => { + Ok(Box::new(CipherSuiteAes256CbcSha::new(true))) + } + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha => { + Ok(Box::new(CipherSuiteAes256CbcSha::new(false))) + } + CipherSuiteId::Tls_Psk_With_Aes_128_Ccm => { + Ok(Box::new(new_cipher_suite_tls_psk_with_aes_128_ccm())) + } + CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8 => { + Ok(Box::new(new_cipher_suite_tls_psk_with_aes_128_ccm8())) + } + CipherSuiteId::Tls_Psk_With_Aes_128_Gcm_Sha256 => { + Ok(Box::::default()) + } + CipherSuiteId::Tls_Ecdhe_Rsa_With_ChaCha20_Poly1305_Sha256 => { + Ok(Box::new(CipherSuiteChaCha20Poly1305Sha256::new(true))) + } + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_ChaCha20_Poly1305_Sha256 => { + Ok(Box::new(CipherSuiteChaCha20Poly1305Sha256::new(false))) + } + + _ => Err(Error::ErrInvalidCipherSuite), + } +} + +// CipherSuites we support in order of preference +pub(crate) fn default_cipher_suites() -> Vec> { + vec![ + Box::new(CipherSuiteAes128GcmSha256::new(false)), + Box::new(CipherSuiteAes256CbcSha::new(false)), + Box::new(CipherSuiteAes128GcmSha256::new(true)), + Box::new(CipherSuiteAes256CbcSha::new(true)), + Box::new(CipherSuiteChaCha20Poly1305Sha256::new(false)), + ] +} + +fn all_cipher_suites() -> Vec> { + vec![ + Box::new(new_cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm()), + Box::new(new_cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8()), + Box::new(CipherSuiteAes128GcmSha256::new(false)), + Box::new(CipherSuiteAes128GcmSha256::new(true)), + Box::new(CipherSuiteAes256CbcSha::new(false)), + Box::new(CipherSuiteAes256CbcSha::new(true)), + Box::new(new_cipher_suite_tls_psk_with_aes_128_ccm()), + Box::new(new_cipher_suite_tls_psk_with_aes_128_ccm8()), + Box::::default(), + Box::new(CipherSuiteChaCha20Poly1305Sha256::new(false)), + Box::new(CipherSuiteChaCha20Poly1305Sha256::new(true)), + ] +} + +fn cipher_suites_for_ids(ids: &[CipherSuiteId]) -> Result>> { + let mut cipher_suites = vec![]; + for id in ids { + cipher_suites.push(cipher_suite_for_id(*id)?); + } + Ok(cipher_suites) +} + +pub(crate) fn parse_cipher_suites( + user_selected_suites: &[CipherSuiteId], + exclude_psk: bool, + exclude_non_psk: bool, +) -> Result>> { + let cipher_suites = if !user_selected_suites.is_empty() { + cipher_suites_for_ids(user_selected_suites)? + } else { + default_cipher_suites() + }; + + let filtered_cipher_suites: Vec> = cipher_suites + .into_iter() + .filter(|c| !((exclude_psk && c.is_psk()) || (exclude_non_psk && !c.is_psk()))) + .collect(); + + if filtered_cipher_suites.is_empty() { + Err(Error::ErrNoAvailableCipherSuites) + } else { + Ok(filtered_cipher_suites) + } +} diff --git a/vendor/dtls/src/client_certificate_type.rs b/vendor/dtls/src/client_certificate_type.rs new file mode 100644 index 00000000..d87aacb6 --- /dev/null +++ b/vendor/dtls/src/client_certificate_type.rs @@ -0,0 +1,16 @@ +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum ClientCertificateType { + RsaSign = 1, + EcdsaSign = 64, + Unsupported, +} + +impl From for ClientCertificateType { + fn from(val: u8) -> Self { + match val { + 1 => ClientCertificateType::RsaSign, + 64 => ClientCertificateType::EcdsaSign, + _ => ClientCertificateType::Unsupported, + } + } +} diff --git a/vendor/dtls/src/compression_methods.rs b/vendor/dtls/src/compression_methods.rs new file mode 100644 index 00000000..a03195ec --- /dev/null +++ b/vendor/dtls/src/compression_methods.rs @@ -0,0 +1,60 @@ +use std::io::{Read, Write}; + +use byteorder::{ReadBytesExt, WriteBytesExt}; + +use crate::error::Result; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum CompressionMethodId { + Null = 0, + Unsupported, +} + +impl From for CompressionMethodId { + fn from(val: u8) -> Self { + match val { + 0 => CompressionMethodId::Null, + _ => CompressionMethodId::Unsupported, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CompressionMethods { + pub ids: Vec, +} + +impl CompressionMethods { + pub fn size(&self) -> usize { + 1 + self.ids.len() + } + + pub fn marshal(&self, writer: &mut W) -> Result<()> { + writer.write_u8(self.ids.len() as u8)?; + + for id in &self.ids { + writer.write_u8(*id as u8)?; + } + + Ok(writer.flush()?) + } + + pub fn unmarshal(reader: &mut R) -> Result { + let compression_methods_count = reader.read_u8()? as usize; + let mut ids = vec![]; + for _ in 0..compression_methods_count { + let id = reader.read_u8()?.into(); + if id != CompressionMethodId::Unsupported { + ids.push(id); + } + } + + Ok(CompressionMethods { ids }) + } +} + +pub fn default_compression_methods() -> CompressionMethods { + CompressionMethods { + ids: vec![CompressionMethodId::Null], + } +} diff --git a/vendor/dtls/src/config.rs b/vendor/dtls/src/config.rs new file mode 100644 index 00000000..f1c381ea --- /dev/null +++ b/vendor/dtls/src/config.rs @@ -0,0 +1,193 @@ +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; + +use tokio::time::Duration; + +use crate::cipher_suite::*; +use crate::crypto::*; +use crate::error::*; +use crate::extension::extension_use_srtp::SrtpProtectionProfile; +use crate::handshaker::VerifyPeerCertificateFn; +use crate::signature_hash_algorithm::SignatureScheme; + +/// Config is used to configure a DTLS client or server. +/// After a Config is passed to a DTLS function it must not be modified. +#[derive(Clone)] +pub struct Config { + /// certificates contains certificate chain to present to the other side of the connection. + /// Server MUST set this if psk is non-nil + /// client SHOULD sets this so CertificateRequests can be handled if psk is non-nil + pub certificates: Vec, + + /// cipher_suites is a list of supported cipher suites. + /// If cipher_suites is nil, a default list is used + pub cipher_suites: Vec, + + /// signature_schemes contains the signature and hash schemes that the peer requests to verify. + pub signature_schemes: Vec, + + /// srtp_protection_profiles are the supported protection profiles + /// Clients will send this via use_srtp and assert that the server properly responds + /// Servers will assert that clients send one of these profiles and will respond as needed + pub srtp_protection_profiles: Vec, + + /// client_auth determines the server's policy for + /// TLS Client Authentication. The default is NoClientCert. + pub client_auth: ClientAuthType, + + /// extended_master_secret determines if the "Extended Master Secret" extension + /// should be disabled, requested, or required (default requested). + pub extended_master_secret: ExtendedMasterSecretType, + + /// flight_interval controls how often we send outbound handshake messages + /// defaults to time.Second + pub flight_interval: Duration, + + /// psk sets the pre-shared key used by this DTLS connection + /// If psk is non-nil only psk cipher_suites will be used + pub psk: Option, + pub psk_identity_hint: Option>, + + /// insecure_skip_verify controls whether a client verifies the + /// server's certificate chain and host name. + /// If insecure_skip_verify is true, TLS accepts any certificate + /// presented by the server and any host name in that certificate. + /// In this mode, TLS is susceptible to man-in-the-middle attacks. + /// This should be used only for testing. + pub insecure_skip_verify: bool, + + /// insecure_hashes allows the use of hashing algorithms that are known + /// to be vulnerable. + pub insecure_hashes: bool, + + /// insecure_verification allows the use of verification algorithms that are + /// known to be vulnerable or deprecated + pub insecure_verification: bool, + /// VerifyPeerCertificate, if not nil, is called after normal + /// certificate verification by either a client or server. It + /// receives the certificate provided by the peer and also a flag + /// that tells if normal verification has succeeded. If it returns a + /// non-nil error, the handshake is aborted and that error results. + /// + /// If normal verification fails then the handshake will abort before + /// considering this callback. If normal verification is disabled by + /// setting insecure_skip_verify, or (for a server) when client_auth is + /// RequestClientCert or RequireAnyClientCert, then this callback will + /// be considered but the verifiedChains will always be nil. + pub verify_peer_certificate: Option, + + /// roots_cas defines the set of root certificate authorities + /// that one peer uses when verifying the other peer's certificates. + /// If RootCAs is nil, TLS uses the host's root CA set. + /// Used by Client to verify server's certificate + pub roots_cas: crate::certificate_verifier::RootCertStore, + + /// client_cas defines the set of root certificate authorities + /// that servers use if required to verify a client certificate + /// by the policy in client_auth. + /// Used by Server to verify client's certificate + pub client_cas: crate::certificate_verifier::RootCertStore, + + /// server_name is used to verify the hostname on the returned + /// certificates unless insecure_skip_verify is given. + pub server_name: String, + + /// mtu is the length at which handshake messages will be fragmented to + /// fit within the maximum transmission unit (default is 1200 bytes) + pub mtu: usize, + + /// replay_protection_window is the size of the replay attack protection window. + /// Duplication of the sequence number is checked in this window size. + /// Packet with sequence number older than this value compared to the latest + /// accepted packet will be discarded. (default is 64) + pub replay_protection_window: usize, +} + +impl Default for Config { + fn default() -> Self { + Config { + certificates: vec![], + cipher_suites: vec![], + signature_schemes: vec![], + srtp_protection_profiles: vec![], + client_auth: ClientAuthType::default(), + extended_master_secret: ExtendedMasterSecretType::default(), + flight_interval: Duration::default(), + psk: None, + psk_identity_hint: None, + insecure_skip_verify: false, + insecure_hashes: false, + insecure_verification: false, + verify_peer_certificate: None, + roots_cas: crate::certificate_verifier::RootCertStore::empty(), + client_cas: crate::certificate_verifier::RootCertStore::empty(), + server_name: String::default(), + mtu: 0, + replay_protection_window: 0, + } + } +} + +pub(crate) const DEFAULT_MTU: usize = 1200; // bytes + +// PSKCallback is called once we have the remote's psk_identity_hint. +// If the remote provided none it will be nil +pub(crate) type PskCallback = + Arc Pin>> + Send>>) + Send + Sync>; + +// ClientAuthType declares the policy the server will follow for +// TLS Client Authentication. +#[derive(Default, Copy, Clone, PartialEq, Eq)] +pub enum ClientAuthType { + #[default] + NoClientCert = 0, + RequestClientCert = 1, + RequireAnyClientCert = 2, + VerifyClientCertIfGiven = 3, + RequireAndVerifyClientCert = 4, +} + +// ExtendedMasterSecretType declares the policy the client and server +// will follow for the Extended Master Secret extension +#[derive(Default, PartialEq, Eq, Copy, Clone)] +pub enum ExtendedMasterSecretType { + #[default] + Request = 0, + Require = 1, + Disable = 2, +} + +pub(crate) fn validate_config(is_client: bool, config: &Config) -> Result<()> { + if is_client && config.psk.is_some() && config.psk_identity_hint.is_none() { + return Err(Error::ErrPskAndIdentityMustBeSetForClient); + } + + if !is_client && config.psk.is_none() && config.certificates.is_empty() { + return Err(Error::ErrServerMustHaveCertificate); + } + + if !config.certificates.is_empty() && config.psk.is_some() { + return Err(Error::ErrPskAndCertificate); + } + + if config.psk_identity_hint.is_some() && config.psk.is_none() { + return Err(Error::ErrIdentityNoPsk); + } + + for cert in &config.certificates { + match cert.private_key.kind { + CryptoPrivateKeyKind::Ed25519(_) => {} + CryptoPrivateKeyKind::Ecdsa256(_) => {} + _ => return Err(Error::ErrInvalidPrivateKey), + } + } + + parse_cipher_suites( + &config.cipher_suites, + config.psk.is_none(), + config.psk.is_some(), + )?; + + Ok(()) +} diff --git a/vendor/dtls/src/conn/conn_test.rs b/vendor/dtls/src/conn/conn_test.rs new file mode 100644 index 00000000..d8c69194 --- /dev/null +++ b/vendor/dtls/src/conn/conn_test.rs @@ -0,0 +1,2464 @@ +use std::future::Future; +use std::pin::Pin; +use std::time::SystemTime; + +use rand::Rng; +use rustls_pki_types::CertificateDer; +use tokio::time::sleep; +use util::conn::conn_pipe::*; +use util::KeyingMaterialExporter; + +use super::*; +use crate::cipher_suite::cipher_suite_aes_128_gcm_sha256::*; +use crate::cipher_suite::*; +use crate::compression_methods::*; +use crate::crypto::*; +use crate::curve::*; +use crate::error::*; +use crate::extension::extension_supported_elliptic_curves::*; +use crate::extension::extension_supported_point_formats::*; +use crate::extension::extension_supported_signature_algorithms::*; +use crate::extension::renegotiation_info::ExtensionRenegotiationInfo; +use crate::extension::*; +use crate::handshake::handshake_message_certificate::*; +use crate::handshake::handshake_message_client_hello::*; +use crate::handshake::handshake_message_hello_verify_request::*; +use crate::handshake::handshake_message_server_hello::*; +use crate::handshake::handshake_message_server_hello_done::*; +use crate::handshake::handshake_message_server_key_exchange::*; +use crate::handshake::handshake_random::*; +use crate::signature_hash_algorithm::*; + +const ERR_TEST_PSK_INVALID_IDENTITY: &str = "TestPSK: Server got invalid identity"; +const ERR_PSK_REJECTED: &str = "PSK Rejected"; +const ERR_NOT_EXPECTED_CHAIN: &str = "not expected chain"; +const ERR_EXPECTED_CHAIN: &str = "expected chain"; +const ERR_WRONG_CERT: &str = "wrong cert"; + +async fn build_pipe() -> Result<(DTLSConn, DTLSConn)> { + let (ua, ub) = pipe(); + + pipe_conn(Arc::new(ua), Arc::new(ub)).await +} + +async fn pipe_conn( + ca: Arc, + cb: Arc, +) -> Result<(DTLSConn, DTLSConn)> { + let (c_tx, mut c_rx) = mpsc::channel(1); + + // Setup client + tokio::spawn(async move { + let client = create_test_client( + ca, + Config { + srtp_protection_profiles: vec![SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80], + ..Default::default() + }, + true, + ) + .await; + + let _ = c_tx.send(client).await; + }); + + // Setup server + let sever = create_test_server( + cb, + Config { + srtp_protection_profiles: vec![SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80], + ..Default::default() + }, + true, + ) + .await?; + + // Receive client + let client = match c_rx.recv().await.unwrap() { + Ok(client) => client, + Err(err) => return Err(err), + }; + + Ok((client, sever)) +} + +fn psk_callback_client(hint: &[u8]) -> Pin>> + Send>> { + trace!( + "Server's hint: {}", + String::from_utf8(hint.to_vec()).unwrap() + ); + Box::pin(async move { Ok(vec![0xAB, 0xC1, 0x23]) }) +} + +fn psk_callback_server(hint: &[u8]) -> Pin>> + Send>> { + trace!( + "Client's hint: {}", + String::from_utf8(hint.to_vec()).unwrap() + ); + Box::pin(async move { + sleep(Duration::from_millis(1)).await; // Now it's possible to await in the psk callback + Ok(vec![0xAB, 0xC1, 0x23]) + }) +} + +fn psk_callback_hint_fail(_hint: &[u8]) -> Pin>> + Send>> { + Box::pin(async move { Err(Error::Other(ERR_PSK_REJECTED.to_owned())) }) +} + +async fn create_test_client( + ca: Arc, + mut cfg: Config, + generate_certificate: bool, +) -> Result { + if generate_certificate { + let client_cert = Certificate::generate_self_signed(vec!["localhost".to_owned()])?; + cfg.certificates = vec![client_cert]; + } + + cfg.insecure_skip_verify = true; + DTLSConn::new(ca, cfg, true, None).await +} + +async fn create_test_server( + cb: Arc, + mut cfg: Config, + generate_certificate: bool, +) -> Result { + if generate_certificate { + let server_cert = Certificate::generate_self_signed(vec!["localhost".to_owned()])?; + cfg.certificates = vec![server_cert]; + } + + DTLSConn::new(cb, cfg, false, None).await +} + +#[tokio::test] +async fn test_routine_leak_on_close() -> Result<()> { + /*env_logger::Builder::new() + .format(|buf, record| { + writeln!( + buf, + "{}:{} [{}] {} - {}", + record.file().unwrap_or("unknown"), + record.line().unwrap_or(0), + record.level(), + chrono::Local::now().format("%H:%M:%S.%6f"), + record.args() + ) + }) + .filter(None, LevelFilter::Trace) + .init();*/ + + let (ca, cb) = build_pipe().await?; + + let buf_a = vec![0xFA; 100]; + let n_a = ca.write(&buf_a, Some(Duration::from_secs(5))).await?; + assert_eq!(n_a, 100); + + let mut buf_b = vec![0; 1024]; + let n_b = cb.read(&mut buf_b, Some(Duration::from_secs(5))).await?; + assert_eq!(n_a, 100); + assert_eq!(&buf_a[..], &buf_b[0..n_b]); + + cb.close().await?; + ca.close().await?; + + { + drop(ca); + drop(cb); + } + + tokio::time::sleep(Duration::from_millis(1)).await; + + Ok(()) +} + +#[tokio::test] +async fn test_sequence_number_overflow_on_application_data() -> Result<()> { + /*env_logger::Builder::new() + .format(|buf, record| { + writeln!( + buf, + "{}:{} [{}] {} - {}", + record.file().unwrap_or("unknown"), + record.line().unwrap_or(0), + record.level(), + chrono::Local::now().format("%H:%M:%S.%6f"), + record.args() + ) + }) + .filter(None, LevelFilter::Trace) + .init();*/ + + let (ca, cb) = build_pipe().await?; + + { + let mut lsn = ca.state.local_sequence_number.lock().await; + lsn[1] = MAX_SEQUENCE_NUMBER; + } + + let buf_a = vec![0xFA; 100]; + let n_a = ca.write(&buf_a, Some(Duration::from_secs(5))).await?; + assert_eq!(n_a, 100); + + let mut buf_b = vec![0; 1024]; + let n_b = cb.read(&mut buf_b, Some(Duration::from_secs(5))).await?; + assert_eq!(n_a, 100); + assert_eq!(&buf_a[..], &buf_b[0..n_b]); + + let result = ca.write(&buf_a, Some(Duration::from_secs(5))).await; + if let Err(err) = result { + assert_eq!( + err.to_string(), + Error::ErrSequenceNumberOverflow.to_string() + ); + } else { + panic!("Expected error but it is OK"); + } + + cb.close().await?; + + if let Err(err) = ca.close().await { + assert_eq!( + err.to_string(), + Error::ErrSequenceNumberOverflow.to_string() + ); + } else { + panic!("Expected error but it is OK"); + } + + { + drop(ca); + drop(cb); + } + + tokio::time::sleep(Duration::from_millis(1)).await; + + Ok(()) +} + +#[tokio::test] +async fn test_sequence_number_overflow_on_handshake() -> Result<()> { + /*env_logger::Builder::new() + .format(|buf, record| { + writeln!( + buf, + "{}:{} [{}] {} - {}", + record.file().unwrap_or("unknown"), + record.line().unwrap_or(0), + record.level(), + chrono::Local::now().format("%H:%M:%S.%6f"), + record.args() + ) + }) + .filter(None, LevelFilter::Trace) + .init();*/ + + let (ca, cb) = build_pipe().await?; + + { + let mut lsn = ca.state.local_sequence_number.lock().await; + lsn[0] = MAX_SEQUENCE_NUMBER + 1; + } + + // Try to send handshake packet. + if let Err(err) = ca + .write_packets(vec![Packet { + record: RecordLayer::new( + PROTOCOL_VERSION1_2, + 0, + Content::Handshake(Handshake::new(HandshakeMessage::ClientHello( + HandshakeMessageClientHello { + version: PROTOCOL_VERSION1_2, + random: HandshakeRandom::default(), + cookie: vec![0; 64], + + cipher_suites: vec![CipherSuiteId::Tls_Psk_With_Aes_128_Gcm_Sha256], + compression_methods: default_compression_methods(), + extensions: vec![], + }, + ))), + ), + should_encrypt: false, + reset_local_sequence_number: false, + }]) + .await + { + assert_eq!( + err.to_string(), + Error::ErrSequenceNumberOverflow.to_string() + ); + } else { + panic!("Expected error but it is OK"); + } + + cb.close().await?; + ca.close().await?; + + { + drop(ca); + drop(cb); + } + + tokio::time::sleep(Duration::from_millis(1)).await; + + Ok(()) +} + +#[tokio::test] +async fn test_handshake_with_alert() -> Result<()> { + /*env_logger::Builder::new() + .format(|buf, record| { + writeln!( + buf, + "{}:{} [{}] {} - {}", + record.file().unwrap_or("unknown"), + record.line().unwrap_or(0), + record.level(), + chrono::Local::now().format("%H:%M:%S.%6f"), + record.args() + ) + }) + .filter(None, LevelFilter::Trace) + .init();*/ + + let cases = vec![ + ( + "CipherSuiteNoIntersection", + Config { + // Server + cipher_suites: vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256], + ..Default::default() + }, + Config { + // Client + cipher_suites: vec![CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256], + ..Default::default() + }, + Error::ErrCipherSuiteNoIntersection, + Error::ErrAlertFatalOrClose, //errClient: &errAlert{&alert{alertLevelFatal, alertInsufficientSecurity}}, + ), + ( + "SignatureSchemesNoIntersection", + Config { + // Server + cipher_suites: vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256], + signature_schemes: vec![SignatureScheme::EcdsaWithP256AndSha256], + ..Default::default() + }, + Config { + // Client + cipher_suites: vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256], + signature_schemes: vec![SignatureScheme::EcdsaWithP521AndSha512], + ..Default::default() + }, + Error::ErrAlertFatalOrClose, //errServer: &errAlert{&alert{alertLevelFatal, alertInsufficientSecurity}}, + Error::ErrNoAvailableSignatureSchemes, //NoAvailableSignatureSchemes, + ), + ]; + + for (name, config_server, config_client, err_server, err_client) in cases { + let (client_err_tx, mut client_err_rx) = mpsc::channel(1); + + let (ca, cb) = pipe(); + tokio::spawn(async move { + let result = create_test_client(Arc::new(ca), config_client, true).await; + let _ = client_err_tx.send(result).await; + }); + + let result_server = create_test_server(Arc::new(cb), config_server, true).await; + if let Err(err) = result_server { + assert_eq!( + err.to_string(), + err_server.to_string(), + "{name} Server error exp({err_server}) failed({err})" + ); + } else { + panic!("{name} expected error but create_test_server return OK"); + } + + let result_client = client_err_rx.recv().await; + if let Some(result_client) = result_client { + if let Err(err) = result_client { + assert_eq!( + err.to_string(), + err_client.to_string(), + "{name} Client error exp({err_client}) failed({err})" + ); + } else { + panic!("{name} expected error but create_test_client return OK"); + } + } + } + + Ok(()) +} + +#[tokio::test] +async fn test_export_keying_material() -> Result<()> { + let export_label = "EXTRACTOR-dtls_srtp"; + let expected_server_key = vec![0x61, 0x09, 0x9d, 0x7d, 0xcb, 0x08, 0x52, 0x2c, 0xe7, 0x7b]; + let expected_client_key = vec![0x87, 0xf0, 0x40, 0x02, 0xf6, 0x1c, 0xf1, 0xfe, 0x8c, 0x77]; + + let (_decrypted_tx, decrypted_rx) = mpsc::channel(1); + let (_handshake_tx, handshake_rx) = mpsc::channel(1); + let (packet_tx, _packet_rx) = mpsc::channel(1); + let (handle_queue_tx, _handle_queue_rx) = mpsc::channel(1); + let (ca, _cb) = pipe(); + + let mut c = DTLSConn { + conn: Arc::new(ca), + state: State { + local_random: HandshakeRandom { + gmt_unix_time: SystemTime::UNIX_EPOCH + .checked_add(Duration::new(500, 0)) + .unwrap(), + ..Default::default() + }, + remote_random: HandshakeRandom { + gmt_unix_time: SystemTime::UNIX_EPOCH + .checked_add(Duration::new(1000, 0)) + .unwrap(), + ..Default::default() + }, + local_sequence_number: Arc::new(Mutex::new(vec![0, 0])), + cipher_suite: Arc::new(Mutex::new(Some(Box::new(CipherSuiteAes128GcmSha256::new( + false, + ))))), + ..Default::default() + }, + cache: HandshakeCache::new(), + decrypted_rx: Mutex::new(decrypted_rx), + handshake_completed_successfully: Arc::new(AtomicBool::new(false)), + connection_closed_by_user: false, + closed: AtomicBool::new(false), + current_flight: Box::new(Flight0 {}) as Box, + flights: None, + cfg: HandshakeConfig::default(), + retransmit: false, + handshake_rx, + + packet_tx: Arc::new(packet_tx), + handle_queue_tx, + handshake_done_tx: None, + + reader_close_tx: Mutex::new(None), + }; + + c.set_local_epoch(0); + let state = c.connection_state().await; + if let Err(err) = state.export_keying_material(export_label, &[], 0).await { + assert!( + err.to_string() + .contains(&Error::ErrHandshakeInProgress.to_string()), + "ExportKeyingMaterial when epoch == 0: expected '{}' actual '{}'", + Error::ErrHandshakeInProgress, + err, + ); + } else { + panic!("expect error but export_keying_material returns OK"); + } + + c.set_local_epoch(1); + let state = c.connection_state().await; + if let Err(err) = state.export_keying_material(export_label, &[0x00], 0).await { + assert!( + err.to_string() + .contains(&Error::ErrContextUnsupported.to_string()), + "ExportKeyingMaterial with context: expected '{}' actual '{}'", + Error::ErrContextUnsupported, + err + ); + } else { + panic!("expect error but export_keying_material returns OK"); + } + + for k in INVALID_KEYING_LABELS.iter() { + let state = c.connection_state().await; + if let Err(err) = state.export_keying_material(k, &[], 0).await { + assert!( + err.to_string() + .contains(&Error::ErrReservedExportKeyingMaterial.to_string()), + "ExportKeyingMaterial reserved label: expected '{}' actual '{}'", + Error::ErrReservedExportKeyingMaterial, + err, + ); + } else { + panic!("expect error but export_keying_material returns OK"); + } + } + + let state = c.connection_state().await; + let keying_material = state.export_keying_material(export_label, &[], 10).await?; + assert_eq!( + &keying_material, &expected_server_key, + "ExportKeyingMaterial client export: expected ({:?}) actual ({:?})", + &expected_server_key, &keying_material, + ); + + c.state.is_client = true; + let state = c.connection_state().await; + let keying_material = state.export_keying_material(export_label, &[], 10).await?; + assert_eq!( + &keying_material, &expected_client_key, + "ExportKeyingMaterial client export: expected ({:?}) actual ({:?})", + &expected_client_key, &keying_material, + ); + + Ok(()) +} + +#[tokio::test] +async fn test_psk() -> Result<()> { + /*env_logger::Builder::new() + .format(|buf, record| { + writeln!( + buf, + "{}:{} [{}] {} - {}", + record.file().unwrap_or("unknown"), + record.line().unwrap_or(0), + record.level(), + chrono::Local::now().format("%H:%M:%S.%6f"), + record.args() + ) + }) + .filter(None, LevelFilter::Trace) + .init();*/ + + let tests = vec![ + ( + "Server identity specified", + Some("Test Identity".as_bytes().to_vec()), + ), + ("Server identity nil", None), + ]; + + for (name, server_identity) in tests { + let client_identity = "Client Identity".as_bytes(); + let (client_res_tx, mut client_res_rx) = mpsc::channel(1); + + let (ca, cb) = pipe(); + tokio::spawn(async move { + let conf = Config { + psk: Some(Arc::new(psk_callback_client)), + psk_identity_hint: Some(client_identity.to_vec()), + cipher_suites: vec![CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8], + ..Default::default() + }; + + let result = create_test_client(Arc::new(ca), conf, false).await; + let _ = client_res_tx.send(result).await; + }); + + let config = Config { + psk: Some(Arc::new(psk_callback_server)), + psk_identity_hint: server_identity, + cipher_suites: vec![CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8], + ..Default::default() + }; + + let server = create_test_server(Arc::new(cb), config, false).await?; + + let actual_psk_identity_hint = &server.connection_state().await.identity_hint; + assert_eq!( + actual_psk_identity_hint, client_identity, + "TestPSK: Server ClientPSKIdentity Mismatch '{name}': expected({client_identity:?}) actual({actual_psk_identity_hint:?})", + ); + + if let Some(result) = client_res_rx.recv().await { + if let Ok(client) = result { + client.close().await?; + } else { + panic!("{name}: Expected create_test_client successfully, but got error",); + } + } + + let _ = server.close().await; + } + + Ok(()) +} + +#[tokio::test] +async fn test_psk_hint_fail() -> Result<()> { + /*env_logger::Builder::new() + .format(|buf, record| { + writeln!( + buf, + "{}:{} [{}] {} - {}", + record.file().unwrap_or("unknown"), + record.line().unwrap_or(0), + record.level(), + chrono::Local::now().format("%H:%M:%S.%6f"), + record.args() + ) + }) + .filter(None, LevelFilter::Trace) + .init();*/ + + let (client_res_tx, mut client_res_rx) = mpsc::channel(1); + + let (ca, cb) = pipe(); + tokio::spawn(async move { + let conf = Config { + psk: Some(Arc::new(psk_callback_hint_fail)), + psk_identity_hint: Some(vec![]), + cipher_suites: vec![CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8], + ..Default::default() + }; + + let result = create_test_client(Arc::new(ca), conf, false).await; + let _ = client_res_tx.send(result).await; + }); + + let config = Config { + psk: Some(Arc::new(psk_callback_hint_fail)), + psk_identity_hint: Some(vec![]), + cipher_suites: vec![CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8], + ..Default::default() + }; + + if let Err(server_err) = create_test_server(Arc::new(cb), config, false).await { + assert_eq!( + server_err.to_string(), + Error::ErrAlertFatalOrClose.to_string(), + "TestPSK: Server error exp({}) failed({})", + Error::ErrAlertFatalOrClose, + server_err, + ); + } else { + panic!("Expected server error, but got OK"); + } + + let result = client_res_rx.recv().await; + if let Some(client) = result { + if let Err(client_err) = client { + assert!( + client_err.to_string().contains(ERR_PSK_REJECTED), + "TestPSK: Client error exp({ERR_PSK_REJECTED}) failed({client_err})", + ); + } else { + panic!("Expected client error, but got OK"); + } + } + + Ok(()) +} + +#[tokio::test] +async fn test_client_timeout() -> Result<()> { + /*env_logger::Builder::new() + .format(|buf, record| { + writeln!( + buf, + "{}:{} [{}] {} - {}", + record.file().unwrap_or("unknown"), + record.line().unwrap_or(0), + record.level(), + chrono::Local::now().format("%H:%M:%S.%6f"), + record.args() + ) + }) + .filter(None, LevelFilter::Trace) + .init();*/ + + let (client_res_tx, mut client_res_rx) = mpsc::channel(1); + + let (ca, _cb) = pipe(); + tokio::spawn(async move { + let conf = Config::default(); + let result = tokio::time::timeout( + Duration::from_millis(100), + create_test_client(Arc::new(ca), conf, true), + ) + .await; + let _ = client_res_tx.send(result).await; + }); + + // no server! + let result = client_res_rx.recv().await; + if let Some(client_timeout_result) = result { + assert!(client_timeout_result.is_err(), "Expected Error but got Ok"); + } + + Ok(()) +} + +//use std::io::Write; + +#[tokio::test] +async fn test_srtp_configuration() -> Result<()> { + /*env_logger::Builder::new() + .format(|buf, record| { + writeln!( + buf, + "{}:{} [{}] {} - {}", + record.file().unwrap_or("unknown"), + record.line().unwrap_or(0), + record.level(), + chrono::Local::now().format("%H:%M:%S.%6f"), + record.args() + ) + }) + .filter(None, LevelFilter::Trace) + .init();*/ + + #[allow(clippy::type_complexity)] + let tests: Vec<( + &str, + Vec, + Vec, + SrtpProtectionProfile, + Option, + Option, + )> = vec![ + ( + "No SRTP in use", + vec![], + vec![], + SrtpProtectionProfile::Unsupported, + None, + None, + ), + ( + "SRTP both ends", + vec![SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80], + vec![SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80], + SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80, + None, + None, + ), + ( + "SRTP client only", + vec![SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80], + vec![], + SrtpProtectionProfile::Unsupported, + Some(Error::ErrAlertFatalOrClose), + Some(Error::ErrServerNoMatchingSrtpProfile), + ), + ( + "SRTP server only", + vec![], + vec![SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80], + SrtpProtectionProfile::Unsupported, + None, + None, + ), + ( + "Multiple Suites", + vec![ + SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80, + SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_32, + ], + vec![ + SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80, + SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_32, + ], + SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80, + None, + None, + ), + ( + "Multiple Suites, Client Chooses", + vec![ + SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80, + SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_32, + ], + vec![ + SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_32, + SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80, + ], + SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80, + None, + None, + ), + ]; + + for (name, client_srtp, server_srtp, expected_profile, want_client_err, want_server_err) in + tests + { + let (client_res_tx, mut client_res_rx) = mpsc::channel(1); + let (ca, cb) = pipe(); + tokio::spawn(async move { + let conf = Config { + srtp_protection_profiles: client_srtp, + ..Default::default() + }; + + let result = create_test_client(Arc::new(ca), conf, true).await; + let _ = client_res_tx.send(result).await; + }); + + let config = Config { + srtp_protection_profiles: server_srtp, + ..Default::default() + }; + + let result = create_test_server(Arc::new(cb), config, true).await; + if let Some(expected_err) = want_server_err { + if let Err(err) = result { + assert_eq!( + err.to_string(), + expected_err.to_string(), + "{name} TestPSK: Server error exp({expected_err}) failed({err})", + ); + } else { + panic!("{name} expected error, but got ok"); + } + } else { + match result { + Ok(server) => { + let actual_server_srtp = server.selected_srtpprotection_profile(); + assert_eq!(actual_server_srtp, expected_profile, + "test_srtp_configuration: Server SRTPProtectionProfile Mismatch '{name}': expected({expected_profile:?}) actual({actual_server_srtp:?})"); + } + Err(err) => { + panic!("{name} expected no error: {err}"); + } + }; + } + + let client_result = client_res_rx.recv().await; + if let Some(result) = client_result { + if let Some(expected_err) = want_client_err { + if let Err(err) = result { + assert_eq!( + err.to_string(), + expected_err.to_string(), + "TestPSK: Client error exp({expected_err}) failed({err})", + ); + } else { + panic!("{name} expected error, but got ok"); + } + } else if let Ok(client) = result { + let actual_client_srtp = client.selected_srtpprotection_profile(); + assert_eq!(actual_client_srtp, expected_profile, + "test_srtp_configuration: Client SRTPProtectionProfile Mismatch '{name}': expected({expected_profile:?}) actual({actual_client_srtp:?})"); + } else { + panic!("{name} expected no error"); + } + } else { + panic!("{name} expected client, but got none"); + } + } + + Ok(()) +} + +#[tokio::test] +async fn test_client_certificate() -> Result<()> { + /*env_logger::Builder::new() + .format(|buf, record| { + writeln!( + buf, + "{}:{} [{}] {} - {}", + record.file().unwrap_or("unknown"), + record.line().unwrap_or(0), + record.level(), + chrono::Local::now().format("%H:%M:%S.%6f"), + record.args() + ) + }) + .filter(None, LevelFilter::Trace) + .init();*/ + + let server_name = "localhost".to_owned(); + + let srv_cert = Certificate::generate_self_signed(vec!["localhost".to_owned()])?; + let mut srv_ca_pool = crate::certificate_verifier::RootCertStore::empty(); + srv_ca_pool + .add(srv_cert.certificate[0].to_owned()) + .map_err(|_err| Error::Other("add srv_cert error".to_owned()))?; + + let cert = Certificate::generate_self_signed(vec!["localhost".to_owned()])?; + let mut ca_pool = crate::certificate_verifier::RootCertStore::empty(); + ca_pool + .add(cert.certificate[0].to_owned()) + .map_err(|_err| Error::Other("add cert error".to_owned()))?; + + let tests = vec![ + ( + "NoClientCert", + Config { + roots_cas: srv_ca_pool.clone(), + server_name: server_name.clone(), + ..Default::default() + }, + Config { + certificates: vec![srv_cert.clone()], + client_auth: ClientAuthType::NoClientCert, + ..Default::default() + }, + false, + ), + ( + "NoClientCert_cert", + Config { + roots_cas: srv_ca_pool.clone(), + server_name: server_name.clone(), + certificates: vec![cert.clone()], + ..Default::default() + }, + Config { + certificates: vec![srv_cert.clone()], + client_auth: ClientAuthType::RequireAnyClientCert, + ..Default::default() + }, + false, + ), + ( + "RequestClientCert_cert", + Config { + roots_cas: srv_ca_pool.clone(), + server_name: server_name.clone(), + certificates: vec![cert.clone()], + ..Default::default() + }, + Config { + certificates: vec![srv_cert.clone()], + client_auth: ClientAuthType::RequestClientCert, + ..Default::default() + }, + false, + ), + ( + "RequestClientCert_no_cert", + Config { + roots_cas: srv_ca_pool.clone(), + server_name: server_name.clone(), + ..Default::default() + }, + Config { + certificates: vec![srv_cert.clone()], + client_auth: ClientAuthType::RequestClientCert, + ..Default::default() + }, + false, + ), + ( + "RequireAnyClientCert", + Config { + roots_cas: srv_ca_pool.clone(), + server_name: server_name.clone(), + certificates: vec![cert.clone()], + ..Default::default() + }, + Config { + certificates: vec![srv_cert.clone()], + client_auth: ClientAuthType::RequireAnyClientCert, + ..Default::default() + }, + false, + ), + ( + "RequireAnyClientCert_error", + Config { + roots_cas: srv_ca_pool.clone(), + server_name: server_name.clone(), + ..Default::default() + }, + Config { + certificates: vec![srv_cert.clone()], + client_auth: ClientAuthType::RequireAnyClientCert, + ..Default::default() + }, + true, + ), + ( + "VerifyClientCertIfGiven_no_cert", + Config { + roots_cas: srv_ca_pool.clone(), + server_name: server_name.clone(), + ..Default::default() + }, + Config { + certificates: vec![srv_cert.clone()], + client_auth: ClientAuthType::VerifyClientCertIfGiven, + client_cas: ca_pool.clone(), + ..Default::default() + }, + false, + ), + ( + "VerifyClientCertIfGiven_cert", + Config { + roots_cas: srv_ca_pool.clone(), + server_name: server_name.clone(), + certificates: vec![cert.clone()], + ..Default::default() + }, + Config { + certificates: vec![srv_cert.clone()], + client_auth: ClientAuthType::VerifyClientCertIfGiven, + client_cas: ca_pool.clone(), + ..Default::default() + }, + false, + ), + ( + "VerifyClientCertIfGiven_error", + Config { + roots_cas: srv_ca_pool.clone(), + server_name: server_name.clone(), + certificates: vec![cert.clone()], + ..Default::default() + }, + Config { + certificates: vec![srv_cert.clone()], + client_auth: ClientAuthType::VerifyClientCertIfGiven, + ..Default::default() + }, + true, + ), + ( + "RequireAndVerifyClientCert", + Config { + roots_cas: srv_ca_pool.clone(), + server_name: server_name.clone(), + certificates: vec![cert.clone()], + ..Default::default() + }, + Config { + certificates: vec![srv_cert.clone()], + client_auth: ClientAuthType::RequireAndVerifyClientCert, + client_cas: ca_pool.clone(), + ..Default::default() + }, + false, + ), + ]; + + for (name, client_cfg, server_cfg, want_err) in tests { + let (client_res_tx, mut client_res_rx) = mpsc::channel(1); + let (ca, cb) = pipe(); + let client_cfg_clone = client_cfg.clone(); + tokio::spawn(async move { + let result = DTLSConn::new(Arc::new(ca), client_cfg_clone, true, None).await; + let _ = client_res_tx.send(result).await; + }); + + let result = DTLSConn::new(Arc::new(cb), server_cfg.clone(), false, None).await; + let client_result = client_res_rx.recv().await; + + if want_err { + if result.is_err() { + continue; + } + panic!("{name} Error expected"); + } + + assert!( + result.is_ok(), + "{} Server failed({:?})", + name, + result.err().unwrap() + ); + assert!(client_result.is_some(), "{name}, expected client conn"); + + let res = client_result.unwrap(); + assert!( + res.is_ok(), + "{} Client failed({:?})", + name, + res.err().unwrap() + ); + + let server = result.unwrap(); + let client = res.unwrap(); + + let actual_client_cert = &server.connection_state().await.peer_certificates; + if server_cfg.client_auth == ClientAuthType::RequireAnyClientCert + || server_cfg.client_auth == ClientAuthType::RequireAndVerifyClientCert + { + assert!( + !actual_client_cert.is_empty(), + "{name} Client did not provide a certificate", + ); + //if actual_client_cert.len() != len(tt.clientCfg.Certificates[0].Certificate) || !bytes.Equal(tt.clientCfg.Certificates[0].Certificate[0], actual_client_cert[0]) { + assert_eq!( + actual_client_cert[0], + client_cfg.certificates[0].certificate[0].as_ref(), + "{name} Client certificate was not communicated correctly", + ); + } + + if server_cfg.client_auth == ClientAuthType::NoClientCert { + assert!( + actual_client_cert.is_empty(), + "{name} Client certificate wasn't expected", + ); + } + + let actual_server_cert = &client.connection_state().await.peer_certificates; + assert!( + !actual_server_cert.is_empty(), + "{name} Server did not provide a certificate", + ); + + /*if len(actual_server_cert) != len(tt.serverCfg.Certificates[0].Certificate) + || !bytes.Equal( + tt.serverCfg.Certificates[0].Certificate[0], + actual_server_cert[0], + )*/ + assert_eq!( + actual_server_cert[0].len(), + server_cfg.certificates[0].certificate[0].as_ref().len(), + "{name} Server certificate was not communicated correctly", + ); + assert_eq!( + actual_server_cert[0], + server_cfg.certificates[0].certificate[0].as_ref(), + "{name} Server certificate was not communicated correctly", + ); + } + + Ok(()) +} + +#[tokio::test] +async fn test_extended_master_secret() -> Result<()> { + /*env_logger::Builder::new() + .format(|buf, record| { + writeln!( + buf, + "{}:{} [{}] {} - {}", + record.file().unwrap_or("unknown"), + record.line().unwrap_or(0), + record.level(), + chrono::Local::now().format("%H:%M:%S.%6f"), + record.args() + ) + }) + .filter(None, LevelFilter::Trace) + .init();*/ + + let tests = vec![ + ( + "Request_Request_ExtendedMasterSecret", + Config { + extended_master_secret: ExtendedMasterSecretType::Request, + ..Default::default() + }, + Config { + extended_master_secret: ExtendedMasterSecretType::Request, + ..Default::default() + }, + None, + None, + ), + ( + "Request_Require_ExtendedMasterSecret", + Config { + extended_master_secret: ExtendedMasterSecretType::Request, + ..Default::default() + }, + Config { + extended_master_secret: ExtendedMasterSecretType::Require, + ..Default::default() + }, + None, + None, + ), + ( + "Request_Disable_ExtendedMasterSecret", + Config { + extended_master_secret: ExtendedMasterSecretType::Request, + ..Default::default() + }, + Config { + extended_master_secret: ExtendedMasterSecretType::Disable, + ..Default::default() + }, + None, + None, + ), + ( + "Require_Request_ExtendedMasterSecret", + Config { + extended_master_secret: ExtendedMasterSecretType::Require, + ..Default::default() + }, + Config { + extended_master_secret: ExtendedMasterSecretType::Request, + ..Default::default() + }, + None, + None, + ), + ( + "Require_Require_ExtendedMasterSecret", + Config { + extended_master_secret: ExtendedMasterSecretType::Require, + ..Default::default() + }, + Config { + extended_master_secret: ExtendedMasterSecretType::Require, + ..Default::default() + }, + None, + None, + ), + ( + "Require_Disable_ExtendedMasterSecret", + Config { + extended_master_secret: ExtendedMasterSecretType::Require, + ..Default::default() + }, + Config { + extended_master_secret: ExtendedMasterSecretType::Disable, + ..Default::default() + }, + Some(Error::ErrClientRequiredButNoServerEms), + Some(Error::ErrAlertFatalOrClose), + ), + ( + "Disable_Request_ExtendedMasterSecret", + Config { + extended_master_secret: ExtendedMasterSecretType::Disable, + ..Default::default() + }, + Config { + extended_master_secret: ExtendedMasterSecretType::Request, + ..Default::default() + }, + None, + None, + ), + ( + "Disable_Require_ExtendedMasterSecret", + Config { + extended_master_secret: ExtendedMasterSecretType::Disable, + ..Default::default() + }, + Config { + extended_master_secret: ExtendedMasterSecretType::Require, + ..Default::default() + }, + Some(Error::ErrAlertFatalOrClose), + Some(Error::ErrServerRequiredButNoClientEms), + ), + ( + "Disable_Disable_ExtendedMasterSecret", + Config { + extended_master_secret: ExtendedMasterSecretType::Disable, + ..Default::default() + }, + Config { + extended_master_secret: ExtendedMasterSecretType::Disable, + ..Default::default() + }, + None, + None, + ), + ]; + + for (name, client_cfg, server_cfg, expected_client_err, expected_server_err) in tests { + let (client_res_tx, mut client_res_rx) = mpsc::channel(1); + let (ca, cb) = pipe(); + let client_cfg_clone = client_cfg.clone(); + tokio::spawn(async move { + let result = create_test_client(Arc::new(ca), client_cfg_clone, true).await; + let _ = client_res_tx.send(result).await; + }); + + let result = create_test_server(Arc::new(cb), server_cfg.clone(), true).await; + let client_result = client_res_rx.recv().await; + assert!(client_result.is_some(), "{name}, expected client conn"); + let res = client_result.unwrap(); + + if let Some(client_err) = expected_client_err { + if let Err(err) = res { + assert_eq!( + err.to_string(), + client_err.to_string(), + "Client error expected: \"{client_err}\" but got \"{err}\"", + ); + } else { + panic!("{name} expected err, but got ok"); + } + } else { + assert!(res.is_ok(), "{name} expected ok, but got err"); + } + + if let Some(server_err) = expected_server_err { + if let Err(err) = result { + assert_eq!( + err.to_string(), + server_err.to_string(), + "Server error expected: \"{server_err}\" but got \"{err}\"", + ); + } else { + panic!("{name} expected err, but got ok"); + } + } else { + assert!(result.is_ok(), "{name} expected ok, but got err"); + } + } + + Ok(()) +} + +fn fn_not_expected_chain(_cert: &[Vec], chain: &[CertificateDer<'static>]) -> Result<()> { + if !chain.is_empty() { + return Err(Error::Other(ERR_NOT_EXPECTED_CHAIN.to_owned())); + } + Ok(()) +} + +fn fn_expected_chain(_cert: &[Vec], chain: &[CertificateDer<'static>]) -> Result<()> { + if chain.is_empty() { + return Err(Error::Other(ERR_EXPECTED_CHAIN.to_owned())); + } + Ok(()) +} + +fn fn_wrong_cert(_cert: &[Vec], _chain: &[CertificateDer<'static>]) -> Result<()> { + Err(Error::Other(ERR_WRONG_CERT.to_owned())) +} + +#[tokio::test] +async fn test_server_certificate() -> Result<()> { + /*env_logger::Builder::new() + .format(|buf, record| { + writeln!( + buf, + "{}:{} [{}] {} - {}", + record.file().unwrap_or("unknown"), + record.line().unwrap_or(0), + record.level(), + chrono::Local::now().format("%H:%M:%S.%6f"), + record.args() + ) + }) + .filter(None, LevelFilter::Trace) + .init();*/ + + let server_name = "localhost".to_owned(); + let cert = Certificate::generate_self_signed(vec![server_name.clone()])?; + let mut ca_pool = crate::certificate_verifier::RootCertStore::empty(); + ca_pool + .add(cert.certificate[0].clone()) + .map_err(|_err| Error::Other("add cert error".to_owned()))?; + + let tests = vec![ + ( + "no_ca", + Config { + server_name: server_name.clone(), + ..Default::default() + }, + Config { + certificates: vec![cert.clone()], + client_auth: ClientAuthType::NoClientCert, + ..Default::default() + }, + true, + ), + ( + "good_ca", + Config { + roots_cas: ca_pool.clone(), + server_name: server_name.clone(), + ..Default::default() + }, + Config { + certificates: vec![cert.clone()], + client_auth: ClientAuthType::NoClientCert, + ..Default::default() + }, + false, + ), + ( + "no_ca_skip_verify", + Config { + insecure_skip_verify: true, + server_name: server_name.clone(), + ..Default::default() + }, + Config { + certificates: vec![cert.clone()], + client_auth: ClientAuthType::NoClientCert, + ..Default::default() + }, + false, + ), + ( + "good_ca_skip_verify_custom_verify_peer", + Config { + roots_cas: ca_pool.clone(), + server_name: server_name.clone(), + certificates: vec![cert.clone()], + ..Default::default() + }, + Config { + certificates: vec![cert.clone()], + client_auth: ClientAuthType::RequireAnyClientCert, + verify_peer_certificate: Some(Arc::new(fn_not_expected_chain)), + ..Default::default() + }, + false, + ), + ( + "good_ca_verify_custom_verify_peer", + Config { + roots_cas: ca_pool.clone(), + server_name: server_name.clone(), + certificates: vec![cert.clone()], + ..Default::default() + }, + Config { + certificates: vec![cert.clone()], + client_auth: ClientAuthType::RequireAndVerifyClientCert, + client_cas: ca_pool.clone(), + verify_peer_certificate: Some(Arc::new(fn_expected_chain)), + ..Default::default() + }, + false, + ), + ( + "good_ca_custom_verify_peer", + Config { + roots_cas: ca_pool.clone(), + server_name: server_name.clone(), + verify_peer_certificate: Some(Arc::new(fn_wrong_cert)), + ..Default::default() + }, + Config { + certificates: vec![cert.clone()], + client_auth: ClientAuthType::NoClientCert, + ..Default::default() + }, + true, + ), + ( + "server_name", + Config { + roots_cas: ca_pool.clone(), + server_name: server_name.clone(), + ..Default::default() + }, + Config { + certificates: vec![cert.clone()], + client_auth: ClientAuthType::NoClientCert, + ..Default::default() + }, + false, + ), + ( + "server_name_error", + Config { + roots_cas: ca_pool.clone(), + server_name: "barfoo".to_owned(), + ..Default::default() + }, + Config { + certificates: vec![cert.clone()], + client_auth: ClientAuthType::NoClientCert, + ..Default::default() + }, + true, + ), + ]; + + for (name, client_cfg, server_cfg, want_err) in tests { + let (res_tx, mut res_rx) = mpsc::channel(1); + let (ca, cb) = pipe(); + + tokio::spawn(async move { + let result = DTLSConn::new(Arc::new(cb), server_cfg, false, None).await; + let _ = res_tx.send(result).await; + }); + + let cli_result = DTLSConn::new(Arc::new(ca), client_cfg, true, None).await; + + if !want_err && cli_result.is_err() { + panic!("{}: Client failed({})", name, cli_result.err().unwrap()); + } + if want_err && cli_result.is_ok() { + panic!("{name}: Error expected"); + } + + let _ = res_rx.recv().await; + } + Ok(()) +} + +#[tokio::test] +async fn test_cipher_suite_configuration() -> Result<()> { + /*env_logger::Builder::new() + .format(|buf, record| { + writeln!( + buf, + "{}:{} [{}] {} - {}", + record.file().unwrap_or("unknown"), + record.line().unwrap_or(0), + record.level(), + chrono::Local::now().format("%H:%M:%S.%6f"), + record.args() + ) + }) + .filter(None, LevelFilter::Trace) + .init();*/ + + let tests = vec![ + ( + "No CipherSuites specified", + vec![], + vec![], + None, + None, + None, + ), + ( + "Invalid CipherSuite", + vec![CipherSuiteId::Unsupported], + vec![CipherSuiteId::Unsupported], + Some(Error::ErrInvalidCipherSuite), + Some(Error::ErrInvalidCipherSuite), + None, + ), + ( + "Valid CipherSuites specified", + vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256], + vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256], + None, + None, + Some(CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256), + ), + ( + "CipherSuites mismatch", + vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256], + vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha], + Some(Error::ErrAlertFatalOrClose), + Some(Error::ErrCipherSuiteNoIntersection), + None, + ), + ( + "Valid CipherSuites CCM specified", + vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm], + vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm], + None, + None, + Some(CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm), + ), + ( + "Valid CipherSuites CCM-8 specified", + vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8], + vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8], + None, + None, + Some(CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8), + ), + ( + "Server supports subset of client suites", + vec![ + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256, + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha, + ], + vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha], + None, + None, + Some(CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha), + ), + ]; + + for ( + name, + client_cipher_suites, + server_cipher_suites, + want_client_error, + want_server_error, + want_selected_cipher_suite, + ) in tests + { + let (client_res_tx, mut client_res_rx) = mpsc::channel(1); + let (ca, cb) = pipe(); + tokio::spawn(async move { + let conf = Config { + cipher_suites: client_cipher_suites, + ..Default::default() + }; + + let result = create_test_client(Arc::new(ca), conf, true).await; + let _ = client_res_tx.send(result).await; + }); + + let config = Config { + cipher_suites: server_cipher_suites, + ..Default::default() + }; + + let result = create_test_server(Arc::new(cb), config, true).await; + if let Some(expected_err) = want_server_error { + if let Err(err) = result { + assert_eq!( + err.to_string(), + expected_err.to_string(), + "{name} test_cipher_suite_configuration: Server error exp({expected_err}) failed({err})", + ); + } else { + panic!("{name} expected error, but got ok"); + } + } else { + assert!(result.is_ok(), "{name} expected ok, but got error") + } + + let client_result = client_res_rx.recv().await; + if let Some(result) = client_result { + if let Some(expected_err) = want_client_error { + if let Err(err) = result { + assert_eq!( + err.to_string(), + expected_err.to_string(), + "{name} test_cipher_suite_configuration: Client error exp({expected_err}) failed({err})", + ); + } else { + panic!("{name} expected error, but got ok"); + } + } else { + assert!(result.is_ok(), "{name} expected ok, but got error"); + let client = result.unwrap(); + if let Some(want_cs) = want_selected_cipher_suite { + let cipher_suite = client.state.cipher_suite.lock().await; + assert!(cipher_suite.is_some(), "{name} expected some, but got none"); + if let Some(cs) = &*cipher_suite { + assert_eq!(cs.id(), want_cs, + "test_cipher_suite_configuration: Server Selected Bad Cipher Suite '{}': expected({}) actual({})", + name, want_cs, cs.id()); + } + } + } + } else { + panic!("{name} expected Some, but got None"); + } + } + + Ok(()) +} + +fn psk_callback(_b: &[u8]) -> Pin>> + Send>> { + Box::pin(async move { Ok(vec![0x00, 0x01, 0x02]) }) +} + +#[tokio::test] +async fn test_psk_configuration() -> Result<()> { + /*env_logger::Builder::new() + .format(|buf, record| { + writeln!( + buf, + "{}:{} [{}] {} - {}", + record.file().unwrap_or("unknown"), + record.line().unwrap_or(0), + record.level(), + chrono::Local::now().format("%H:%M:%S.%6f"), + record.args() + ) + }) + .filter(None, LevelFilter::Trace) + .init();*/ + + let tests = vec![ + ( + "PSK specified", + false, + false, + true, //Some(psk_callback), + true, //Some(psk_callback), + Some(vec![0x00]), + Some(vec![0x00]), + Some(Error::ErrNoAvailableCipherSuites), + Some(Error::ErrNoAvailableCipherSuites), + ), + ( + "PSK and certificate specified", + true, + true, + true, //Some(psk_callback), + true, //Some(psk_callback), + Some(vec![0x00]), + Some(vec![0x00]), + Some(Error::ErrPskAndCertificate), + Some(Error::ErrPskAndCertificate), + ), + ( + "PSK and no identity specified", + false, + false, + true, //Some(psk_callback), + true, //Some(psk_callback), + None, + None, + Some(Error::ErrPskAndIdentityMustBeSetForClient), + Some(Error::ErrNoAvailableCipherSuites), + ), + ( + "No PSK and identity specified", + false, + false, + false, + false, + Some(vec![0x00]), + Some(vec![0x00]), + Some(Error::ErrIdentityNoPsk), + Some(Error::ErrServerMustHaveCertificate), + ), + ]; + + for ( + name, + client_has_certificate, + server_has_certificate, + client_psk, + server_psk, + client_psk_identity, + server_psk_identity, + want_client_error, + want_server_error, + ) in tests + { + let (client_res_tx, mut client_res_rx) = mpsc::channel(1); + let (ca, cb) = pipe(); + tokio::spawn(async move { + let conf = Config { + psk: if client_psk { + Some(Arc::new(psk_callback)) + } else { + None + }, + psk_identity_hint: client_psk_identity, + ..Default::default() + }; + + let result = create_test_client(Arc::new(ca), conf, client_has_certificate).await; + let _ = client_res_tx.send(result).await; + }); + + let config = Config { + psk: if server_psk { + Some(Arc::new(psk_callback)) + } else { + None + }, + psk_identity_hint: server_psk_identity, + ..Default::default() + }; + + let result = create_test_server(Arc::new(cb), config, server_has_certificate).await; + if let Some(expected_err) = want_server_error { + if let Err(err) = result { + assert_eq!( + err.to_string(), + expected_err.to_string(), + "{name} test_psk_configuration: Server error exp({expected_err}) failed({err})", + ); + } else { + panic!("{name} expected error, but got ok"); + } + } else { + assert!(result.is_ok(), "{name} expected ok, but got error") + } + + let client_result = client_res_rx.recv().await; + if let Some(result) = client_result { + if let Some(expected_err) = want_client_error { + if let Err(err) = result { + assert_eq!( + err.to_string(), + expected_err.to_string(), + "{name} test_psk_configuration: Client error exp({expected_err}) failed({err})", + ); + } else { + panic!("{name} expected error, but got ok"); + } + } else { + assert!(result.is_ok(), "{name} expected ok, but got error"); + } + } else { + panic!("{name} expected Some, but got None"); + } + } + + Ok(()) +} + +#[tokio::test] +async fn test_server_timeout() -> Result<()> { + /*env_logger::Builder::new() + .format(|buf, record| { + writeln!( + buf, + "{}:{} [{}] {} - {}", + record.file().unwrap_or("unknown"), + record.line().unwrap_or(0), + record.level(), + chrono::Local::now().format("%H:%M:%S.%6f"), + record.args() + ) + }) + .filter(None, LevelFilter::Trace) + .init();*/ + + let mut cookie = vec![0u8; 20]; + rand::rng().fill(cookie.as_mut_slice()); + + let random_bytes = [0u8; RANDOM_BYTES_LENGTH]; + let gmt_unix_time = SystemTime::UNIX_EPOCH + .checked_add(Duration::new(500, 0)) + .unwrap(); + let random = HandshakeRandom { + gmt_unix_time, + random_bytes, + }; + + let cipher_suites = vec![ + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256, //&cipherSuiteTLSEcdheEcdsaWithAes128GcmSha256{}, + CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256, //&cipherSuiteTLSEcdheRsaWithAes128GcmSha256{}, + ]; + + let extensions = vec![ + Extension::SupportedSignatureAlgorithms(ExtensionSupportedSignatureAlgorithms { + signature_hash_algorithms: vec![ + SignatureHashAlgorithm { + hash: HashAlgorithm::Sha256, + signature: SignatureAlgorithm::Ecdsa, + }, + SignatureHashAlgorithm { + hash: HashAlgorithm::Sha384, + signature: SignatureAlgorithm::Ecdsa, + }, + SignatureHashAlgorithm { + hash: HashAlgorithm::Sha512, + signature: SignatureAlgorithm::Ecdsa, + }, + SignatureHashAlgorithm { + hash: HashAlgorithm::Sha256, + signature: SignatureAlgorithm::Rsa, + }, + SignatureHashAlgorithm { + hash: HashAlgorithm::Sha384, + signature: SignatureAlgorithm::Rsa, + }, + SignatureHashAlgorithm { + hash: HashAlgorithm::Sha512, + signature: SignatureAlgorithm::Rsa, + }, + ], + }), + Extension::SupportedEllipticCurves(ExtensionSupportedEllipticCurves { + elliptic_curves: vec![NamedCurve::X25519, NamedCurve::P256, NamedCurve::P384], + }), + Extension::SupportedPointFormats(ExtensionSupportedPointFormats { + point_formats: vec![ELLIPTIC_CURVE_POINT_FORMAT_UNCOMPRESSED], + }), + ]; + + let record = RecordLayer::new( + PROTOCOL_VERSION1_2, + 0, + Content::Handshake(Handshake::new(HandshakeMessage::ClientHello( + HandshakeMessageClientHello { + version: PROTOCOL_VERSION1_2, + cookie, + random, + cipher_suites, + compression_methods: default_compression_methods(), + extensions, + }, + ))), + ); + + let mut packet = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(packet.as_mut()); + record.marshal(&mut writer)?; + } + + use util::Conn; + let (ca, cb) = pipe(); + + // Client reader + let (ca_read_chan_tx, mut ca_read_chan_rx) = mpsc::channel(1000); + + let ca_rx = Arc::new(ca); + let ca_tx = Arc::clone(&ca_rx); + + tokio::spawn(async move { + let mut data = vec![0; 8192]; + loop { + if let Ok(n) = ca_rx.recv(&mut data).await { + let result = ca_read_chan_tx.send(data[..n].to_vec()).await; + if result.is_ok() { + return; + } + } else { + return; + } + } + }); + + // Start sending ClientHello packets until server responds with first packet + tokio::spawn(async move { + loop { + let timer = tokio::time::sleep(Duration::from_millis(10)); + tokio::pin!(timer); + + tokio::select! { + _ = timer.as_mut() => { + let result = ca_tx.send(&packet).await; + if result.is_err() { + return; + } + } + _ = ca_read_chan_rx.recv() => return, + } + } + }); + + let config = Config { + cipher_suites: vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256], + flight_interval: Duration::from_millis(100), + ..Default::default() + }; + + let result = tokio::time::timeout( + Duration::from_millis(50), + create_test_server(Arc::new(cb), config, true), + ) + .await; + assert!(result.is_err(), "Expected Error but got Ok"); + + // Wait a little longer to ensure no additional messages have been sent by the server + //tokio::time::sleep(Duration::from_millis(300)).await; + + /*tokio::select! { + case msg := <-caReadChan: + t.Fatalf("Expected no additional messages from server, got: %+v", msg) + default: + }*/ + + Ok(()) +} + +#[tokio::test] +async fn test_protocol_version_validation() -> Result<()> { + /*env_logger::Builder::new() + .format(|buf, record| { + writeln!( + buf, + "{}:{} [{}] {} - {}", + record.file().unwrap_or("unknown"), + record.line().unwrap_or(0), + record.level(), + chrono::Local::now().format("%H:%M:%S.%6f"), + record.args() + ) + }) + .filter(None, LevelFilter::Trace) + .init();*/ + + let mut cookie = vec![0; 20]; + rand::rng().fill(cookie.as_mut_slice()); + + let random_bytes = [0u8; RANDOM_BYTES_LENGTH]; + let gmt_unix_time = SystemTime::UNIX_EPOCH + .checked_add(Duration::new(500, 0)) + .unwrap(); + let random = HandshakeRandom { + gmt_unix_time, + random_bytes, + }; + + let local_keypair = NamedCurve::X25519.generate_keypair()?; + + //|"Server"| + { + let server_cases = vec![ + ( + "ClientHelloVersion", + vec![RecordLayer::new( + PROTOCOL_VERSION1_2, + 0, + Content::Handshake(Handshake::new(HandshakeMessage::ClientHello( + HandshakeMessageClientHello { + version: ProtocolVersion { + major: 0xfe, + minor: 0xff, + }, // try to downgrade + cookie: cookie.clone(), + random: random.clone(), + cipher_suites: vec![ + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256, + ], + compression_methods: default_compression_methods(), + extensions: vec![], + }, + ))), + )], + ), + ( + "SecondsClientHelloVersion", + vec![ + RecordLayer::new( + PROTOCOL_VERSION1_2, + 0, + Content::Handshake(Handshake::new(HandshakeMessage::ClientHello( + HandshakeMessageClientHello { + version: PROTOCOL_VERSION1_2, + cookie: cookie.clone(), + random: random.clone(), + cipher_suites: vec![ + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256, + ], + compression_methods: default_compression_methods(), + extensions: vec![], + }, + ))), + ), + { + let mut handshake = Handshake::new(HandshakeMessage::ClientHello( + HandshakeMessageClientHello { + version: ProtocolVersion { + major: 0xfe, + minor: 0xff, + }, // try to downgrade + cookie: cookie.clone(), + random: random.clone(), + cipher_suites: vec![ + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256, + ], + compression_methods: default_compression_methods(), + extensions: vec![], + }, + )); + handshake.handshake_header.message_sequence = 1; + let mut record_layer = + RecordLayer::new(PROTOCOL_VERSION1_2, 0, Content::Handshake(handshake)); + record_layer.record_layer_header.sequence_number = 1; + + record_layer + }, + ], + ), + ]; + + use util::Conn; + for (name, records) in server_cases { + let (ca, cb) = pipe(); + + tokio::spawn(async move { + let config = Config { + cipher_suites: vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256], + flight_interval: Duration::from_millis(100), + ..Default::default() + }; + let timeout_result = tokio::time::timeout( + Duration::from_millis(1000), + create_test_server(Arc::new(cb), config, true), + ) + .await; + match timeout_result { + Ok(result) => { + if let Err(err) = result { + assert_eq!( + err.to_string(), + Error::ErrUnsupportedProtocolVersion.to_string(), + "{} Client error exp({}) failed({})", + name, + Error::ErrUnsupportedProtocolVersion, + err, + ); + } else { + panic!("{name} expected error, but got ok"); + } + } + Err(err) => { + panic!("server timeout {err}"); + } + }; + }); + + tokio::time::sleep(Duration::from_millis(50)).await; + + let mut resp = vec![0; 1024]; + let mut n = 0; + for record in records { + let mut packet = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(packet.as_mut()); + record.marshal(&mut writer)?; + } + + let _ = ca.send(&packet).await; + n = ca.recv(&mut resp).await?; + } + + let mut reader = BufReader::new(&resp[..n]); + let h = RecordLayerHeader::unmarshal(&mut reader)?; + assert_eq!( + h.content_type, + ContentType::Alert, + "Peer must return alert to unsupported protocol version" + ); + } + } + + //"Client" + { + let client_cases = vec![( + "ServerHelloVersion", + vec![ + RecordLayer::new( + PROTOCOL_VERSION1_2, + 0, + Content::Handshake(Handshake::new(HandshakeMessage::HelloVerifyRequest( + HandshakeMessageHelloVerifyRequest { + version: PROTOCOL_VERSION1_2, + cookie: cookie.clone(), + }, + ))), + ), + { + let mut handshake = Handshake::new(HandshakeMessage::ServerHello( + HandshakeMessageServerHello { + version: ProtocolVersion { + major: 0xfe, + minor: 0xff, + }, // try to downgrade + random: random.clone(), + cipher_suite: CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256, + compression_method: default_compression_methods().ids[0], + extensions: vec![], + }, + )); + handshake.handshake_header.message_sequence = 1; + let mut record = + RecordLayer::new(PROTOCOL_VERSION1_2, 0, Content::Handshake(handshake)); + record.record_layer_header.sequence_number = 1; + record + }, + { + let mut handshake = Handshake::new(HandshakeMessage::Certificate( + HandshakeMessageCertificate { + certificate: vec![], + }, + )); + handshake.handshake_header.message_sequence = 2; + let mut record = + RecordLayer::new(PROTOCOL_VERSION1_2, 0, Content::Handshake(handshake)); + record.record_layer_header.sequence_number = 2; + record + }, + { + let mut handshake = Handshake::new(HandshakeMessage::ServerKeyExchange( + HandshakeMessageServerKeyExchange { + identity_hint: vec![], + elliptic_curve_type: EllipticCurveType::NamedCurve, + named_curve: NamedCurve::X25519, + public_key: local_keypair.public_key.clone(), + algorithm: SignatureHashAlgorithm { + hash: HashAlgorithm::Sha256, + signature: SignatureAlgorithm::Ecdsa, + }, + signature: vec![0; 64], + }, + )); + handshake.handshake_header.message_sequence = 3; + let mut record = + RecordLayer::new(PROTOCOL_VERSION1_2, 0, Content::Handshake(handshake)); + record.record_layer_header.sequence_number = 3; + record + }, + { + let mut handshake = Handshake::new(HandshakeMessage::ServerHelloDone( + HandshakeMessageServerHelloDone {}, + )); + handshake.handshake_header.message_sequence = 4; + let mut record = + RecordLayer::new(PROTOCOL_VERSION1_2, 0, Content::Handshake(handshake)); + record.record_layer_header.sequence_number = 4; + record + }, + ], + )]; + + use util::Conn; + for (name, records) in client_cases { + let (ca, cb) = pipe(); + + tokio::spawn(async move { + let config = Config { + cipher_suites: vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256], + flight_interval: Duration::from_millis(100), + ..Default::default() + }; + let timeout_result = tokio::time::timeout( + Duration::from_millis(1000), + create_test_client(Arc::new(cb), config, true), + ) + .await; + match timeout_result { + Ok(result) => { + if let Err(err) = result { + assert_eq!( + err.to_string(), + Error::ErrUnsupportedProtocolVersion.to_string(), + "{} Server error exp({}) failed({})", + name, + Error::ErrUnsupportedProtocolVersion, + err, + ); + } else { + panic!("{name} expected error, but got ok"); + } + } + Err(err) => { + panic!("server timeout {err}"); + } + }; + }); + + tokio::time::sleep(Duration::from_millis(50)).await; + + let mut resp = vec![0; 1024]; + for record in records { + let _ = ca.recv(&mut resp).await?; + + let mut packet = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(packet.as_mut()); + record.marshal(&mut writer)?; + } + let _ = ca.send(&packet).await; + } + + let n = ca.recv(&mut resp).await?; + + let mut reader = BufReader::new(&resp[..n]); + let h = RecordLayerHeader::unmarshal(&mut reader)?; + + assert_eq!( + h.content_type, + ContentType::Alert, + "Peer must return alert to unsupported protocol version" + ); + } + } + + Ok(()) +} + +#[tokio::test] +async fn test_multiple_hello_verify_request() -> Result<()> { + /*env_logger::Builder::new() + .format(|buf, record| { + writeln!( + buf, + "{}:{} [{}] {} - {}", + record.file().unwrap_or("unknown"), + record.line().unwrap_or(0), + record.level(), + chrono::Local::now().format("%H:%M:%S.%6f"), + record.args() + ) + }) + .filter(None, LevelFilter::Trace) + .init();*/ + + let mut cookies = vec![ + // first clientHello contains an empty cookie + vec![], + ]; + + let mut packets = vec![]; + for i in 0..2 { + let mut cookie = vec![0; 20]; + rand::rng().fill(cookie.as_mut_slice()); + cookies.push(cookie.clone()); + + let mut handshake = Handshake::new(HandshakeMessage::HelloVerifyRequest( + HandshakeMessageHelloVerifyRequest { + version: PROTOCOL_VERSION1_2, + cookie, + }, + )); + handshake.handshake_header.message_sequence = i as u16; + + let mut record = RecordLayer::new(PROTOCOL_VERSION1_2, 0, Content::Handshake(handshake)); + record.record_layer_header.sequence_number = i as u64; + + let mut packet = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(packet.as_mut()); + record.marshal(&mut writer)?; + } + + packets.push(packet); + } + + let (ca, cb) = pipe(); + + tokio::spawn(async move { + let conf = Config::default(); + let _ = tokio::time::timeout( + Duration::from_millis(100), + create_test_client(Arc::new(ca), conf, true), + ) + .await; + }); + + for i in 0..cookies.len() { + let cookie = &cookies[i]; + trace!("cookie {i}: {cookie:?}"); + + // read client hello + let mut resp = vec![0; 1024]; + let n = cb.recv(&mut resp).await?; + let mut reader = BufReader::new(&resp[..n]); + let record = RecordLayer::unmarshal(&mut reader)?; + match record.content { + Content::Handshake(h) => match h.handshake_message { + HandshakeMessage::ClientHello(client_hello) => { + assert_eq!( + &client_hello.cookie, cookie, + "Wrong cookie {}, expected: {:?}, got: {:?}", + i, &client_hello.cookie, cookie + ); + } + _ => panic!("unexpected handshake message"), + }, + _ => panic!("unexpected content"), + }; + + if packets.len() <= i { + break; + } + // write hello verify request + cb.send(&packets[i]).await?; + } + + Ok(()) +} + +async fn send_client_hello( + cookie: Vec, + ca: &Arc, + sequence_number: u64, + send_renegotiation_info: bool, +) -> Result<()> { + let mut extensions = vec![]; + if send_renegotiation_info { + extensions.push(Extension::RenegotiationInfo(ExtensionRenegotiationInfo { + renegotiated_connection: 0, + })); + } + + let mut h = Handshake::new(HandshakeMessage::ClientHello(HandshakeMessageClientHello { + version: PROTOCOL_VERSION1_2, + random: HandshakeRandom::default(), + cookie, + + cipher_suites: vec![CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256], + compression_methods: default_compression_methods(), + extensions, + })); + h.handshake_header.message_sequence = sequence_number as u16; + + let mut record = RecordLayer::new(PROTOCOL_VERSION1_2, 0, Content::Handshake(h)); + record.record_layer_header.sequence_number = sequence_number; + + let mut packet = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(packet.as_mut()); + record.marshal(&mut writer)?; + } + + ca.send(&packet).await?; + + Ok(()) +} + +// Assert that a DTLS Server always responds with RenegotiationInfo if +// a ClientHello contained that extension or not +#[cfg(not(target_os = "windows"))] // this times out in CI on windows. +#[tokio::test] +async fn test_renegotiation_info() -> Result<()> { + let mut resp = vec![0u8; 1024]; + + let tests = vec![ + ("Include RenegotiationInfo", true), + ("No RenegotiationInfo", false), + ]; + + for (name, send_renegotiation_info) in tests { + let (ca, cb) = pipe(); + + tokio::spawn(async move { + let conf = Config::default(); + let _ = tokio::time::timeout( + Duration::from_millis(100), + create_test_server(Arc::new(cb), conf, true), + ) + .await; + }); + + tokio::time::sleep(Duration::from_millis(5)).await; + + let ca: Arc = Arc::new(ca); + send_client_hello(vec![], &ca, 0, send_renegotiation_info).await?; + + let n = ca.recv(&mut resp).await?; + let mut reader = BufReader::new(&resp[..n]); + let record = RecordLayer::unmarshal(&mut reader)?; + + let hello_verify_request = match record.content { + Content::Handshake(h) => match h.handshake_message { + HandshakeMessage::HelloVerifyRequest(hvr) => hvr, + _ => { + panic!("unexpected handshake message"); + } + }, + _ => { + panic!("unexpected content"); + } + }; + + send_client_hello( + hello_verify_request.cookie.clone(), + &ca, + 1, + send_renegotiation_info, + ) + .await?; + let n = ca.recv(&mut resp).await?; + let messages = unpack_datagram(&resp[..n])?; + + let mut reader = BufReader::new(&messages[0][..]); + let record = RecordLayer::unmarshal(&mut reader)?; + + let server_hello = match record.content { + Content::Handshake(h) => match h.handshake_message { + HandshakeMessage::ServerHello(sh) => sh, + _ => { + panic!("unexpected handshake message"); + } + }, + _ => { + panic!("unexpected content"); + } + }; + + let got_negotiation_info = server_hello + .extensions + .iter() + .any(|v| matches!(v, Extension::RenegotiationInfo(_))); + + assert!( + got_negotiation_info, + "{name}: Received ServerHello without RenegotiationInfo" + ); + + ca.close().await?; + } + + Ok(()) +} diff --git a/vendor/dtls/src/conn/mod.rs b/vendor/dtls/src/conn/mod.rs new file mode 100644 index 00000000..5a016c7d --- /dev/null +++ b/vendor/dtls/src/conn/mod.rs @@ -0,0 +1,1212 @@ +#[cfg(test)] +mod conn_test; + +use std::io::{BufReader, BufWriter}; +use std::marker::{Send, Sync}; +use std::net::SocketAddr; +use std::sync::atomic::Ordering; +use std::sync::Arc; + +use async_trait::async_trait; +use log::*; +use portable_atomic::{AtomicBool, AtomicU16}; +use tokio::sync::{mpsc, oneshot, Mutex}; +use tokio::time::Duration; +use util::replay_detector::*; +use util::Conn; + +use crate::alert::*; +use crate::application_data::*; +use crate::cipher_suite::*; +use crate::config::*; +use crate::content::*; +use crate::curve::named_curve::NamedCurve; +use crate::error::*; +use crate::extension::extension_use_srtp::*; +use crate::flight::flight0::*; +use crate::flight::flight1::*; +use crate::flight::flight5::*; +use crate::flight::flight6::*; +use crate::flight::*; +use crate::fragment_buffer::*; +use crate::handshake::handshake_cache::*; +use crate::handshake::handshake_header::HandshakeHeader; +use crate::handshake::*; +use crate::handshaker::*; +use crate::record_layer::record_layer_header::*; +use crate::record_layer::*; +use crate::signature_hash_algorithm::parse_signature_schemes; +use crate::state::*; + +pub(crate) const INITIAL_TICKER_INTERVAL: Duration = Duration::from_secs(1); +pub(crate) const COOKIE_LENGTH: usize = 20; +pub(crate) const DEFAULT_NAMED_CURVE: NamedCurve = NamedCurve::X25519; +pub(crate) const INBOUND_BUFFER_SIZE: usize = 8192; +// Default replay protection window is specified by RFC 6347 Section 4.1.2.6 +pub(crate) const DEFAULT_REPLAY_PROTECTION_WINDOW: usize = 64; + +pub static INVALID_KEYING_LABELS: &[&str] = &[ + "client finished", + "server finished", + "master secret", + "key expansion", +]; + +type PacketSendRequest = (Vec, Option>>); + +struct ConnReaderContext { + is_client: bool, + replay_protection_window: usize, + replay_detector: Vec>, + decrypted_tx: mpsc::Sender>>, + encrypted_packets: Vec>, + fragment_buffer: FragmentBuffer, + cache: HandshakeCache, + cipher_suite: Arc>>>, + remote_epoch: Arc, + // use additional oneshot sender to mimic rendezvous channel behavior + handshake_tx: mpsc::Sender<(oneshot::Sender<()>, mpsc::Sender<()>)>, + handshake_done_rx: mpsc::Receiver<()>, + packet_tx: Arc>, +} + +// Conn represents a DTLS connection +pub struct DTLSConn { + conn: Arc, + pub(crate) cache: HandshakeCache, // caching of handshake messages for verifyData generation + decrypted_rx: Mutex>>>, // Decrypted Application Data or error, pull by calling `Read` + pub(crate) state: State, // Internal state + + handshake_completed_successfully: Arc, + connection_closed_by_user: bool, + // closeLock sync.Mutex + closed: AtomicBool, // *closer.Closer + //handshakeLoopsFinished sync.WaitGroup + + //readDeadline :deadline.Deadline, + //writeDeadline :deadline.Deadline, + + //log logging.LeveledLogger + /* + reading chan struct{} + handshakeRecv chan chan struct{} + cancelHandshaker func() + cancelHandshakeReader func() + */ + pub(crate) current_flight: Box, + pub(crate) flights: Option>, + pub(crate) cfg: HandshakeConfig, + pub(crate) retransmit: bool, + // use additional oneshot sender to mimic rendezvous channel behavior + pub(crate) handshake_rx: mpsc::Receiver<(oneshot::Sender<()>, mpsc::Sender<()>)>, + + pub(crate) packet_tx: Arc>, + pub(crate) handle_queue_tx: mpsc::Sender>, + pub(crate) handshake_done_tx: Option>, + + reader_close_tx: Mutex>>, +} + +type UtilResult = std::result::Result; + +#[async_trait] +impl Conn for DTLSConn { + async fn connect(&self, _addr: SocketAddr) -> UtilResult<()> { + Err(util::Error::Other("Not applicable".to_owned())) + } + + async fn recv(&self, buf: &mut [u8]) -> UtilResult { + self.read(buf, None).await.map_err(util::Error::from_std) + } + + async fn recv_from(&self, buf: &mut [u8]) -> UtilResult<(usize, SocketAddr)> { + if let Some(raddr) = self.conn.remote_addr() { + let n = self.read(buf, None).await.map_err(util::Error::from_std)?; + Ok((n, raddr)) + } else { + Err(util::Error::Other( + "No remote address is provided by underlying Conn".to_owned(), + )) + } + } + + async fn send(&self, buf: &[u8]) -> UtilResult { + self.write(buf, None).await.map_err(util::Error::from_std) + } + + async fn send_to(&self, _buf: &[u8], _target: SocketAddr) -> UtilResult { + Err(util::Error::Other("Not applicable".to_owned())) + } + + fn local_addr(&self) -> UtilResult { + self.conn.local_addr() + } + + fn remote_addr(&self) -> Option { + self.conn.remote_addr() + } + + async fn close(&self) -> UtilResult<()> { + self.close().await.map_err(util::Error::from_std) + } + + fn as_any(&self) -> &(dyn std::any::Any + Send + Sync) { + self + } +} + +impl DTLSConn { + pub async fn new( + conn: Arc, + mut config: Config, + is_client: bool, + initial_state: Option, + ) -> Result { + validate_config(is_client, &config)?; + + let local_cipher_suites: Vec = parse_cipher_suites( + &config.cipher_suites, + config.psk.is_none(), + config.psk.is_some(), + )? + .iter() + .map(|cs| cs.id()) + .collect(); + + let sigs: Vec = config.signature_schemes.iter().map(|x| *x as u16).collect(); + let local_signature_schemes = parse_signature_schemes(&sigs, config.insecure_hashes)?; + + let retransmit_interval = if config.flight_interval != Duration::from_secs(0) { + config.flight_interval + } else { + INITIAL_TICKER_INTERVAL + }; + + /* + loggerFactory := config.LoggerFactory + if loggerFactory == nil { + loggerFactory = logging.NewDefaultLoggerFactory() + } + + logger := loggerFactory.NewLogger("dtls") + */ + let maximum_transmission_unit = if config.mtu == 0 { + DEFAULT_MTU + } else { + config.mtu + }; + + let replay_protection_window = if config.replay_protection_window == 0 { + DEFAULT_REPLAY_PROTECTION_WINDOW + } else { + config.replay_protection_window + }; + + let mut server_name = config.server_name.clone(); + + // Use host from conn address when server_name is not provided + if is_client && server_name.is_empty() { + if let Some(remote_addr) = conn.remote_addr() { + server_name = remote_addr.ip().to_string(); + } else { + warn!("conn.remote_addr is empty, please set explicitly server_name in Config! Use default \"localhost\" as server_name now"); + "localhost".clone_into(&mut server_name); + } + } + + let cfg = HandshakeConfig { + local_psk_callback: config.psk.take(), + local_psk_identity_hint: config.psk_identity_hint.take(), + local_cipher_suites, + local_signature_schemes, + extended_master_secret: config.extended_master_secret, + local_srtp_protection_profiles: config.srtp_protection_profiles.clone(), + server_name, + client_auth: config.client_auth, + local_certificates: config.certificates.clone(), + insecure_skip_verify: config.insecure_skip_verify, + insecure_verification: config.insecure_verification, + verify_peer_certificate: config.verify_peer_certificate.take(), + client_cert_verifier: if config.client_auth as u8 + >= ClientAuthType::VerifyClientCertIfGiven as u8 + { + Some(Arc::new( + crate::certificate_verifier::CertificateVerifier::new(config.client_cas), + )) + } else { + None + }, + server_cert_verifier: Arc::new(crate::certificate_verifier::CertificateVerifier::new( + config.roots_cas, + )), + retransmit_interval, + //log: logger, + initial_epoch: 0, + ..Default::default() + }; + + let (state, flight, initial_fsm_state) = if let Some(state) = initial_state { + let flight = if is_client { + Box::new(Flight5 {}) as Box + } else { + Box::new(Flight6 {}) as Box + }; + + (state, flight, HandshakeState::Finished) + } else { + let flight = if is_client { + Box::new(Flight1 {}) as Box + } else { + Box::new(Flight0 {}) as Box + }; + + ( + State { + is_client, + ..Default::default() + }, + flight, + HandshakeState::Preparing, + ) + }; + + let (decrypted_tx, decrypted_rx) = mpsc::channel(1); + let (handshake_tx, handshake_rx) = mpsc::channel(1); + let (handshake_done_tx, handshake_done_rx) = mpsc::channel(1); + let (packet_tx, mut packet_rx) = mpsc::channel(1); + let (handle_queue_tx, mut handle_queue_rx) = mpsc::channel(1); + let (reader_close_tx, mut reader_close_rx) = mpsc::channel(1); + + let packet_tx = Arc::new(packet_tx); + let packet_tx2 = Arc::clone(&packet_tx); + let next_conn_rx = Arc::clone(&conn); + let next_conn_tx = Arc::clone(&conn); + let cache = HandshakeCache::new(); + let mut cache1 = cache.clone(); + let cache2 = cache.clone(); + let handshake_completed_successfully = Arc::new(AtomicBool::new(false)); + let handshake_completed_successfully2 = Arc::clone(&handshake_completed_successfully); + + let mut c = DTLSConn { + conn: Arc::clone(&conn), + cache, + decrypted_rx: Mutex::new(decrypted_rx), + state, + handshake_completed_successfully, + connection_closed_by_user: false, + closed: AtomicBool::new(false), + + current_flight: flight, + flights: None, + cfg, + retransmit: false, + handshake_rx, + packet_tx, + handle_queue_tx, + handshake_done_tx: Some(handshake_done_tx), + reader_close_tx: Mutex::new(Some(reader_close_tx)), + }; + + let cipher_suite1 = Arc::clone(&c.state.cipher_suite); + let sequence_number = Arc::clone(&c.state.local_sequence_number); + + tokio::spawn(async move { + loop { + let rx = packet_rx.recv().await; + if let Some(r) = rx { + let (pkt, result_tx) = r; + + let result = DTLSConn::handle_outgoing_packets( + &next_conn_tx, + pkt, + &mut cache1, + is_client, + &sequence_number, + &cipher_suite1, + maximum_transmission_unit, + ) + .await; + + if let Some(tx) = result_tx { + let _ = tx.send(result).await; + } + } else { + trace!("{}: handle_outgoing_packets exit", srv_cli_str(is_client)); + break; + } + } + }); + + let local_epoch = Arc::clone(&c.state.local_epoch); + let remote_epoch = Arc::clone(&c.state.remote_epoch); + let cipher_suite2 = Arc::clone(&c.state.cipher_suite); + + tokio::spawn(async move { + let mut buf = vec![0u8; INBOUND_BUFFER_SIZE]; + let mut ctx = ConnReaderContext { + is_client, + replay_protection_window, + replay_detector: vec![], + decrypted_tx, + encrypted_packets: vec![], + fragment_buffer: FragmentBuffer::new(), + cache: cache2, + cipher_suite: cipher_suite2, + remote_epoch, + handshake_tx, + handshake_done_rx, + packet_tx: packet_tx2, + }; + + //trace!("before enter read_and_buffer: {}] ", srv_cli_str(is_client)); + loop { + tokio::select! { + _ = reader_close_rx.recv() => { + trace!( + "{}: read_and_buffer exit", + srv_cli_str(ctx.is_client), + ); + break; + } + result = DTLSConn::read_and_buffer( + &mut ctx, + &next_conn_rx, + &mut handle_queue_rx, + &mut buf, + &local_epoch, + &handshake_completed_successfully2, + ) => { + if let Err(err) = result { + trace!( + "{}: read_and_buffer return err: {}", + srv_cli_str(is_client), + err + ); + if Error::ErrAlertFatalOrClose == err { + trace!( + "{}: read_and_buffer exit with {}", + srv_cli_str(ctx.is_client), + err + ); + + break; + } + } + } + } + } + }); + + // Do handshake + c.handshake(initial_fsm_state).await?; + + trace!("Handshake Completed"); + + Ok(c) + } + + // Read reads data from the connection. + pub async fn read(&self, p: &mut [u8], duration: Option) -> Result { + if !self.is_handshake_completed_successfully() { + return Err(Error::ErrHandshakeInProgress); + } + + let rx = { + let mut decrypted_rx = self.decrypted_rx.lock().await; + if let Some(d) = duration { + let timer = tokio::time::sleep(d); + tokio::pin!(timer); + + tokio::select! { + r = decrypted_rx.recv() => r, + _ = timer.as_mut() => return Err(Error::ErrDeadlineExceeded), + } + } else { + decrypted_rx.recv().await + } + }; + + if let Some(out) = rx { + match out { + Ok(val) => { + let n = val.len(); + if p.len() < n { + return Err(Error::ErrBufferTooSmall); + } + p[..n].copy_from_slice(&val); + Ok(n) + } + Err(err) => Err(err), + } + } else { + Err(Error::ErrAlertFatalOrClose) + } + } + + // Write writes len(p) bytes from p to the DTLS connection + pub async fn write(&self, p: &[u8], duration: Option) -> Result { + if self.is_connection_closed() { + return Err(Error::ErrConnClosed); + } + + if !self.is_handshake_completed_successfully() { + return Err(Error::ErrHandshakeInProgress); + } + + let pkts = vec![Packet { + record: RecordLayer::new( + PROTOCOL_VERSION1_2, + self.get_local_epoch(), + Content::ApplicationData(ApplicationData { data: p.to_vec() }), + ), + should_encrypt: true, + reset_local_sequence_number: false, + }]; + + if let Some(d) = duration { + let timer = tokio::time::sleep(d); + tokio::pin!(timer); + + tokio::select! { + result = self.write_packets(pkts) => { + result?; + } + _ = timer.as_mut() => return Err(Error::ErrDeadlineExceeded), + } + } else { + self.write_packets(pkts).await?; + } + + Ok(p.len()) + } + + // Close closes the connection. + pub async fn close(&self) -> Result<()> { + if self + .closed + .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) + .is_ok() + { + // Discard error from notify() to return non-error on the first user call of Close() + // even if the underlying connection is already closed. + self.notify(AlertLevel::Warning, AlertDescription::CloseNotify) + .await?; + + { + let mut reader_close_tx = self.reader_close_tx.lock().await; + reader_close_tx.take(); + } + self.conn.close().await?; + } + + Ok(()) + } + + /// connection_state returns basic DTLS details about the connection. + /// Note that this replaced the `Export` function of v1. + pub async fn connection_state(&self) -> State { + self.state.clone().await + } + + /// selected_srtpprotection_profile returns the selected SRTPProtectionProfile + pub fn selected_srtpprotection_profile(&self) -> SrtpProtectionProfile { + self.state.srtp_protection_profile + } + + pub(crate) async fn notify(&self, level: AlertLevel, desc: AlertDescription) -> Result<()> { + self.write_packets(vec![Packet { + record: RecordLayer::new( + PROTOCOL_VERSION1_2, + self.get_local_epoch(), + Content::Alert(Alert { + alert_level: level, + alert_description: desc, + }), + ), + should_encrypt: self.is_handshake_completed_successfully(), + reset_local_sequence_number: false, + }]) + .await + } + + pub(crate) async fn write_packets(&self, pkts: Vec) -> Result<()> { + let (tx, mut rx) = mpsc::channel(1); + + self.packet_tx.send((pkts, Some(tx))).await?; + + if let Some(result) = rx.recv().await { + result + } else { + Ok(()) + } + } + + async fn handle_outgoing_packets( + next_conn: &Arc, + mut pkts: Vec, + cache: &mut HandshakeCache, + is_client: bool, + local_sequence_number: &Arc>>, + cipher_suite: &Arc>>>, + maximum_transmission_unit: usize, + ) -> Result<()> { + let mut raw_packets = vec![]; + for p in &mut pkts { + if let Content::Handshake(h) = &p.record.content { + let mut handshake_raw = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(handshake_raw.as_mut()); + p.record.marshal(&mut writer)?; + } + trace!( + "Send [handshake:{}] -> {} (epoch: {}, seq: {})", + srv_cli_str(is_client), + h.handshake_header.handshake_type, + p.record.record_layer_header.epoch, + h.handshake_header.message_sequence + ); + cache + .push( + handshake_raw[RECORD_LAYER_HEADER_SIZE..].to_vec(), + p.record.record_layer_header.epoch, + h.handshake_header.message_sequence, + h.handshake_header.handshake_type, + is_client, + ) + .await; + + let raw_handshake_packets = DTLSConn::process_handshake_packet( + local_sequence_number, + cipher_suite, + maximum_transmission_unit, + p, + h, + ) + .await?; + raw_packets.extend_from_slice(&raw_handshake_packets); + } else { + /*if let Content::Alert(a) = &p.record.content { + if a.alert_description == AlertDescription::CloseNotify { + closed = true; + } + }*/ + + let raw_packet = + DTLSConn::process_packet(local_sequence_number, cipher_suite, p).await?; + raw_packets.push(raw_packet); + } + } + + if !raw_packets.is_empty() { + let compacted_raw_packets = + compact_raw_packets(&raw_packets, maximum_transmission_unit); + + for compacted_raw_packets in &compacted_raw_packets { + next_conn.send(compacted_raw_packets).await?; + } + } + + Ok(()) + } + + async fn process_packet( + local_sequence_number: &Arc>>, + cipher_suite: &Arc>>>, + p: &mut Packet, + ) -> Result> { + let epoch = p.record.record_layer_header.epoch as usize; + let seq = { + let mut lsn = local_sequence_number.lock().await; + while lsn.len() <= epoch { + lsn.push(0); + } + + lsn[epoch] += 1; + lsn[epoch] - 1 + }; + //trace!("{}: seq = {}", srv_cli_str(is_client), seq); + + if seq > MAX_SEQUENCE_NUMBER { + // RFC 6347 Section 4.1.0 + // The implementation must either abandon an association or rehandshake + // prior to allowing the sequence number to wrap. + return Err(Error::ErrSequenceNumberOverflow); + } + p.record.record_layer_header.sequence_number = seq; + + let mut raw_packet = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(raw_packet.as_mut()); + p.record.marshal(&mut writer)?; + } + + if p.should_encrypt { + let cipher_suite = cipher_suite.lock().await; + if let Some(cipher_suite) = &*cipher_suite { + raw_packet = cipher_suite.encrypt(&p.record.record_layer_header, &raw_packet)?; + } + } + + Ok(raw_packet) + } + + async fn process_handshake_packet( + local_sequence_number: &Arc>>, + cipher_suite: &Arc>>>, + maximum_transmission_unit: usize, + p: &Packet, + h: &Handshake, + ) -> Result>> { + let mut raw_packets = vec![]; + + let handshake_fragments = DTLSConn::fragment_handshake(maximum_transmission_unit, h)?; + + let epoch = p.record.record_layer_header.epoch as usize; + + let mut lsn = local_sequence_number.lock().await; + while lsn.len() <= epoch { + lsn.push(0); + } + + for handshake_fragment in &handshake_fragments { + let seq = { + lsn[epoch] += 1; + lsn[epoch] - 1 + }; + //trace!("seq = {}", seq); + if seq > MAX_SEQUENCE_NUMBER { + return Err(Error::ErrSequenceNumberOverflow); + } + + let record_layer_header = RecordLayerHeader { + protocol_version: p.record.record_layer_header.protocol_version, + content_type: p.record.record_layer_header.content_type, + content_len: handshake_fragment.len() as u16, + epoch: p.record.record_layer_header.epoch, + sequence_number: seq, + }; + + let mut record_layer_header_bytes = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(record_layer_header_bytes.as_mut()); + record_layer_header.marshal(&mut writer)?; + } + + //p.record.record_layer_header = record_layer_header; + + let mut raw_packet = vec![]; + raw_packet.extend_from_slice(&record_layer_header_bytes); + raw_packet.extend_from_slice(handshake_fragment); + if p.should_encrypt { + let cipher_suite = cipher_suite.lock().await; + if let Some(cipher_suite) = &*cipher_suite { + raw_packet = cipher_suite.encrypt(&record_layer_header, &raw_packet)?; + } + } + + raw_packets.push(raw_packet); + } + + Ok(raw_packets) + } + + fn fragment_handshake(maximum_transmission_unit: usize, h: &Handshake) -> Result>> { + let mut content = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(content.as_mut()); + h.handshake_message.marshal(&mut writer)?; + } + + let mut fragmented_handshakes = vec![]; + + let mut content_fragments = split_bytes(&content, maximum_transmission_unit); + if content_fragments.is_empty() { + content_fragments = vec![vec![]]; + } + + let mut offset = 0; + for content_fragment in &content_fragments { + let content_fragment_len = content_fragment.len(); + + let handshake_header_fragment = HandshakeHeader { + handshake_type: h.handshake_header.handshake_type, + length: h.handshake_header.length, + message_sequence: h.handshake_header.message_sequence, + fragment_offset: offset as u32, + fragment_length: content_fragment_len as u32, + }; + + offset += content_fragment_len; + + let mut handshake_header_fragment_raw = vec![]; + { + let mut writer = + BufWriter::<&mut Vec>::new(handshake_header_fragment_raw.as_mut()); + handshake_header_fragment.marshal(&mut writer)?; + } + + let mut fragmented_handshake = vec![]; + fragmented_handshake.extend_from_slice(&handshake_header_fragment_raw); + fragmented_handshake.extend_from_slice(content_fragment); + + fragmented_handshakes.push(fragmented_handshake); + } + + Ok(fragmented_handshakes) + } + + pub(crate) fn set_handshake_completed_successfully(&mut self) { + self.handshake_completed_successfully + .store(true, Ordering::SeqCst); + } + + pub(crate) fn is_handshake_completed_successfully(&self) -> bool { + self.handshake_completed_successfully.load(Ordering::SeqCst) + } + + async fn read_and_buffer( + ctx: &mut ConnReaderContext, + next_conn: &Arc, + handle_queue_rx: &mut mpsc::Receiver>, + buf: &mut [u8], + local_epoch: &Arc, + handshake_completed_successfully: &Arc, + ) -> Result<()> { + let n = next_conn.recv(buf).await?; + let pkts = unpack_datagram(&buf[..n])?; + let mut has_handshake = false; + for pkt in pkts { + let (hs, alert, mut err) = DTLSConn::handle_incoming_packet(ctx, pkt, true).await; + if let Some(alert) = alert { + let alert_err = ctx + .packet_tx + .send(( + vec![Packet { + record: RecordLayer::new( + PROTOCOL_VERSION1_2, + local_epoch.load(Ordering::SeqCst), + Content::Alert(Alert { + alert_level: alert.alert_level, + alert_description: alert.alert_description, + }), + ), + should_encrypt: handshake_completed_successfully.load(Ordering::SeqCst), + reset_local_sequence_number: false, + }], + None, + )) + .await; + + if let Err(alert_err) = alert_err { + if err.is_none() { + err = Some(Error::Other(alert_err.to_string())); + } + } + + if alert.alert_level == AlertLevel::Fatal + || alert.alert_description == AlertDescription::CloseNotify + { + return Err(Error::ErrAlertFatalOrClose); + } + } + + if let Some(err) = err { + return Err(err); + } + + if hs { + has_handshake = true + } + } + + if has_handshake { + let (done_tx, mut done_rx) = mpsc::channel(1); + let rendezvous_at_handshake = async { + let (rendezvous_tx, rendezvous_rx) = oneshot::channel(); + _ = ctx.handshake_tx.send((rendezvous_tx, done_tx)).await; + rendezvous_rx.await + }; + tokio::select! { + _ = rendezvous_at_handshake => { + let mut wait_done_rx = true; + while wait_done_rx{ + tokio::select!{ + _ = done_rx.recv() => { + // If the other party may retransmit the flight, + // we should respond even if it not a new message. + wait_done_rx = false; + } + done = handle_queue_rx.recv() => { + //trace!("recv handle_queue: {} ", srv_cli_str(ctx.is_client)); + + let pkts = ctx.encrypted_packets.drain(..).collect(); + DTLSConn::handle_queued_packets(ctx, local_epoch, handshake_completed_successfully, pkts).await?; + + drop(done); + } + } + } + } + _ = ctx.handshake_done_rx.recv() => {} + } + } + + Ok(()) + } + + async fn handle_queued_packets( + ctx: &mut ConnReaderContext, + local_epoch: &Arc, + handshake_completed_successfully: &Arc, + pkts: Vec>, + ) -> Result<()> { + for p in pkts { + let (_, alert, mut err) = DTLSConn::handle_incoming_packet(ctx, p, false).await; // don't re-enqueue + if let Some(alert) = alert { + let alert_err = ctx + .packet_tx + .send(( + vec![Packet { + record: RecordLayer::new( + PROTOCOL_VERSION1_2, + local_epoch.load(Ordering::SeqCst), + Content::Alert(Alert { + alert_level: alert.alert_level, + alert_description: alert.alert_description, + }), + ), + should_encrypt: handshake_completed_successfully.load(Ordering::SeqCst), + reset_local_sequence_number: false, + }], + None, + )) + .await; + + if let Err(alert_err) = alert_err { + if err.is_none() { + err = Some(Error::Other(alert_err.to_string())); + } + } + if alert.alert_level == AlertLevel::Fatal + || alert.alert_description == AlertDescription::CloseNotify + { + return Err(Error::ErrAlertFatalOrClose); + } + } + + if let Some(err) = err { + return Err(err); + } + } + + Ok(()) + } + + async fn handle_incoming_packet( + ctx: &mut ConnReaderContext, + mut pkt: Vec, + enqueue: bool, + ) -> (bool, Option, Option) { + let mut reader = BufReader::new(pkt.as_slice()); + let h = match RecordLayerHeader::unmarshal(&mut reader) { + Ok(h) => h, + Err(err) => { + // Decode error must be silently discarded + // [RFC6347 Section-4.1.2.7] + debug!( + "{}: discarded broken packet: {}", + srv_cli_str(ctx.is_client), + err + ); + return (false, None, None); + } + }; + + // Validate epoch + let epoch = ctx.remote_epoch.load(Ordering::SeqCst); + if h.epoch > epoch { + if h.epoch > epoch + 1 { + debug!( + "{}: discarded future packet (epoch: {}, seq: {})", + srv_cli_str(ctx.is_client), + h.epoch, + h.sequence_number, + ); + return (false, None, None); + } + if enqueue { + debug!( + "{}: received packet of next epoch, queuing packet", + srv_cli_str(ctx.is_client) + ); + ctx.encrypted_packets.push(pkt); + } + return (false, None, None); + } + + // Anti-replay protection + while ctx.replay_detector.len() <= h.epoch as usize { + ctx.replay_detector + .push(Box::new(SlidingWindowDetector::new( + ctx.replay_protection_window, + MAX_SEQUENCE_NUMBER, + ))); + } + + let ok = ctx.replay_detector[h.epoch as usize].check(h.sequence_number); + if !ok { + debug!( + "{}: discarded duplicated packet (epoch: {}, seq: {})", + srv_cli_str(ctx.is_client), + h.epoch, + h.sequence_number, + ); + return (false, None, None); + } + + // Decrypt + if h.epoch != 0 { + let invalid_cipher_suite = { + let cipher_suite = ctx.cipher_suite.lock().await; + if cipher_suite.is_none() { + true + } else if let Some(cipher_suite) = &*cipher_suite { + !cipher_suite.is_initialized() + } else { + false + } + }; + if invalid_cipher_suite { + if enqueue { + debug!( + "{}: handshake not finished, queuing packet", + srv_cli_str(ctx.is_client) + ); + ctx.encrypted_packets.push(pkt); + } + return (false, None, None); + } + + let cipher_suite = ctx.cipher_suite.lock().await; + if let Some(cipher_suite) = &*cipher_suite { + pkt = match cipher_suite.decrypt(&pkt) { + Ok(pkt) => pkt, + Err(err) => { + debug!("{}: decrypt failed: {}", srv_cli_str(ctx.is_client), err); + + // If we get an error for PSK we need to return an error. + if cipher_suite.is_psk() { + return ( + false, + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::UnknownPskIdentity, + }), + None, + ); + } else { + return (false, None, None); + } + } + }; + } + } + + let is_handshake = match ctx.fragment_buffer.push(&pkt) { + Ok(is_handshake) => is_handshake, + Err(err) => { + // Decode error must be silently discarded + // [RFC6347 Section-4.1.2.7] + debug!("{}: defragment failed: {}", srv_cli_str(ctx.is_client), err); + return (false, None, None); + } + }; + if is_handshake { + ctx.replay_detector[h.epoch as usize].accept(); + while let Ok((out, epoch)) = ctx.fragment_buffer.pop() { + //log::debug!("Extension Debug: out.len()={}", out.len()); + let mut reader = BufReader::new(out.as_slice()); + let raw_handshake = match Handshake::unmarshal(&mut reader) { + Ok(rh) => { + trace!( + "Recv [handshake:{}] -> {} (epoch: {}, seq: {})", + srv_cli_str(ctx.is_client), + rh.handshake_header.handshake_type, + h.epoch, + rh.handshake_header.message_sequence + ); + rh + } + Err(err) => { + debug!( + "{}: handshake parse failed: {}", + srv_cli_str(ctx.is_client), + err + ); + continue; + } + }; + + ctx.cache + .push( + out, + epoch, + raw_handshake.handshake_header.message_sequence, + raw_handshake.handshake_header.handshake_type, + !ctx.is_client, + ) + .await; + } + + return (true, None, None); + } + + let mut reader = BufReader::new(pkt.as_slice()); + let r = match RecordLayer::unmarshal(&mut reader) { + Ok(r) => r, + Err(err) => { + return ( + false, + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::DecodeError, + }), + Some(err), + ); + } + }; + + match r.content { + Content::Alert(mut a) => { + trace!("{}: <- {}", srv_cli_str(ctx.is_client), a); + if a.alert_description == AlertDescription::CloseNotify { + // Respond with a close_notify [RFC5246 Section 7.2.1] + a = Alert { + alert_level: AlertLevel::Warning, + alert_description: AlertDescription::CloseNotify, + }; + } + ctx.replay_detector[h.epoch as usize].accept(); + return ( + false, + Some(a), + Some(Error::Other(format!("Error of Alert {a}"))), + ); + } + Content::ChangeCipherSpec(_) => { + let invalid_cipher_suite = { + let cipher_suite = ctx.cipher_suite.lock().await; + if cipher_suite.is_none() { + true + } else if let Some(cipher_suite) = &*cipher_suite { + !cipher_suite.is_initialized() + } else { + false + } + }; + + if invalid_cipher_suite { + if enqueue { + debug!( + "{}: CipherSuite not initialized, queuing packet", + srv_cli_str(ctx.is_client) + ); + ctx.encrypted_packets.push(pkt); + } + return (false, None, None); + } + + let new_remote_epoch = h.epoch + 1; + trace!( + "{}: <- ChangeCipherSpec (epoch: {})", + srv_cli_str(ctx.is_client), + new_remote_epoch + ); + + if epoch + 1 == new_remote_epoch { + ctx.remote_epoch.store(new_remote_epoch, Ordering::SeqCst); + ctx.replay_detector[h.epoch as usize].accept(); + } + } + Content::ApplicationData(a) => { + if h.epoch == 0 { + return ( + false, + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::UnexpectedMessage, + }), + Some(Error::ErrApplicationDataEpochZero), + ); + } + + ctx.replay_detector[h.epoch as usize].accept(); + + let _ = ctx.decrypted_tx.send(Ok(a.data)).await; + //TODO + /*select { + case self.decrypted < - content.data: + case < -c.closed.Done(): + }*/ + } + _ => { + return ( + false, + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::UnexpectedMessage, + }), + Some(Error::ErrUnhandledContextType), + ); + } + }; + + (false, None, None) + } + + fn is_connection_closed(&self) -> bool { + self.closed.load(Ordering::SeqCst) + } + + pub(crate) fn set_local_epoch(&mut self, epoch: u16) { + self.state.local_epoch.store(epoch, Ordering::SeqCst); + } + + pub(crate) fn get_local_epoch(&self) -> u16 { + self.state.local_epoch.load(Ordering::SeqCst) + } +} + +fn compact_raw_packets(raw_packets: &[Vec], maximum_transmission_unit: usize) -> Vec> { + let mut combined_raw_packets = vec![]; + let mut current_combined_raw_packet = vec![]; + + for raw_packet in raw_packets { + if !current_combined_raw_packet.is_empty() + && current_combined_raw_packet.len() + raw_packet.len() >= maximum_transmission_unit + { + combined_raw_packets.push(current_combined_raw_packet); + current_combined_raw_packet = vec![]; + } + current_combined_raw_packet.extend_from_slice(raw_packet); + } + + combined_raw_packets.push(current_combined_raw_packet); + + combined_raw_packets +} + +fn split_bytes(bytes: &[u8], split_len: usize) -> Vec> { + let mut splits = vec![]; + let num_bytes = bytes.len(); + for i in (0..num_bytes).step_by(split_len) { + let mut j = i + split_len; + if j > num_bytes { + j = num_bytes; + } + + splits.push(bytes[i..j].to_vec()); + } + + splits +} diff --git a/vendor/dtls/src/content.rs b/vendor/dtls/src/content.rs new file mode 100644 index 00000000..c1fa2ebf --- /dev/null +++ b/vendor/dtls/src/content.rs @@ -0,0 +1,85 @@ +use std::io::{Read, Write}; + +use super::alert::*; +use super::application_data::*; +use super::change_cipher_spec::*; +use super::handshake::*; +use crate::error::*; + +/// ## Specifications +/// +/// * [RFC 4346 §6.2.1] +/// +/// [RFC 4346 §6.2.1]: https://tools.ietf.org/html/rfc4346#section-6.2.1 +#[derive(Default, Copy, Clone, PartialEq, Eq, Debug)] +pub enum ContentType { + ChangeCipherSpec = 20, + Alert = 21, + Handshake = 22, + ApplicationData = 23, + #[default] + Invalid, +} + +impl From for ContentType { + fn from(val: u8) -> Self { + match val { + 20 => ContentType::ChangeCipherSpec, + 21 => ContentType::Alert, + 22 => ContentType::Handshake, + 23 => ContentType::ApplicationData, + _ => ContentType::Invalid, + } + } +} + +#[derive(PartialEq, Debug, Clone)] +pub enum Content { + ChangeCipherSpec(ChangeCipherSpec), + Alert(Alert), + Handshake(Handshake), + ApplicationData(ApplicationData), +} + +impl Content { + pub fn content_type(&self) -> ContentType { + match self { + Content::ChangeCipherSpec(c) => c.content_type(), + Content::Alert(c) => c.content_type(), + Content::Handshake(c) => c.content_type(), + Content::ApplicationData(c) => c.content_type(), + } + } + + pub fn size(&self) -> usize { + match self { + Content::ChangeCipherSpec(c) => c.size(), + Content::Alert(c) => c.size(), + Content::Handshake(c) => c.size(), + Content::ApplicationData(c) => c.size(), + } + } + + pub fn marshal(&self, writer: &mut W) -> Result<()> { + match self { + Content::ChangeCipherSpec(c) => c.marshal(writer), + Content::Alert(c) => c.marshal(writer), + Content::Handshake(c) => c.marshal(writer), + Content::ApplicationData(c) => c.marshal(writer), + } + } + + pub fn unmarshal(content_type: ContentType, reader: &mut R) -> Result { + match content_type { + ContentType::ChangeCipherSpec => Ok(Content::ChangeCipherSpec( + ChangeCipherSpec::unmarshal(reader)?, + )), + ContentType::Alert => Ok(Content::Alert(Alert::unmarshal(reader)?)), + ContentType::Handshake => Ok(Content::Handshake(Handshake::unmarshal(reader)?)), + ContentType::ApplicationData => Ok(Content::ApplicationData( + ApplicationData::unmarshal(reader)?, + )), + _ => Err(Error::ErrInvalidContentType), + } + } +} diff --git a/vendor/dtls/src/crypto/crypto_cbc.rs b/vendor/dtls/src/crypto/crypto_cbc.rs new file mode 100644 index 00000000..4c71a6c2 --- /dev/null +++ b/vendor/dtls/src/crypto/crypto_cbc.rs @@ -0,0 +1,136 @@ +// AES-CBC (Cipher Block Chaining) +// First historic block cipher for AES. +// CBC mode is insecure and must not be used. It’s been progressively deprecated and +// removed from SSL libraries. +// Introduced with TLS 1.0 year 2002. Superseded by GCM in TLS 1.2 year 2008. +// Removed in TLS 1.3 year 2018. +// RFC 3268 year 2002 https://tools.ietf.org/html/rfc3268 + +// https://github.com/RustCrypto/block-ciphers + +use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit}; +use p256::elliptic_curve::subtle::ConstantTimeEq; +use rand::Rng; +use std::io::Cursor; +use std::ops::Not; + +use super::padding::DtlsPadding; +use crate::content::*; +use crate::error::*; +use crate::prf::*; +use crate::record_layer::record_layer_header::*; +type Aes256CbcEnc = cbc::Encryptor; +type Aes256CbcDec = cbc::Decryptor; + +// State needed to handle encrypted input/output +#[derive(Clone)] +pub struct CryptoCbc { + local_key: Vec, + remote_key: Vec, + write_mac: Vec, + read_mac: Vec, +} + +impl CryptoCbc { + const BLOCK_SIZE: usize = 16; + const MAC_SIZE: usize = 20; + + pub fn new( + local_key: &[u8], + local_mac: &[u8], + remote_key: &[u8], + remote_mac: &[u8], + ) -> Result { + Ok(CryptoCbc { + local_key: local_key.to_vec(), + write_mac: local_mac.to_vec(), + + remote_key: remote_key.to_vec(), + read_mac: remote_mac.to_vec(), + }) + } + + pub fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { + let mut payload = raw[RECORD_LAYER_HEADER_SIZE..].to_vec(); + let raw = &raw[..RECORD_LAYER_HEADER_SIZE]; + + // Generate + Append MAC + let h = pkt_rlh; + + let mac = prf_mac( + h.epoch, + h.sequence_number, + h.content_type, + h.protocol_version, + &payload, + &self.write_mac, + )?; + payload.extend_from_slice(&mac); + + let mut iv: Vec = vec![0; Self::BLOCK_SIZE]; + rand::rng().fill(iv.as_mut_slice()); + + let write_cbc = Aes256CbcEnc::new_from_slices(&self.local_key, &iv)?; + let encrypted = write_cbc.encrypt_padded_vec_mut::(&payload); + + // Prepend unencrypte header with encrypted payload + let mut r = vec![]; + r.extend_from_slice(raw); + r.extend_from_slice(&iv); + r.extend_from_slice(&encrypted); + + let r_len = (r.len() - RECORD_LAYER_HEADER_SIZE) as u16; + r[RECORD_LAYER_HEADER_SIZE - 2..RECORD_LAYER_HEADER_SIZE] + .copy_from_slice(&r_len.to_be_bytes()); + + Ok(r) + } + + pub fn decrypt(&self, r: &[u8]) -> Result> { + let mut reader = Cursor::new(r); + let h = RecordLayerHeader::unmarshal(&mut reader)?; + if h.content_type == ContentType::ChangeCipherSpec { + // Nothing to encrypt with ChangeCipherSpec + return Ok(r.to_vec()); + } + + if r.len() < RECORD_LAYER_HEADER_SIZE + Self::BLOCK_SIZE { + return Err(Error::ErrInvalidPacketLength); + } + + let body = &r[RECORD_LAYER_HEADER_SIZE..]; + let iv = &body[0..Self::BLOCK_SIZE]; + let body = &body[Self::BLOCK_SIZE..]; + + if body.is_empty() || !body.len().is_multiple_of(Self::BLOCK_SIZE) { + return Err(Error::ErrInvalidPacketLength); + } + + let read_cbc = Aes256CbcDec::new_from_slices(&self.remote_key, iv)?; + + let decrypted = read_cbc + .decrypt_padded_vec_mut::(body) + .map_err(|_| Error::ErrInvalidPacketLength)?; + + let recv_mac = &decrypted[decrypted.len() - Self::MAC_SIZE..]; + let decrypted = &decrypted[0..decrypted.len() - Self::MAC_SIZE]; + let mac = prf_mac( + h.epoch, + h.sequence_number, + h.content_type, + h.protocol_version, + decrypted, + &self.read_mac, + )?; + + if recv_mac.ct_eq(&mac).not().into() { + return Err(Error::ErrInvalidMac); + } + + let mut d = Vec::with_capacity(RECORD_LAYER_HEADER_SIZE + decrypted.len()); + d.extend_from_slice(&r[..RECORD_LAYER_HEADER_SIZE]); + d.extend_from_slice(decrypted); + + Ok(d) + } +} diff --git a/vendor/dtls/src/crypto/crypto_ccm.rs b/vendor/dtls/src/crypto/crypto_ccm.rs new file mode 100644 index 00000000..071b4c54 --- /dev/null +++ b/vendor/dtls/src/crypto/crypto_ccm.rs @@ -0,0 +1,188 @@ +// AES-CCM (Counter with CBC-MAC) +// Alternative to GCM mode. +// Available in OpenSSL as of TLS 1.3 (2018), but disabled by default. +// Two AES computations per block, thus expected to be somewhat slower than AES-GCM. +// RFC 6655 year 2012 https://tools.ietf.org/html/rfc6655 +// Much lower adoption, probably because it came after GCM and offer no significant benefit. + +// https://github.com/RustCrypto/AEADs +// https://docs.rs/ccm/0.3.0/ccm/ Or https://crates.io/crates/aes-ccm? + +use std::io::Cursor; + +use aes::Aes128; +use ccm::aead::generic_array::GenericArray; +use ccm::aead::AeadInPlace; +use ccm::consts::{U12, U16, U8}; +use ccm::Ccm; +use ccm::KeyInit; +use rand::Rng; + +use super::*; +use crate::content::*; +use crate::error::*; +use crate::record_layer::record_layer_header::*; + +const CRYPTO_CCM_8_TAG_LENGTH: usize = 8; +const CRYPTO_CCM_TAG_LENGTH: usize = 16; +const CRYPTO_CCM_NONCE_LENGTH: usize = 12; + +type AesCcm8 = Ccm; +type AesCcm = Ccm; + +#[derive(Clone)] +pub enum CryptoCcmTagLen { + CryptoCcm8TagLength, + CryptoCcmTagLength, +} + +enum CryptoCcmType { + CryptoCcm8(AesCcm8), + CryptoCcm(AesCcm), +} + +// State needed to handle encrypted input/output +pub struct CryptoCcm { + local_ccm: CryptoCcmType, + remote_ccm: CryptoCcmType, + local_write_iv: Vec, + remote_write_iv: Vec, + // used by clone() + local_write_key: Vec, + remote_write_key: Vec, +} + +impl Clone for CryptoCcm { + fn clone(&self) -> Self { + match self.local_ccm { + CryptoCcmType::CryptoCcm(_) => Self::new( + &CryptoCcmTagLen::CryptoCcmTagLength, + &self.local_write_key, + &self.local_write_iv, + &self.remote_write_key, + &self.remote_write_iv, + ), + CryptoCcmType::CryptoCcm8(_) => Self::new( + &CryptoCcmTagLen::CryptoCcm8TagLength, + &self.local_write_key, + &self.local_write_iv, + &self.remote_write_key, + &self.remote_write_iv, + ), + } + } +} + +impl CryptoCcm { + pub fn new( + tag_len: &CryptoCcmTagLen, + local_key: &[u8], + local_write_iv: &[u8], + remote_key: &[u8], + remote_write_iv: &[u8], + ) -> Self { + let key = GenericArray::from_slice(local_key); + let local_ccm = match tag_len { + CryptoCcmTagLen::CryptoCcmTagLength => CryptoCcmType::CryptoCcm(AesCcm::new(key)), + CryptoCcmTagLen::CryptoCcm8TagLength => CryptoCcmType::CryptoCcm8(AesCcm8::new(key)), + }; + + let key = GenericArray::from_slice(remote_key); + let remote_ccm = match tag_len { + CryptoCcmTagLen::CryptoCcmTagLength => CryptoCcmType::CryptoCcm(AesCcm::new(key)), + CryptoCcmTagLen::CryptoCcm8TagLength => CryptoCcmType::CryptoCcm8(AesCcm8::new(key)), + }; + + CryptoCcm { + local_ccm, + local_write_key: local_key.to_vec(), + local_write_iv: local_write_iv.to_vec(), + remote_ccm, + remote_write_key: remote_key.to_vec(), + remote_write_iv: remote_write_iv.to_vec(), + } + } + + pub fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { + let payload = &raw[RECORD_LAYER_HEADER_SIZE..]; + let raw = &raw[..RECORD_LAYER_HEADER_SIZE]; + + let mut nonce = vec![0u8; CRYPTO_CCM_NONCE_LENGTH]; + nonce[..4].copy_from_slice(&self.local_write_iv[..4]); + rand::rng().fill(&mut nonce[4..]); + let nonce = GenericArray::from_slice(&nonce); + + let additional_data = generate_aead_additional_data(pkt_rlh, payload.len()); + + let mut buffer: Vec = Vec::new(); + buffer.extend_from_slice(payload); + + match &self.local_ccm { + CryptoCcmType::CryptoCcm(ccm) => { + ccm.encrypt_in_place(nonce, &additional_data, &mut buffer) + .map_err(|e| Error::Other(e.to_string()))?; + } + CryptoCcmType::CryptoCcm8(ccm8) => { + ccm8.encrypt_in_place(nonce, &additional_data, &mut buffer) + .map_err(|e| Error::Other(e.to_string()))?; + } + } + + let mut r = Vec::with_capacity(raw.len() + nonce.len() + buffer.len()); + + r.extend_from_slice(raw); + r.extend_from_slice(&nonce[4..]); + r.extend_from_slice(&buffer); + + // Update recordLayer size to include explicit nonce + let r_len = (r.len() - RECORD_LAYER_HEADER_SIZE) as u16; + r[RECORD_LAYER_HEADER_SIZE - 2..RECORD_LAYER_HEADER_SIZE] + .copy_from_slice(&r_len.to_be_bytes()); + + Ok(r) + } + + pub fn decrypt(&self, r: &[u8]) -> Result> { + let mut reader = Cursor::new(r); + let h = RecordLayerHeader::unmarshal(&mut reader)?; + if h.content_type == ContentType::ChangeCipherSpec { + // Nothing to encrypt with ChangeCipherSpec + return Ok(r.to_vec()); + } + + if r.len() <= (RECORD_LAYER_HEADER_SIZE + 8) { + return Err(Error::ErrNotEnoughRoomForNonce); + } + + let mut nonce = vec![]; + nonce.extend_from_slice(&self.remote_write_iv[..4]); + nonce.extend_from_slice(&r[RECORD_LAYER_HEADER_SIZE..RECORD_LAYER_HEADER_SIZE + 8]); + let nonce = GenericArray::from_slice(&nonce); + + let out = &r[RECORD_LAYER_HEADER_SIZE + 8..]; + + let mut buffer: Vec = Vec::new(); + buffer.extend_from_slice(out); + + match &self.remote_ccm { + CryptoCcmType::CryptoCcm(ccm) => { + let additional_data = + generate_aead_additional_data(&h, out.len() - CRYPTO_CCM_TAG_LENGTH); + ccm.decrypt_in_place(nonce, &additional_data, &mut buffer) + .map_err(|e| Error::Other(e.to_string()))?; + } + CryptoCcmType::CryptoCcm8(ccm8) => { + let additional_data = + generate_aead_additional_data(&h, out.len() - CRYPTO_CCM_8_TAG_LENGTH); + ccm8.decrypt_in_place(nonce, &additional_data, &mut buffer) + .map_err(|e| Error::Other(e.to_string()))?; + } + } + + let mut d = Vec::with_capacity(RECORD_LAYER_HEADER_SIZE + buffer.len()); + d.extend_from_slice(&r[..RECORD_LAYER_HEADER_SIZE]); + d.extend_from_slice(&buffer); + + Ok(d) + } +} diff --git a/vendor/dtls/src/crypto/crypto_chacha20.rs b/vendor/dtls/src/crypto/crypto_chacha20.rs new file mode 100644 index 00000000..5ee5f3dc --- /dev/null +++ b/vendor/dtls/src/crypto/crypto_chacha20.rs @@ -0,0 +1,123 @@ +use std::io::Cursor; + +use chacha20poly1305::aead::generic_array::GenericArray; +use chacha20poly1305::aead::AeadInPlace; +use chacha20poly1305::{ChaCha20Poly1305, KeyInit}; + +use super::*; +use crate::content::*; +use crate::error::*; +use crate::record_layer::record_layer_header::*; // what about Aes256Gcm? + +const CRYPTO_CHACHA20_TAG_LENGTH: usize = 16; +const CRYPTO_CHACHA20_NONCE_LENGTH: usize = 12; + +// State needed to handle encrypted input/output +#[derive(Clone)] +pub struct CryptoChaCha20 { + local_cc: ChaCha20Poly1305, + remote_cc: ChaCha20Poly1305, + local_key: Vec, + remote_key: Vec, + local_write_iv: Vec, + remote_write_iv: Vec, +} + +fn noncegen(nonce: &mut [u8], epoch: u16, seqnum: u64) { + let epoch: u64 = epoch.into(); + let seqnum = (seqnum & 0xFFFFFFFFFFFF) | (epoch << 48); + for i in 0..8 { + nonce[i + 4] ^= ((seqnum >> (8 * (7 - i))) & 0xFF) as u8; + } +} + +impl CryptoChaCha20 { + pub fn new( + local_key: &[u8], + local_write_iv: &[u8], + remote_key: &[u8], + remote_write_iv: &[u8], + ) -> Self { + let key = GenericArray::from_slice(local_key); + let local_cc = ChaCha20Poly1305::new(key); + + let key = GenericArray::from_slice(remote_key); + let remote_cc = ChaCha20Poly1305::new(key); + + CryptoChaCha20 { + local_cc, + local_write_iv: local_write_iv.to_vec(), + remote_cc, + local_key: local_key.to_vec(), + remote_key: remote_key.to_vec(), + remote_write_iv: remote_write_iv.to_vec(), + } + } + + pub fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { + let payload = &raw[RECORD_LAYER_HEADER_SIZE..]; + let raw = &raw[..RECORD_LAYER_HEADER_SIZE]; + + let mut nonce = vec![0u8; CRYPTO_CHACHA20_NONCE_LENGTH]; + nonce[..CRYPTO_CHACHA20_NONCE_LENGTH] + .copy_from_slice(&self.local_write_iv[..CRYPTO_CHACHA20_NONCE_LENGTH]); + + noncegen(&mut nonce[..], pkt_rlh.epoch, pkt_rlh.sequence_number); + let nonce = GenericArray::from_slice(&nonce); + + let additional_data = generate_aead_additional_data(pkt_rlh, payload.len()); + + let mut buffer: Vec = Vec::new(); + buffer.extend_from_slice(payload); + + let tag = self + .local_cc + .encrypt_in_place_detached(nonce, &additional_data, &mut buffer) + .map_err(|e| Error::Other(e.to_string()))?; + + let mut r = Vec::with_capacity(raw.len() + buffer.len() + tag.len()); + r.extend_from_slice(raw); + r.extend_from_slice(&buffer); + r.extend_from_slice(&tag); + + // Update recordLayer size to include explicit nonce + let r_len = (r.len() - RECORD_LAYER_HEADER_SIZE) as u16; + r[RECORD_LAYER_HEADER_SIZE - 2..RECORD_LAYER_HEADER_SIZE] + .copy_from_slice(&r_len.to_be_bytes()); + + Ok(r) + } + + pub fn decrypt(&self, r: &[u8]) -> Result> { + let mut reader = Cursor::new(r); + let h = RecordLayerHeader::unmarshal(&mut reader)?; + if h.content_type == ContentType::ChangeCipherSpec { + // Nothing to encrypt with ChangeCipherSpec + return Ok(r.to_vec()); + } + + let mut nonce = vec![]; + nonce.extend_from_slice(&self.remote_write_iv[..]); + + noncegen(&mut nonce[..], h.epoch, h.sequence_number); + let nonce = GenericArray::from_slice(&nonce); + + let out = &r[RECORD_LAYER_HEADER_SIZE..]; + + let additional_data = + generate_aead_additional_data(&h, out.len() - CRYPTO_CHACHA20_TAG_LENGTH); + + let mut buffer: Vec = Vec::new(); + buffer.extend_from_slice(out); + + self.remote_cc + .decrypt_in_place(nonce, &additional_data, &mut buffer) + .map_err(|e| Error::Other(e.to_string()))?; + + let mut d = Vec::with_capacity(RECORD_LAYER_HEADER_SIZE + buffer.len()); + d.extend_from_slice(&r[..RECORD_LAYER_HEADER_SIZE]); + d.extend_from_slice(&buffer); + + Ok(d) + } +} diff --git a/vendor/dtls/src/crypto/crypto_gcm.rs b/vendor/dtls/src/crypto/crypto_gcm.rs new file mode 100644 index 00000000..a692bd95 --- /dev/null +++ b/vendor/dtls/src/crypto/crypto_gcm.rs @@ -0,0 +1,119 @@ +// AES-GCM (Galois Counter Mode) +// The most widely used block cipher worldwide. +// Mandatory as of TLS 1.2 (2008) and used by default by most clients. +// RFC 5288 year 2008 https://tools.ietf.org/html/rfc5288 + +// https://github.com/RustCrypto/AEADs +// https://docs.rs/aes-gcm/0.8.0/aes_gcm/ + +use std::io::Cursor; + +use aes_gcm::aead::generic_array::GenericArray; +use aes_gcm::aead::AeadInPlace; +use aes_gcm::{Aes128Gcm, KeyInit}; +use rand::Rng; + +use super::*; +use crate::content::*; +use crate::error::*; +use crate::record_layer::record_layer_header::*; // what about Aes256Gcm? + +const CRYPTO_GCM_TAG_LENGTH: usize = 16; +const CRYPTO_GCM_NONCE_LENGTH: usize = 12; + +// State needed to handle encrypted input/output +#[derive(Clone)] +pub struct CryptoGcm { + local_gcm: Aes128Gcm, + remote_gcm: Aes128Gcm, + local_write_iv: Vec, + remote_write_iv: Vec, +} + +impl CryptoGcm { + pub fn new( + local_key: &[u8], + local_write_iv: &[u8], + remote_key: &[u8], + remote_write_iv: &[u8], + ) -> Self { + let key = GenericArray::from_slice(local_key); + let local_gcm = Aes128Gcm::new(key); + + let key = GenericArray::from_slice(remote_key); + let remote_gcm = Aes128Gcm::new(key); + + CryptoGcm { + local_gcm, + local_write_iv: local_write_iv.to_vec(), + remote_gcm, + remote_write_iv: remote_write_iv.to_vec(), + } + } + + pub fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { + let payload = &raw[RECORD_LAYER_HEADER_SIZE..]; + let raw = &raw[..RECORD_LAYER_HEADER_SIZE]; + + let mut nonce = vec![0u8; CRYPTO_GCM_NONCE_LENGTH]; + nonce[..4].copy_from_slice(&self.local_write_iv[..4]); + rand::rng().fill(&mut nonce[4..]); + let nonce = GenericArray::from_slice(&nonce); + + let additional_data = generate_aead_additional_data(pkt_rlh, payload.len()); + + let mut buffer: Vec = Vec::new(); + buffer.extend_from_slice(payload); + + self.local_gcm + .encrypt_in_place(nonce, &additional_data, &mut buffer) + .map_err(|e| Error::Other(e.to_string()))?; + + let mut r = Vec::with_capacity(raw.len() + nonce.len() + buffer.len()); + r.extend_from_slice(raw); + r.extend_from_slice(&nonce[4..]); + r.extend_from_slice(&buffer); + + // Update recordLayer size to include explicit nonce + let r_len = (r.len() - RECORD_LAYER_HEADER_SIZE) as u16; + r[RECORD_LAYER_HEADER_SIZE - 2..RECORD_LAYER_HEADER_SIZE] + .copy_from_slice(&r_len.to_be_bytes()); + + Ok(r) + } + + pub fn decrypt(&self, r: &[u8]) -> Result> { + let mut reader = Cursor::new(r); + let h = RecordLayerHeader::unmarshal(&mut reader)?; + if h.content_type == ContentType::ChangeCipherSpec { + // Nothing to encrypt with ChangeCipherSpec + return Ok(r.to_vec()); + } + + if r.len() <= (RECORD_LAYER_HEADER_SIZE + 8) { + return Err(Error::ErrNotEnoughRoomForNonce); + } + + let mut nonce = vec![]; + nonce.extend_from_slice(&self.remote_write_iv[..4]); + nonce.extend_from_slice(&r[RECORD_LAYER_HEADER_SIZE..RECORD_LAYER_HEADER_SIZE + 8]); + let nonce = GenericArray::from_slice(&nonce); + + let out = &r[RECORD_LAYER_HEADER_SIZE + 8..]; + + let additional_data = generate_aead_additional_data(&h, out.len() - CRYPTO_GCM_TAG_LENGTH); + + let mut buffer: Vec = Vec::new(); + buffer.extend_from_slice(out); + + self.remote_gcm + .decrypt_in_place(nonce, &additional_data, &mut buffer) + .map_err(|e| Error::Other(e.to_string()))?; + + let mut d = Vec::with_capacity(RECORD_LAYER_HEADER_SIZE + buffer.len()); + d.extend_from_slice(&r[..RECORD_LAYER_HEADER_SIZE]); + d.extend_from_slice(&buffer); + + Ok(d) + } +} diff --git a/vendor/dtls/src/crypto/crypto_test.rs b/vendor/dtls/src/crypto/crypto_test.rs new file mode 100644 index 00000000..f06988bc --- /dev/null +++ b/vendor/dtls/src/crypto/crypto_test.rs @@ -0,0 +1,221 @@ +use std::io::Cursor; + +use x509_parser::pem::Pem; + +use super::crypto_ccm::*; +use super::*; +use crate::content::ContentType; +use crate::record_layer::record_layer_header::{ProtocolVersion, RECORD_LAYER_HEADER_SIZE}; + +const RAW_PRIVATE_KEY: &str = " +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAxIA2BrrnR2sIlATsp7aRBD/3krwZ7vt9dNeoDQAee0s6SuYP +6MBx/HPnAkwNvPS90R05a7pwRkoT6Ur4PfPhCVlUe8lV+0Eto3ZSEeHz3HdsqlM3 +bso67L7Dqrc7MdVstlKcgJi8yeAoGOIL9/igOv0XBFCeznm9nznx6mnsR5cugw+1 +ypXelaHmBCLV7r5SeVSh57+KhvZGbQ2fFpUaTPegRpJZXBNS8lSeWvtOv9d6N5UB +ROTAJodMZT5AfX0jB0QB9IT/0I96H6BSENH08NXOeXApMuLKvnAf361rS7cRAfRL +rWZqERMP4u6Cnk0Cnckc3WcW27kGGIbtwbqUIQIDAQABAoIBAGF7OVIdZp8Hejn0 +N3L8HvT8xtUEe9kS6ioM0lGgvX5s035Uo4/T6LhUx0VcdXRH9eLHnLTUyN4V4cra +ZkxVsE3zAvZl60G6E+oDyLMWZOP6Wu4kWlub9597A5atT7BpMIVCdmFVZFLB4SJ3 +AXkC3nplFAYP+Lh1rJxRIrIn2g+pEeBboWbYA++oDNuMQffDZaokTkJ8Bn1JZYh0 +xEXKY8Bi2Egd5NMeZa1UFO6y8tUbZfwgVs6Enq5uOgtfayq79vZwyjj1kd29MBUD +8g8byV053ZKxbUOiOuUts97eb+fN3DIDRTcT2c+lXt/4C54M1FclJAbtYRK/qwsl +pYWKQAECgYEA4ZUbqQnTo1ICvj81ifGrz+H4LKQqe92Hbf/W51D/Umk2kP702W22 +HP4CvrJRtALThJIG9m2TwUjl/WAuZIBrhSAbIvc3Fcoa2HjdRp+sO5U1ueDq7d/S +Z+PxRI8cbLbRpEdIaoR46qr/2uWZ943PHMv9h4VHPYn1w8b94hwD6vkCgYEA3v87 +mFLzyM9ercnEv9zHMRlMZFQhlcUGQZvfb8BuJYl/WogyT6vRrUuM0QXULNEPlrin +mBQTqc1nCYbgkFFsD2VVt1qIyiAJsB9MD1LNV6YuvE7T2KOSadmsA4fa9PUqbr71 +hf3lTTq+LeR09LebO7WgSGYY+5YKVOEGpYMR1GkCgYEAxPVQmk3HKHEhjgRYdaG5 +lp9A9ZE8uruYVJWtiHgzBTxx9TV2iST+fd/We7PsHFTfY3+wbpcMDBXfIVRKDVwH +BMwchXH9+Ztlxx34bYJaegd0SmA0Hw9ugWEHNgoSEmWpM1s9wir5/ELjc7dGsFtz +uzvsl9fpdLSxDYgAAdzeGtkCgYBAzKIgrVox7DBzB8KojhtD5ToRnXD0+H/M6OKQ +srZPKhlb0V/tTtxrIx0UUEFLlKSXA6mPw6XDHfDnD86JoV9pSeUSlrhRI+Ysy6tq +eIE7CwthpPZiaYXORHZ7wCqcK/HcpJjsCs9rFbrV0yE5S3FMdIbTAvgXg44VBB7O +UbwIoQKBgDuY8gSrA5/A747wjjmsdRWK4DMTMEV4eCW1BEP7Tg7Cxd5n3xPJiYhr +nhLGN+mMnVIcv2zEMS0/eNZr1j/0BtEdx+3IC6Eq+ONY0anZ4Irt57/5QeKgKn/L +JPhfPySIPG4UmwE4gW8t79vfOKxnUu2fDD1ZXUYopan6EckACNH/ +-----END RSA PRIVATE KEY----- +"; + +#[test] +fn test_generate_key_signature() -> Result<()> { + let reader = Cursor::new(RAW_PRIVATE_KEY.as_bytes()); + let pem = match Pem::read(reader) { + Ok((pem, _)) => pem, + Err(_) => return Err(Error::Other("Pem::read error".to_owned())), + }; + //let private_key = rsa::RSAPrivateKey::from_pkcs1(&pem.contents)?; + + let client_random = vec![ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, + 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, + 0x1e, 0x1f, + ]; + let server_random = vec![ + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, + 0x7f, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, + 0x8e, 0x8f, + ]; + let public_key = vec![ + 0x20, 0x9f, 0xd7, 0xad, 0x6d, 0xcf, 0xf4, 0x29, 0x8d, 0xd3, 0xf9, 0x6d, 0x5b, 0x1b, 0x2a, + 0xf9, 0x10, 0xa0, 0x53, 0x5b, 0x14, 0x88, 0xd7, 0xf8, 0xfa, 0xbb, 0x34, 0x9a, 0x98, 0x28, + 0x80, 0xb6, 0x15, + ]; + let expected_signature = vec![ + 0x6f, 0x47, 0x97, 0x85, 0xcc, 0x76, 0x50, 0x93, 0xbd, 0xe2, 0x6a, 0x69, 0x0b, 0xc3, 0x03, + 0xd1, 0xb7, 0xe4, 0xab, 0x88, 0x7b, 0xa6, 0x52, 0x80, 0xdf, 0xaa, 0x25, 0x7a, 0xdb, 0x29, + 0x32, 0xe4, 0xd8, 0x28, 0x28, 0xb3, 0xe8, 0x04, 0x3c, 0x38, 0x16, 0xfc, 0x78, 0xe9, 0x15, + 0x7b, 0xc5, 0xbd, 0x7d, 0xfc, 0xcd, 0x83, 0x00, 0x57, 0x4a, 0x3c, 0x23, 0x85, 0x75, 0x6b, + 0x37, 0xd5, 0x89, 0x72, 0x73, 0xf0, 0x44, 0x8c, 0x00, 0x70, 0x1f, 0x6e, 0xa2, 0x81, 0xd0, + 0x09, 0xc5, 0x20, 0x36, 0xab, 0x23, 0x09, 0x40, 0x1f, 0x4d, 0x45, 0x96, 0x62, 0xbb, 0x81, + 0xb0, 0x30, 0x72, 0xad, 0x3a, 0x0a, 0xac, 0x31, 0x63, 0x40, 0x52, 0x0a, 0x27, 0xf3, 0x34, + 0xde, 0x27, 0x7d, 0xb7, 0x54, 0xff, 0x0f, 0x9f, 0x5a, 0xfe, 0x07, 0x0f, 0x4e, 0x9f, 0x53, + 0x04, 0x34, 0x62, 0xf4, 0x30, 0x74, 0x83, 0x35, 0xfc, 0xe4, 0x7e, 0xbf, 0x5a, 0xc4, 0x52, + 0xd0, 0xea, 0xf9, 0x61, 0x4e, 0xf5, 0x1c, 0x0e, 0x58, 0x02, 0x71, 0xfb, 0x1f, 0x34, 0x55, + 0xe8, 0x36, 0x70, 0x3c, 0xc1, 0xcb, 0xc9, 0xb7, 0xbb, 0xb5, 0x1c, 0x44, 0x9a, 0x6d, 0x88, + 0x78, 0x98, 0xd4, 0x91, 0x2e, 0xeb, 0x98, 0x81, 0x23, 0x30, 0x73, 0x39, 0x43, 0xd5, 0xbb, + 0x70, 0x39, 0xba, 0x1f, 0xdb, 0x70, 0x9f, 0x91, 0x83, 0x56, 0xc2, 0xde, 0xed, 0x17, 0x6d, + 0x2c, 0x3e, 0x21, 0xea, 0x36, 0xb4, 0x91, 0xd8, 0x31, 0x05, 0x60, 0x90, 0xfd, 0xc6, 0x74, + 0xa9, 0x7b, 0x18, 0xfc, 0x1c, 0x6a, 0x1c, 0x6e, 0xec, 0xd3, 0xc1, 0xc0, 0x0d, 0x11, 0x25, + 0x48, 0x37, 0x3d, 0x45, 0x11, 0xa2, 0x31, 0x14, 0x0a, 0x66, 0x9f, 0xd8, 0xac, 0x74, 0xa2, + 0xcd, 0xc8, 0x79, 0xb3, 0x9e, 0xc6, 0x66, 0x25, 0xcf, 0x2c, 0x87, 0x5e, 0x5c, 0x36, 0x75, + 0x86, + ]; + + let signature = generate_key_signature( + &client_random, + &server_random, + &public_key, + NamedCurve::X25519, + &CryptoPrivateKey { + kind: CryptoPrivateKeyKind::Rsa256( + ring::rsa::KeyPair::from_der(&pem.contents) + .map_err(|e| Error::Other(e.to_string()))?, + ), + serialized_der: pem.contents.clone(), + }, //hashAlgorithmSHA256, + )?; + + assert_eq!( + signature, expected_signature, + "Signature generation failed \nexp {expected_signature:?} \nactual {signature:?} " + ); + + Ok(()) +} + +#[test] +fn test_ccm_encryption_and_decryption() -> Result<()> { + let key = vec![ + 0x18, 0x78, 0xac, 0xc2, 0x2a, 0xd8, 0xbd, 0xd8, 0xc6, 0x01, 0xa6, 0x17, 0x12, 0x6f, 0x63, + 0x54, + ]; + let iv = vec![0x0e, 0xb2, 0x09, 0x06]; + + let ccm = CryptoCcm::new(&CryptoCcmTagLen::CryptoCcmTagLength, &key, &iv, &key, &iv); + + let rlh = RecordLayerHeader { + content_type: ContentType::ApplicationData, + protocol_version: ProtocolVersion { + major: 0xfe, + minor: 0xff, + }, + epoch: 0, + sequence_number: 18, + content_len: 3, + }; + + let raw = vec![ + 0x17, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0x03, 0xff, 0xaa, + 0xbb, + ]; + + let cipher_text = ccm.encrypt(&rlh, &raw)?; + + assert_eq!( + &cipher_text[RECORD_LAYER_HEADER_SIZE - 2..RECORD_LAYER_HEADER_SIZE], + [0, 27], + "RecordLayer size updating failed \nexp: {:?} \nactual {:?} ", + [0, 27], + &cipher_text[RECORD_LAYER_HEADER_SIZE - 2..RECORD_LAYER_HEADER_SIZE] + ); + + let plain_text = ccm.decrypt(&cipher_text)?; + + assert_eq!( + raw[RECORD_LAYER_HEADER_SIZE..], + plain_text[RECORD_LAYER_HEADER_SIZE..], + "Decryption failed \nexp: {:?} \nactual {:?} ", + &raw[RECORD_LAYER_HEADER_SIZE..], + &plain_text[RECORD_LAYER_HEADER_SIZE..] + ); + + Ok(()) +} + +#[test] +fn test_certificate_verify() -> Result<()> { + let plain_text: Vec = vec![ + 0x6f, 0x47, 0x97, 0x85, 0xcc, 0x76, 0x50, 0x93, 0xbd, 0xe2, 0x6a, 0x69, 0x0b, 0xc3, 0x03, + 0xd1, 0xb7, 0xe4, 0xab, 0x88, 0x7b, 0xa6, 0x52, 0x80, 0xdf, 0xaa, 0x25, 0x7a, 0xdb, 0x29, + 0x32, 0xe4, 0xd8, 0x28, 0x28, 0xb3, 0xe8, 0x04, 0x3c, 0x38, 0x16, 0xfc, 0x78, 0xe9, 0x15, + 0x7b, 0xc5, 0xbd, 0x7d, 0xfc, 0xcd, 0x83, 0x00, 0x57, 0x4a, 0x3c, 0x23, 0x85, 0x75, 0x6b, + 0x37, 0xd5, 0x89, 0x72, 0x73, 0xf0, 0x44, 0x8c, 0x00, 0x70, 0x1f, 0x6e, 0xa2, 0x81, 0xd0, + 0x09, 0xc5, 0x20, 0x36, 0xab, 0x23, 0x09, 0x40, 0x1f, 0x4d, 0x45, 0x96, 0x62, 0xbb, 0x81, + 0xb0, 0x30, 0x72, 0xad, 0x3a, 0x0a, 0xac, 0x31, 0x63, 0x40, 0x52, 0x0a, 0x27, 0xf3, 0x34, + 0xde, 0x27, 0x7d, 0xb7, 0x54, 0xff, 0x0f, 0x9f, 0x5a, 0xfe, 0x07, 0x0f, 0x4e, 0x9f, 0x53, + 0x04, 0x34, 0x62, 0xf4, 0x30, 0x74, 0x83, 0x35, 0xfc, 0xe4, 0x7e, 0xbf, 0x5a, 0xc4, 0x52, + 0xd0, 0xea, 0xf9, 0x61, 0x4e, 0xf5, 0x1c, 0x0e, 0x58, 0x02, 0x71, 0xfb, 0x1f, 0x34, 0x55, + 0xe8, 0x36, 0x70, 0x3c, 0xc1, 0xcb, 0xc9, 0xb7, 0xbb, 0xb5, 0x1c, 0x44, 0x9a, 0x6d, 0x88, + 0x78, 0x98, 0xd4, 0x91, 0x2e, 0xeb, 0x98, 0x81, 0x23, 0x30, 0x73, 0x39, 0x43, 0xd5, 0xbb, + 0x70, 0x39, 0xba, 0x1f, 0xdb, 0x70, 0x9f, 0x91, 0x83, 0x56, 0xc2, 0xde, 0xed, 0x17, 0x6d, + 0x2c, 0x3e, 0x21, 0xea, 0x36, 0xb4, 0x91, 0xd8, 0x31, 0x05, 0x60, 0x90, 0xfd, 0xc6, 0x74, + 0xa9, 0x7b, 0x18, 0xfc, 0x1c, 0x6a, 0x1c, 0x6e, 0xec, 0xd3, 0xc1, 0xc0, 0x0d, 0x11, 0x25, + 0x48, 0x37, 0x3d, 0x45, 0x11, 0xa2, 0x31, 0x14, 0x0a, 0x66, 0x9f, 0xd8, 0xac, 0x74, 0xa2, + 0xcd, 0xc8, 0x79, 0xb3, 0x9e, 0xc6, 0x66, 0x25, 0xcf, 0x2c, 0x87, 0x5e, 0x5c, 0x36, 0x75, + 0x86, + ]; + + //test ECDSA256 + let certificate_ecdsa256 = Certificate::generate_self_signed(vec!["localhost".to_owned()])?; + let cert_verify_ecdsa256 = + generate_certificate_verify(&plain_text, &certificate_ecdsa256.private_key)?; + verify_certificate_verify( + &plain_text, + &SignatureHashAlgorithm { + hash: HashAlgorithm::Sha256, + signature: SignatureAlgorithm::Ecdsa, + }, + &cert_verify_ecdsa256, + &certificate_ecdsa256 + .certificate + .iter() + .map(|x| x.as_ref().to_owned()) + .collect::>>(), + false, + )?; + + //test ED25519 + let certificate_ed25519 = Certificate::generate_self_signed_with_alg( + vec!["localhost".to_owned()], + &rcgen::PKCS_ED25519, + )?; + let cert_verify_ed25519 = + generate_certificate_verify(&plain_text, &certificate_ed25519.private_key)?; + verify_certificate_verify( + &plain_text, + &SignatureHashAlgorithm { + hash: HashAlgorithm::Sha256, + signature: SignatureAlgorithm::Ed25519, + }, + &cert_verify_ed25519, + &certificate_ed25519 + .certificate + .iter() + .map(|x| x.as_ref().to_owned()) + .collect::>>(), + false, + )?; + + Ok(()) +} diff --git a/vendor/dtls/src/crypto/mod.rs b/vendor/dtls/src/crypto/mod.rs new file mode 100644 index 00000000..8f99f707 --- /dev/null +++ b/vendor/dtls/src/crypto/mod.rs @@ -0,0 +1,514 @@ +#[cfg(test)] +mod crypto_test; + +pub mod crypto_cbc; +pub mod crypto_ccm; +pub mod crypto_chacha20; +pub mod crypto_gcm; +pub mod padding; + +use std::convert::TryFrom; +use std::sync::Arc; + +use der_parser::oid; +use der_parser::oid::Oid; + +use crate::certificate_verifier::CertificateVerifier; +use rustls_pki_types::CertificateDer; + +use rcgen::{generate_simple_self_signed, CertifiedKey, KeyPair}; +use ring::rand::SystemRandom; +use ring::signature::{EcdsaKeyPair, Ed25519KeyPair}; + +use crate::curve::named_curve::*; +use crate::error::*; +use crate::record_layer::record_layer_header::*; +use crate::signature_hash_algorithm::{HashAlgorithm, SignatureAlgorithm, SignatureHashAlgorithm}; + +/// A X.509 certificate(s) used to authenticate a DTLS connection. +#[derive(Clone, PartialEq, Debug)] +pub struct Certificate { + /// DER-encoded certificates. + pub certificate: Vec>, + /// Private key. + pub private_key: CryptoPrivateKey, +} + +impl Certificate { + /// Generate a self-signed certificate. + /// + /// See [`rcgen::generate_simple_self_signed`]. + pub fn generate_self_signed(subject_alt_names: impl Into>) -> Result { + let CertifiedKey { cert, key_pair } = + generate_simple_self_signed(subject_alt_names).unwrap(); + Ok(Certificate { + certificate: vec![cert.der().to_owned()], + private_key: CryptoPrivateKey::try_from(&key_pair)?, + }) + } + + /// Generate a self-signed certificate with the given algorithm. + /// + /// See [`rcgen::Certificate::from_params`]. + pub fn generate_self_signed_with_alg( + subject_alt_names: impl Into>, + alg: &'static rcgen::SignatureAlgorithm, + ) -> Result { + let params = rcgen::CertificateParams::new(subject_alt_names).unwrap(); + let key_pair = rcgen::KeyPair::generate_for(alg).unwrap(); + let cert = params.self_signed(&key_pair).unwrap(); + + Ok(Certificate { + certificate: vec![cert.der().to_owned()], + private_key: CryptoPrivateKey::try_from(&key_pair)?, + }) + } + + /// Parses a certificate from the ASCII PEM format. + #[cfg(feature = "pem")] + pub fn from_pem(pem_str: &str) -> Result { + let mut pems = pem::parse_many(pem_str).map_err(|e| Error::InvalidPEM(e.to_string()))?; + if pems.len() < 2 { + return Err(Error::InvalidPEM(format!( + "expected at least two PEM blocks, got {}", + pems.len() + ))); + } + if pems[0].tag() != "PRIVATE_KEY" { + return Err(Error::InvalidPEM(format!( + "invalid tag (expected: 'PRIVATE_KEY', got: '{}')", + pems[0].tag() + ))); + } + + let keypair = KeyPair::try_from(pems[0].contents()) + .map_err(|e| Error::InvalidPEM(format!("can't decode keypair: {e}")))?; + + let mut rustls_certs = Vec::new(); + for p in pems.drain(1..) { + if p.tag() != "CERTIFICATE" { + return Err(Error::InvalidPEM(format!( + "invalid tag (expected: 'CERTIFICATE', got: '{}')", + p.tag() + ))); + } + rustls_certs.push(CertificateDer::from(p.contents().to_vec())); + } + + Ok(Certificate { + certificate: rustls_certs, + private_key: CryptoPrivateKey::try_from(&keypair)?, + }) + } + + /// Serializes the certificate (including the private key) in PKCS#8 format in PEM. + #[cfg(feature = "pem")] + pub fn serialize_pem(&self) -> String { + let mut data = vec![pem::Pem::new( + "PRIVATE_KEY".to_string(), + self.private_key.serialized_der.clone(), + )]; + for rustls_cert in &self.certificate { + data.push(pem::Pem::new( + "CERTIFICATE".to_string(), + rustls_cert.as_ref(), + )); + } + pem::encode_many(&data) + } +} + +pub(crate) fn value_key_message( + client_random: &[u8], + server_random: &[u8], + public_key: &[u8], + named_curve: NamedCurve, +) -> Vec { + let mut server_ecdh_params = vec![0u8; 4]; + server_ecdh_params[0] = 3; // named curve + server_ecdh_params[1..3].copy_from_slice(&(named_curve as u16).to_be_bytes()); + server_ecdh_params[3] = public_key.len() as u8; + + let mut plaintext = vec![]; + plaintext.extend_from_slice(client_random); + plaintext.extend_from_slice(server_random); + plaintext.extend_from_slice(&server_ecdh_params); + plaintext.extend_from_slice(public_key); + + plaintext +} + +/// Either ED25519, ECDSA or RSA keypair. +#[derive(Debug)] +pub enum CryptoPrivateKeyKind { + Ed25519(Ed25519KeyPair), + Ecdsa256(EcdsaKeyPair), + Rsa256(ring::rsa::KeyPair), +} + +/// Private key. +#[derive(Debug)] +pub struct CryptoPrivateKey { + /// Keypair. + pub kind: CryptoPrivateKeyKind, + /// DER-encoded keypair. + pub serialized_der: Vec, +} + +impl PartialEq for CryptoPrivateKey { + fn eq(&self, other: &Self) -> bool { + if self.serialized_der != other.serialized_der { + return false; + } + + matches!( + (&self.kind, &other.kind), + ( + CryptoPrivateKeyKind::Rsa256(_), + CryptoPrivateKeyKind::Rsa256(_) + ) | ( + CryptoPrivateKeyKind::Ecdsa256(_), + CryptoPrivateKeyKind::Ecdsa256(_) + ) | ( + CryptoPrivateKeyKind::Ed25519(_), + CryptoPrivateKeyKind::Ed25519(_) + ) + ) + } +} + +impl Clone for CryptoPrivateKey { + fn clone(&self) -> Self { + match self.kind { + CryptoPrivateKeyKind::Ed25519(_) => CryptoPrivateKey { + kind: CryptoPrivateKeyKind::Ed25519( + Ed25519KeyPair::from_pkcs8_maybe_unchecked(&self.serialized_der).unwrap(), + ), + serialized_der: self.serialized_der.clone(), + }, + CryptoPrivateKeyKind::Ecdsa256(_) => CryptoPrivateKey { + kind: CryptoPrivateKeyKind::Ecdsa256( + EcdsaKeyPair::from_pkcs8( + &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING, + &self.serialized_der, + &SystemRandom::new(), + ) + .unwrap(), + ), + serialized_der: self.serialized_der.clone(), + }, + CryptoPrivateKeyKind::Rsa256(_) => CryptoPrivateKey { + kind: CryptoPrivateKeyKind::Rsa256( + ring::rsa::KeyPair::from_pkcs8(&self.serialized_der).unwrap(), + ), + serialized_der: self.serialized_der.clone(), + }, + } + } +} + +impl TryFrom<&KeyPair> for CryptoPrivateKey { + type Error = Error; + + fn try_from(key_pair: &KeyPair) -> Result { + Self::from_key_pair(key_pair) + } +} + +impl CryptoPrivateKey { + pub fn from_key_pair(key_pair: &KeyPair) -> Result { + let serialized_der = key_pair.serialize_der(); + if key_pair.is_compatible(&rcgen::PKCS_ED25519) { + Ok(CryptoPrivateKey { + kind: CryptoPrivateKeyKind::Ed25519( + Ed25519KeyPair::from_pkcs8_maybe_unchecked(&serialized_der) + .map_err(|e| Error::Other(e.to_string()))?, + ), + serialized_der, + }) + } else if key_pair.is_compatible(&rcgen::PKCS_ECDSA_P256_SHA256) { + Ok(CryptoPrivateKey { + kind: CryptoPrivateKeyKind::Ecdsa256( + EcdsaKeyPair::from_pkcs8( + &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING, + &serialized_der, + &SystemRandom::new(), + ) + .map_err(|e| Error::Other(e.to_string()))?, + ), + serialized_der, + }) + } else if key_pair.is_compatible(&rcgen::PKCS_RSA_SHA256) { + Ok(CryptoPrivateKey { + kind: CryptoPrivateKeyKind::Rsa256( + ring::rsa::KeyPair::from_pkcs8(&serialized_der) + .map_err(|e| Error::Other(e.to_string()))?, + ), + serialized_der, + }) + } else { + Err(Error::Other("Unsupported key_pair".to_owned())) + } + } +} + +// If the client provided a "signature_algorithms" extension, then all +// certificates provided by the server MUST be signed by a +// hash/signature algorithm pair that appears in that extension +// +// https://tools.ietf.org/html/rfc5246#section-7.4.2 +pub(crate) fn generate_key_signature( + client_random: &[u8], + server_random: &[u8], + public_key: &[u8], + named_curve: NamedCurve, + private_key: &CryptoPrivateKey, /*, hash_algorithm: HashAlgorithm*/ +) -> Result> { + let msg = value_key_message(client_random, server_random, public_key, named_curve); + let signature = match &private_key.kind { + CryptoPrivateKeyKind::Ed25519(kp) => kp.sign(&msg).as_ref().to_vec(), + CryptoPrivateKeyKind::Ecdsa256(kp) => { + let system_random = SystemRandom::new(); + kp.sign(&system_random, &msg) + .map_err(|e| Error::Other(e.to_string()))? + .as_ref() + .to_vec() + } + CryptoPrivateKeyKind::Rsa256(kp) => { + let system_random = SystemRandom::new(); + let mut signature = vec![0; kp.public().modulus_len()]; + kp.sign( + &ring::signature::RSA_PKCS1_SHA256, + &system_random, + &msg, + &mut signature, + ) + .map_err(|e| Error::Other(e.to_string()))?; + + signature + } + }; + + Ok(signature) +} + +// add OID_ED25519 which is not defined in x509_parser +pub const OID_ED25519: Oid<'static> = oid!(1.3.101 .112); +pub const OID_ECDSA: Oid<'static> = oid!(1.2.840 .10045 .2 .1); + +fn verify_signature( + message: &[u8], + hash_algorithm: &SignatureHashAlgorithm, + remote_key_signature: &[u8], + raw_certificates: &[Vec], + insecure_verification: bool, +) -> Result<()> { + if raw_certificates.is_empty() { + return Err(Error::ErrLengthMismatch); + } + + let (_, certificate) = x509_parser::parse_x509_certificate(&raw_certificates[0]) + .map_err(|e| Error::Other(e.to_string()))?; + + let verify_alg: &dyn ring::signature::VerificationAlgorithm = match hash_algorithm.signature { + SignatureAlgorithm::Ed25519 => &ring::signature::ED25519, + SignatureAlgorithm::Ecdsa if hash_algorithm.hash == HashAlgorithm::Sha256 => { + &ring::signature::ECDSA_P256_SHA256_ASN1 + } + SignatureAlgorithm::Ecdsa if hash_algorithm.hash == HashAlgorithm::Sha384 => { + &ring::signature::ECDSA_P384_SHA384_ASN1 + } + SignatureAlgorithm::Rsa if hash_algorithm.hash == HashAlgorithm::Sha1 => { + &ring::signature::RSA_PKCS1_1024_8192_SHA1_FOR_LEGACY_USE_ONLY + } + SignatureAlgorithm::Rsa if (hash_algorithm.hash == HashAlgorithm::Sha256) => { + if remote_key_signature.len() < 256 && insecure_verification { + &ring::signature::RSA_PKCS1_1024_8192_SHA256_FOR_LEGACY_USE_ONLY + } else { + &ring::signature::RSA_PKCS1_2048_8192_SHA256 + } + } + SignatureAlgorithm::Rsa if hash_algorithm.hash == HashAlgorithm::Sha384 => { + &ring::signature::RSA_PKCS1_2048_8192_SHA384 + } + SignatureAlgorithm::Rsa if hash_algorithm.hash == HashAlgorithm::Sha512 => { + if remote_key_signature.len() < 256 && insecure_verification { + &ring::signature::RSA_PKCS1_1024_8192_SHA512_FOR_LEGACY_USE_ONLY + } else { + &ring::signature::RSA_PKCS1_2048_8192_SHA512 + } + } + _ => return Err(Error::ErrKeySignatureVerifyUnimplemented), + }; + + log::trace!("Picked an algorithm {verify_alg:?}"); + + let public_key = ring::signature::UnparsedPublicKey::new( + verify_alg, + certificate + .tbs_certificate + .subject_pki + .subject_public_key + .data, + ); + + public_key + .verify(message, remote_key_signature) + .map_err(|e| Error::Other(e.to_string()))?; + + Ok(()) +} + +pub(crate) fn verify_key_signature( + message: &[u8], + hash_algorithm: &SignatureHashAlgorithm, + remote_key_signature: &[u8], + raw_certificates: &[Vec], + insecure_verification: bool, +) -> Result<()> { + verify_signature( + message, + hash_algorithm, + remote_key_signature, + raw_certificates, + insecure_verification, + ) +} + +// If the server has sent a CertificateRequest message, the client MUST send the Certificate +// message. The ClientKeyExchange message is now sent, and the content +// of that message will depend on the public key algorithm selected +// between the ClientHello and the ServerHello. If the client has sent +// a certificate with signing ability, a digitally-signed +// CertificateVerify message is sent to explicitly verify possession of +// the private key in the certificate. +// https://tools.ietf.org/html/rfc5246#section-7.3 +pub(crate) fn generate_certificate_verify( + handshake_bodies: &[u8], + private_key: &CryptoPrivateKey, /*, hashAlgorithm hashAlgorithm*/ +) -> Result> { + let signature = match &private_key.kind { + CryptoPrivateKeyKind::Ed25519(kp) => kp.sign(handshake_bodies).as_ref().to_vec(), + CryptoPrivateKeyKind::Ecdsa256(kp) => { + let system_random = SystemRandom::new(); + kp.sign(&system_random, handshake_bodies) + .map_err(|e| Error::Other(e.to_string()))? + .as_ref() + .to_vec() + } + CryptoPrivateKeyKind::Rsa256(kp) => { + let system_random = SystemRandom::new(); + let mut signature = vec![0; kp.public().modulus_len()]; + kp.sign( + &ring::signature::RSA_PKCS1_SHA256, + &system_random, + handshake_bodies, + &mut signature, + ) + .map_err(|e| Error::Other(e.to_string()))?; + + signature + } + }; + + Ok(signature) +} + +pub(crate) fn verify_certificate_verify( + handshake_bodies: &[u8], + hash_algorithm: &SignatureHashAlgorithm, + remote_key_signature: &[u8], + raw_certificates: &[Vec], + insecure_verification: bool, +) -> Result<()> { + verify_signature( + handshake_bodies, + hash_algorithm, + remote_key_signature, + raw_certificates, + insecure_verification, + ) +} + +pub(crate) fn load_certs(raw_certificates: &[Vec]) -> Result>> { + if raw_certificates.is_empty() { + return Err(Error::ErrLengthMismatch); + } + + let mut certs = vec![]; + for raw_cert in raw_certificates { + let cert = CertificateDer::from(raw_cert.to_vec()); + certs.push(cert); + } + + Ok(certs) +} + +pub(crate) fn verify_client_cert( + raw_certificates: &[Vec], + cert_verifier: &Arc, +) -> Result>> { + let chains = load_certs(raw_certificates)?; + + let (end_entity, intermediates) = chains + .split_first() + .ok_or(Error::ErrClientCertificateRequired)?; + + match cert_verifier.verify_client_cert(end_entity, intermediates) { + Ok(_) => {} + Err(err) => return Err(Error::Other(err.to_string())), + }; + + Ok(chains) +} + +pub(crate) fn verify_server_cert( + raw_certificates: &[Vec], + cert_verifier: &Arc, + server_name: &str, +) -> Result>> { + let chains = load_certs(raw_certificates)?; + + let (end_entity, intermediates) = chains + .split_first() + .ok_or(Error::ErrServerMustHaveCertificate)?; + match cert_verifier.verify_server_cert(end_entity, intermediates, server_name) { + Ok(_) => {} + Err(err) => return Err(Error::Other(err.to_string())), + }; + + Ok(chains) +} + +pub(crate) fn generate_aead_additional_data(h: &RecordLayerHeader, payload_len: usize) -> Vec { + let mut additional_data = vec![0u8; 13]; + // SequenceNumber MUST be set first + // we only want uint48, clobbering an extra 2 (using uint64, rust doesn't have uint48) + additional_data[..8].copy_from_slice(&h.sequence_number.to_be_bytes()); + additional_data[..2].copy_from_slice(&h.epoch.to_be_bytes()); + additional_data[8] = h.content_type as u8; + additional_data[9] = h.protocol_version.major; + additional_data[10] = h.protocol_version.minor; + additional_data[11..].copy_from_slice(&(payload_len as u16).to_be_bytes()); + + additional_data +} + +#[cfg(test)] +mod test { + #[cfg(feature = "pem")] + use super::*; + + #[cfg(feature = "pem")] + #[test] + fn test_certificate_serialize_pem_and_from_pem() -> crate::error::Result<()> { + let cert = Certificate::generate_self_signed(vec!["webrtc.rs".to_owned()])?; + + let pem = cert.serialize_pem(); + let loaded_cert = Certificate::from_pem(&pem)?; + + assert_eq!(loaded_cert, cert); + + Ok(()) + } +} diff --git a/vendor/dtls/src/crypto/padding.rs b/vendor/dtls/src/crypto/padding.rs new file mode 100644 index 00000000..c10871a4 --- /dev/null +++ b/vendor/dtls/src/crypto/padding.rs @@ -0,0 +1,122 @@ +use cbc::cipher::block_padding::{PadType, RawPadding, UnpadError}; +use core::panic; + +pub enum DtlsPadding {} +/// Reference: RFC5246, 6.2.3.2 +impl RawPadding for DtlsPadding { + const TYPE: PadType = PadType::Reversible; + + fn raw_pad(block: &mut [u8], pos: usize) { + if pos >= block.len() { + panic!("`pos` is bigger or equal to block size"); + } + + let padding_length = block.len() - pos - 1; + if padding_length > 255 { + panic!("block size is too big for DTLS"); + } + + set(&mut block[pos..], padding_length as u8); + } + + fn raw_unpad(data: &[u8]) -> Result<&[u8], UnpadError> { + let padding_length = data.last().copied().unwrap_or(1) as usize; + if padding_length + 1 > data.len() { + return Err(UnpadError); + } + + let padding_begin = data.len() - padding_length - 1; + + if data[padding_begin..data.len() - 1] + .iter() + .any(|&byte| byte as usize != padding_length) + { + return Err(UnpadError); + } + + Ok(&data[0..padding_begin]) + } +} + +/// Sets all bytes in `dst` equal to `value` +#[inline(always)] +fn set(dst: &mut [u8], value: u8) { + // SAFETY: we overwrite valid memory behind `dst` + // note: loop is not used here because it produces + // unnecessary branch which tests for zero-length slices + unsafe { + core::ptr::write_bytes(dst.as_mut_ptr(), value, dst.len()); + } +} + +#[cfg(test)] +pub mod tests { + use rand::Rng; + + use super::*; + + #[test] + fn padding_length_is_amount_of_bytes_excluding_the_padding_length_itself() -> Result<(), ()> { + for original_length in 0..128 { + for padding_length in 0..(256 - original_length) { + let mut block = vec![0; original_length + padding_length + 1]; + rand::rng().fill(&mut block[0..original_length]); + let original = block[0..original_length].to_vec(); + DtlsPadding::raw_pad(&mut block, original_length); + + for byte in block[original_length..].iter() { + assert_eq!(*byte as usize, padding_length); + } + assert_eq!(block[0..original_length], original); + } + } + + Ok(()) + } + + #[test] + #[should_panic] + fn full_block_is_padding_error() { + for original_length in 0..256 { + let mut block = vec![0; original_length]; + DtlsPadding::raw_pad(&mut block, original_length); + } + } + + #[test] + #[should_panic] + fn padding_length_bigger_than_255_is_a_pad_error() { + let padding_length = 256; + for original_length in 0..128 { + let mut block = vec![0; original_length + padding_length + 1]; + DtlsPadding::raw_pad(&mut block, original_length); + } + } + + #[test] + fn empty_block_is_unpadding_error() { + let r = DtlsPadding::raw_unpad(&[]); + assert!(r.is_err()); + } + + #[test] + fn padding_too_big_for_block_is_unpadding_error() { + let r = DtlsPadding::raw_unpad(&[1]); + assert!(r.is_err()); + } + + #[test] + fn one_of_the_padding_bytes_with_value_different_than_padding_length_is_unpadding_error() { + for padding_length in 0..16 { + for invalid_byte in 0..padding_length { + let mut block = vec![0; padding_length + 1]; + DtlsPadding::raw_pad(&mut block, 0); + + assert_eq!(DtlsPadding::raw_unpad(&block).ok(), Some(&[][..])); + block[invalid_byte] = (padding_length - 1) as u8; + let r = DtlsPadding::raw_unpad(&block); + assert!(r.is_err()); + } + } + } +} diff --git a/vendor/dtls/src/curve/mod.rs b/vendor/dtls/src/curve/mod.rs new file mode 100644 index 00000000..dd48d5ff --- /dev/null +++ b/vendor/dtls/src/curve/mod.rs @@ -0,0 +1,17 @@ +pub mod named_curve; + +// https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-10 +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum EllipticCurveType { + NamedCurve = 0x03, + Unsupported, +} + +impl From for EllipticCurveType { + fn from(val: u8) -> Self { + match val { + 0x03 => EllipticCurveType::NamedCurve, + _ => EllipticCurveType::Unsupported, + } + } +} diff --git a/vendor/dtls/src/curve/named_curve.rs b/vendor/dtls/src/curve/named_curve.rs new file mode 100644 index 00000000..6d7d97f8 --- /dev/null +++ b/vendor/dtls/src/curve/named_curve.rs @@ -0,0 +1,83 @@ +use rand_core::OsRng; // requires 'getrandom' feature + +use crate::error::*; + +// https://www.iana.org/assignments/tls-parameters/tls-parameters.xml#tls-parameters-8 +#[repr(u16)] +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum NamedCurve { + Unsupported = 0x0000, + P256 = 0x0017, + P384 = 0x0018, + X25519 = 0x001d, +} + +impl From for NamedCurve { + fn from(val: u16) -> Self { + match val { + 0x0017 => NamedCurve::P256, + 0x0018 => NamedCurve::P384, + 0x001d => NamedCurve::X25519, + _ => NamedCurve::Unsupported, + } + } +} + +pub(crate) enum NamedCurvePrivateKey { + EphemeralSecretP256(p256::ecdh::EphemeralSecret), + EphemeralSecretP384(p384::ecdh::EphemeralSecret), + StaticSecretX25519(x25519_dalek::StaticSecret), +} + +pub struct NamedCurveKeypair { + pub(crate) curve: NamedCurve, + pub(crate) public_key: Vec, + pub(crate) private_key: NamedCurvePrivateKey, +} + +fn elliptic_curve_keypair(curve: NamedCurve) -> Result { + let (public_key, private_key) = match curve { + NamedCurve::P256 => { + let secret_key = p256::ecdh::EphemeralSecret::random(&mut OsRng); + let public_key = p256::EncodedPoint::from(secret_key.public_key()); + ( + public_key.as_bytes().to_vec(), + NamedCurvePrivateKey::EphemeralSecretP256(secret_key), + ) + } + NamedCurve::P384 => { + let secret_key = p384::ecdh::EphemeralSecret::random(&mut OsRng); + let public_key = p384::EncodedPoint::from(secret_key.public_key()); + ( + public_key.as_bytes().to_vec(), + NamedCurvePrivateKey::EphemeralSecretP384(secret_key), + ) + } + NamedCurve::X25519 => { + let secret_key = x25519_dalek::StaticSecret::random_from_rng(OsRng); + let public_key = x25519_dalek::PublicKey::from(&secret_key); + ( + public_key.as_bytes().to_vec(), + NamedCurvePrivateKey::StaticSecretX25519(secret_key), + ) + } + _ => return Err(Error::ErrInvalidNamedCurve), + }; + + Ok(NamedCurveKeypair { + curve, + public_key, + private_key, + }) +} + +impl NamedCurve { + pub fn generate_keypair(&self) -> Result { + match *self { + NamedCurve::X25519 => elliptic_curve_keypair(NamedCurve::X25519), + NamedCurve::P256 => elliptic_curve_keypair(NamedCurve::P256), + NamedCurve::P384 => elliptic_curve_keypair(NamedCurve::P384), + _ => Err(Error::ErrInvalidNamedCurve), + } + } +} diff --git a/vendor/dtls/src/error.rs b/vendor/dtls/src/error.rs new file mode 100644 index 00000000..a90d9fa8 --- /dev/null +++ b/vendor/dtls/src/error.rs @@ -0,0 +1,223 @@ +use std::io; +use std::string::FromUtf8Error; + +use thiserror::Error; +use tokio::sync::mpsc::error::SendError as MpscSendError; +use util::KeyingMaterialExporterError; + +pub type Result = std::result::Result; + +#[derive(Debug, Error, PartialEq)] +#[non_exhaustive] +pub enum Error { + #[error("conn is closed")] + ErrConnClosed, + #[error("read/write timeout")] + ErrDeadlineExceeded, + #[error("buffer is too small")] + ErrBufferTooSmall, + #[error("context is not supported for export_keying_material")] + ErrContextUnsupported, + #[error("packet is too short")] + ErrDtlspacketInvalidLength, + #[error("handshake is in progress")] + ErrHandshakeInProgress, + #[error("invalid content type")] + ErrInvalidContentType, + #[error("invalid mac")] + ErrInvalidMac, + #[error("packet length and declared length do not match")] + ErrInvalidPacketLength, + #[error("export_keying_material can not be used with a reserved label")] + ErrReservedExportKeyingMaterial, + #[error("client sent certificate verify but we have no certificate to verify")] + ErrCertificateVerifyNoCertificate, + #[error("client+server do not support any shared cipher suites")] + ErrCipherSuiteNoIntersection, + #[error("server hello can not be created without a cipher suite")] + ErrCipherSuiteUnset, + #[error("client sent certificate but did not verify it")] + ErrClientCertificateNotVerified, + #[error("server required client verification, but got none")] + ErrClientCertificateRequired, + #[error("server responded with SRTP Profile we do not support")] + ErrClientNoMatchingSrtpProfile, + #[error("client required Extended Master Secret extension, but server does not support it")] + ErrClientRequiredButNoServerEms, + #[error("server hello can not be created without a compression method")] + ErrCompressionMethodUnset, + #[error("client+server cookie does not match")] + ErrCookieMismatch, + #[error("cookie must not be longer then 255 bytes")] + ErrCookieTooLong, + #[error("PSK Identity Hint provided but PSK is nil")] + ErrIdentityNoPsk, + #[error("no certificate provided")] + ErrInvalidCertificate, + #[error("cipher spec invalid")] + ErrInvalidCipherSpec, + #[error("invalid or unknown cipher suite")] + ErrInvalidCipherSuite, + #[error("unable to determine if ClientKeyExchange is a public key or PSK Identity")] + ErrInvalidClientKeyExchange, + #[error("invalid or unknown compression method")] + ErrInvalidCompressionMethod, + #[error("ECDSA signature contained zero or negative values")] + ErrInvalidEcdsasignature, + #[error("invalid or unknown elliptic curve type")] + ErrInvalidEllipticCurveType, + #[error("invalid extension type")] + ErrInvalidExtensionType, + #[error("invalid hash algorithm")] + ErrInvalidHashAlgorithm, + #[error("invalid named curve")] + ErrInvalidNamedCurve, + #[error("invalid private key type")] + ErrInvalidPrivateKey, + #[error("named curve and private key type does not match")] + ErrNamedCurveAndPrivateKeyMismatch, + #[error("invalid server name format")] + ErrInvalidSniFormat, + #[error("invalid signature algorithm")] + ErrInvalidSignatureAlgorithm, + #[error("expected and actual key signature do not match")] + ErrKeySignatureMismatch, + #[error("Conn can not be created with a nil nextConn")] + ErrNilNextConn, + #[error("connection can not be created, no CipherSuites satisfy this Config")] + ErrNoAvailableCipherSuites, + #[error("connection can not be created, no SignatureScheme satisfy this Config")] + ErrNoAvailableSignatureSchemes, + #[error("no certificates configured")] + ErrNoCertificates, + #[error("no config provided")] + ErrNoConfigProvided, + #[error("client requested zero or more elliptic curves that are not supported by the server")] + ErrNoSupportedEllipticCurves, + #[error("unsupported protocol version")] + ErrUnsupportedProtocolVersion, + #[error("Certificate and PSK provided")] + ErrPskAndCertificate, + #[error("PSK and PSK Identity Hint must both be set for client")] + ErrPskAndIdentityMustBeSetForClient, + #[error("SRTP support was requested but server did not respond with use_srtp extension")] + ErrRequestedButNoSrtpExtension, + #[error("Certificate is mandatory for server")] + ErrServerMustHaveCertificate, + #[error("client requested SRTP but we have no matching profiles")] + ErrServerNoMatchingSrtpProfile, + #[error( + "server requires the Extended Master Secret extension, but the client does not support it" + )] + ErrServerRequiredButNoClientEms, + #[error("expected and actual verify data does not match")] + ErrVerifyDataMismatch, + #[error("handshake message unset, unable to marshal")] + ErrHandshakeMessageUnset, + #[error("invalid flight number")] + ErrInvalidFlight, + #[error("unable to generate key signature, unimplemented")] + ErrKeySignatureGenerateUnimplemented, + #[error("unable to verify key signature, unimplemented")] + ErrKeySignatureVerifyUnimplemented, + #[error("data length and declared length do not match")] + ErrLengthMismatch, + #[error("buffer not long enough to contain nonce")] + ErrNotEnoughRoomForNonce, + #[error("feature has not been implemented yet")] + ErrNotImplemented, + #[error("sequence number overflow")] + ErrSequenceNumberOverflow, + #[error("unable to marshal fragmented handshakes")] + ErrUnableToMarshalFragmented, + #[error("invalid state machine transition")] + ErrInvalidFsmTransition, + #[error("ApplicationData with epoch of 0")] + ErrApplicationDataEpochZero, + #[error("unhandled contentType")] + ErrUnhandledContextType, + #[error("context canceled")] + ErrContextCanceled, + #[error("empty fragment")] + ErrEmptyFragment, + #[error("Alert is Fatal or Close Notify")] + ErrAlertFatalOrClose, + + #[error( + "Fragment buffer overflow. New size {new_size} is greater than specified max {max_size}" + )] + ErrFragmentBufferOverflow { new_size: usize, max_size: usize }, + + #[error("{0}")] + Io(#[source] IoError), + #[error("{0}")] + Util(#[from] util::Error), + #[error("utf8: {0}")] + Utf8(#[from] FromUtf8Error), + #[error("{0}")] + Sec1(#[source] sec1::Error), + #[error("{0}")] + Aes(#[from] aes::cipher::InvalidLength), + #[error("{0}")] + P256(#[source] P256Error), + #[error("{0}")] + RcGen(#[from] rcgen::Error), + #[error("mpsc send: {0}")] + MpscSend(String), + #[error("keying material: {0}")] + KeyingMaterial(#[from] KeyingMaterialExporterError), + + /// Error parsing a given PEM string. + #[error("invalid PEM: {0}")] + InvalidPEM(String), + + #[allow(non_camel_case_types)] + #[error("{0}")] + Other(String), +} + +#[derive(Debug, Error)] +#[error("io error: {0}")] +pub struct IoError(#[from] pub io::Error); + +// Workaround for wanting PartialEq for io::Error. +impl PartialEq for IoError { + fn eq(&self, other: &Self) -> bool { + self.0.kind() == other.0.kind() + } +} + +impl From for Error { + fn from(e: io::Error) -> Self { + Error::Io(IoError(e)) + } +} + +impl From for Error { + fn from(e: sec1::Error) -> Self { + Error::Sec1(e) + } +} + +#[derive(Debug, Error)] +#[error("{0}")] +pub struct P256Error(#[source] p256::elliptic_curve::Error); + +impl PartialEq for P256Error { + fn eq(&self, _: &Self) -> bool { + false + } +} + +impl From for Error { + fn from(e: p256::elliptic_curve::Error) -> Self { + Error::P256(P256Error(e)) + } +} + +// Because Tokio SendError is parameterized, we sadly lose the backtrace. +impl From> for Error { + fn from(e: MpscSendError) -> Self { + Error::MpscSend(e.to_string()) + } +} diff --git a/vendor/dtls/src/extension/extension_server_name.rs b/vendor/dtls/src/extension/extension_server_name.rs new file mode 100644 index 00000000..92fd259b --- /dev/null +++ b/vendor/dtls/src/extension/extension_server_name.rs @@ -0,0 +1,56 @@ +#[cfg(test)] +mod extension_server_name_test; + +use std::io::{Read, Write}; + +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; + +use super::*; + +const EXTENSION_SERVER_NAME_TYPE_DNSHOST_NAME: u8 = 0; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ExtensionServerName { + pub(crate) server_name: String, +} + +impl ExtensionServerName { + pub fn extension_value(&self) -> ExtensionValue { + ExtensionValue::ServerName + } + + pub fn size(&self) -> usize { + //TODO: check how to do cryptobyte? + 2 + 2 + 1 + 2 + self.server_name.len() + } + + pub fn marshal(&self, writer: &mut W) -> Result<()> { + //TODO: check how to do cryptobyte? + writer.write_u16::(2 + 1 + 2 + self.server_name.len() as u16)?; + writer.write_u16::(1 + 2 + self.server_name.len() as u16)?; + writer.write_u8(EXTENSION_SERVER_NAME_TYPE_DNSHOST_NAME)?; + writer.write_u16::(self.server_name.len() as u16)?; + writer.write_all(self.server_name.as_bytes())?; + + Ok(writer.flush()?) + } + + pub fn unmarshal(reader: &mut R) -> Result { + //TODO: check how to do cryptobyte? + let _ = reader.read_u16::()? as usize; + let _ = reader.read_u16::()? as usize; + + let name_type = reader.read_u8()?; + if name_type != EXTENSION_SERVER_NAME_TYPE_DNSHOST_NAME { + return Err(Error::ErrInvalidSniFormat); + } + + let buf_len = reader.read_u16::()? as usize; + let mut buf: Vec = vec![0u8; buf_len]; + reader.read_exact(&mut buf)?; + + let server_name = String::from_utf8(buf)?; + + Ok(ExtensionServerName { server_name }) + } +} diff --git a/vendor/dtls/src/extension/extension_server_name/extension_server_name_test.rs b/vendor/dtls/src/extension/extension_server_name/extension_server_name_test.rs new file mode 100644 index 00000000..4cfb7a1a --- /dev/null +++ b/vendor/dtls/src/extension/extension_server_name/extension_server_name_test.rs @@ -0,0 +1,26 @@ +use std::io::{BufReader, BufWriter}; + +use super::*; + +#[test] +fn test_extension_server_name() -> Result<()> { + let extension = ExtensionServerName { + server_name: "test.domain".to_owned(), + }; + + let mut raw = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); + extension.marshal(&mut writer)?; + } + + let mut reader = BufReader::new(raw.as_slice()); + let new_extension = ExtensionServerName::unmarshal(&mut reader)?; + + assert_eq!( + new_extension, extension, + "extensionServerName marshal: got {new_extension:?} expected {extension:?}", + ); + + Ok(()) +} diff --git a/vendor/dtls/src/extension/extension_supported_elliptic_curves.rs b/vendor/dtls/src/extension/extension_supported_elliptic_curves.rs new file mode 100644 index 00000000..64fe8084 --- /dev/null +++ b/vendor/dtls/src/extension/extension_supported_elliptic_curves.rs @@ -0,0 +1,50 @@ +#[cfg(test)] +mod extension_supported_elliptic_curves_test; + +use super::*; +use crate::curve::named_curve::*; + +const EXTENSION_SUPPORTED_GROUPS_HEADER_SIZE: usize = 6; + +/// ## Specifications +/// +/// * [RFC 8422 §5.1.1] +/// +/// [RFC 8422 §5.1.1]: https://tools.ietf.org/html/rfc8422#section-5.1.1 +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ExtensionSupportedEllipticCurves { + pub elliptic_curves: Vec, +} + +impl ExtensionSupportedEllipticCurves { + pub fn extension_value(&self) -> ExtensionValue { + ExtensionValue::SupportedEllipticCurves + } + + pub fn size(&self) -> usize { + 2 + 2 + self.elliptic_curves.len() * 2 + } + + pub fn marshal(&self, writer: &mut W) -> Result<()> { + writer.write_u16::(2 + 2 * self.elliptic_curves.len() as u16)?; + writer.write_u16::(2 * self.elliptic_curves.len() as u16)?; + for v in &self.elliptic_curves { + writer.write_u16::(*v as u16)?; + } + + Ok(writer.flush()?) + } + + pub fn unmarshal(reader: &mut R) -> Result { + let _ = reader.read_u16::()?; + + let group_count = reader.read_u16::()? as usize / 2; + let mut elliptic_curves = vec![]; + for _ in 0..group_count { + let elliptic_curve = reader.read_u16::()?.into(); + elliptic_curves.push(elliptic_curve); + } + + Ok(ExtensionSupportedEllipticCurves { elliptic_curves }) + } +} diff --git a/vendor/dtls/src/extension/extension_supported_elliptic_curves/extension_supported_elliptic_curves_test.rs b/vendor/dtls/src/extension/extension_supported_elliptic_curves/extension_supported_elliptic_curves_test.rs new file mode 100644 index 00000000..2bcf70c1 --- /dev/null +++ b/vendor/dtls/src/extension/extension_supported_elliptic_curves/extension_supported_elliptic_curves_test.rs @@ -0,0 +1,32 @@ +use std::io::{BufReader, BufWriter}; + +use super::*; + +#[test] +fn test_extension_supported_groups() -> Result<()> { + let raw_supported_groups = vec![0x0, 0x4, 0x0, 0x2, 0x0, 0x1d]; // 0x0, 0xa, + let parsed_supported_groups = ExtensionSupportedEllipticCurves { + elliptic_curves: vec![NamedCurve::X25519], + }; + + let mut raw = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); + parsed_supported_groups.marshal(&mut writer)?; + } + + assert_eq!( + raw, raw_supported_groups, + "extensionSupportedGroups marshal: got {raw:?}, want {raw_supported_groups:?}" + ); + + let mut reader = BufReader::new(raw.as_slice()); + let new_supported_groups = ExtensionSupportedEllipticCurves::unmarshal(&mut reader)?; + + assert_eq!( + new_supported_groups, parsed_supported_groups, + "extensionSupportedGroups unmarshal: got {new_supported_groups:?}, want {parsed_supported_groups:?}" + ); + + Ok(()) +} diff --git a/vendor/dtls/src/extension/extension_supported_point_formats.rs b/vendor/dtls/src/extension/extension_supported_point_formats.rs new file mode 100644 index 00000000..60e5df94 --- /dev/null +++ b/vendor/dtls/src/extension/extension_supported_point_formats.rs @@ -0,0 +1,53 @@ +#[cfg(test)] +mod extension_supported_point_formats_test; + +use super::*; + +const EXTENSION_SUPPORTED_POINT_FORMATS_SIZE: usize = 5; + +pub type EllipticCurvePointFormat = u8; + +pub const ELLIPTIC_CURVE_POINT_FORMAT_UNCOMPRESSED: EllipticCurvePointFormat = 0; + +/// ## Specifications +/// +/// * [RFC 4492 §5.1.2] +/// +/// [RFC 4492 §5.1.2]: https://tools.ietf.org/html/rfc4492#section-5.1.2 +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ExtensionSupportedPointFormats { + pub(crate) point_formats: Vec, +} + +impl ExtensionSupportedPointFormats { + pub fn extension_value(&self) -> ExtensionValue { + ExtensionValue::SupportedPointFormats + } + + pub fn size(&self) -> usize { + 2 + 1 + self.point_formats.len() + } + + pub fn marshal(&self, writer: &mut W) -> Result<()> { + writer.write_u16::(1 + self.point_formats.len() as u16)?; + writer.write_u8(self.point_formats.len() as u8)?; + for v in &self.point_formats { + writer.write_u8(*v)?; + } + + Ok(writer.flush()?) + } + + pub fn unmarshal(reader: &mut R) -> Result { + let _ = reader.read_u16::()?; + + let point_format_count = reader.read_u8()? as usize; + let mut point_formats = vec![]; + for _ in 0..point_format_count { + let point_format = reader.read_u8()?; + point_formats.push(point_format); + } + + Ok(ExtensionSupportedPointFormats { point_formats }) + } +} diff --git a/vendor/dtls/src/extension/extension_supported_point_formats/extension_supported_point_formats_test.rs b/vendor/dtls/src/extension/extension_supported_point_formats/extension_supported_point_formats_test.rs new file mode 100644 index 00000000..e624a98a --- /dev/null +++ b/vendor/dtls/src/extension/extension_supported_point_formats/extension_supported_point_formats_test.rs @@ -0,0 +1,33 @@ +use std::io::{BufReader, BufWriter}; + +use super::*; + +#[test] +fn test_extension_supported_point_formats() -> Result<()> { + let raw_extension_supported_point_formats = vec![0x00, 0x02, 0x01, 0x00]; // 0x00, 0x0b, + let parsed_extension_supported_point_formats = ExtensionSupportedPointFormats { + point_formats: vec![ELLIPTIC_CURVE_POINT_FORMAT_UNCOMPRESSED], + }; + + let mut raw = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); + parsed_extension_supported_point_formats.marshal(&mut writer)?; + } + + assert_eq!( + raw, raw_extension_supported_point_formats, + "extensionSupportedPointFormats marshal: got {raw:?}, want {raw_extension_supported_point_formats:?}" + ); + + let mut reader = BufReader::new(raw.as_slice()); + let new_extension_supported_point_formats = + ExtensionSupportedPointFormats::unmarshal(&mut reader)?; + + assert_eq!( + new_extension_supported_point_formats, parsed_extension_supported_point_formats, + "extensionSupportedPointFormats unmarshal: got {new_extension_supported_point_formats:?}, want {parsed_extension_supported_point_formats:?}" + ); + + Ok(()) +} diff --git a/vendor/dtls/src/extension/extension_supported_signature_algorithms.rs b/vendor/dtls/src/extension/extension_supported_signature_algorithms.rs new file mode 100644 index 00000000..24ca158d --- /dev/null +++ b/vendor/dtls/src/extension/extension_supported_signature_algorithms.rs @@ -0,0 +1,54 @@ +#[cfg(test)] +mod extension_supported_signature_algorithms_test; + +use super::*; +use crate::signature_hash_algorithm::*; + +const EXTENSION_SUPPORTED_SIGNATURE_ALGORITHMS_HEADER_SIZE: usize = 6; + +/// ## Specifications +/// +/// * [RFC 5246 §7.4.1.4.1] +/// +/// [RFC 5246 §7.4.1.4.1]: https://tools.ietf.org/html/rfc5246#section-7.4.1.4.1 +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ExtensionSupportedSignatureAlgorithms { + pub(crate) signature_hash_algorithms: Vec, +} + +impl ExtensionSupportedSignatureAlgorithms { + pub fn extension_value(&self) -> ExtensionValue { + ExtensionValue::SupportedSignatureAlgorithms + } + + pub fn size(&self) -> usize { + 2 + 2 + self.signature_hash_algorithms.len() * 2 + } + + pub fn marshal(&self, writer: &mut W) -> Result<()> { + writer.write_u16::(2 + 2 * self.signature_hash_algorithms.len() as u16)?; + writer.write_u16::(2 * self.signature_hash_algorithms.len() as u16)?; + for v in &self.signature_hash_algorithms { + writer.write_u8(v.hash as u8)?; + writer.write_u8(v.signature as u8)?; + } + + Ok(writer.flush()?) + } + + pub fn unmarshal(reader: &mut R) -> Result { + let _ = reader.read_u16::()?; + + let algorithm_count = reader.read_u16::()? as usize / 2; + let mut signature_hash_algorithms = vec![]; + for _ in 0..algorithm_count { + let hash = reader.read_u8()?.into(); + let signature = reader.read_u8()?.into(); + signature_hash_algorithms.push(SignatureHashAlgorithm { hash, signature }); + } + + Ok(ExtensionSupportedSignatureAlgorithms { + signature_hash_algorithms, + }) + } +} diff --git a/vendor/dtls/src/extension/extension_supported_signature_algorithms/extension_supported_signature_algorithms_test.rs b/vendor/dtls/src/extension/extension_supported_signature_algorithms/extension_supported_signature_algorithms_test.rs new file mode 100644 index 00000000..e531fee0 --- /dev/null +++ b/vendor/dtls/src/extension/extension_supported_signature_algorithms/extension_supported_signature_algorithms_test.rs @@ -0,0 +1,48 @@ +use std::io::{BufReader, BufWriter}; + +use super::*; + +#[test] +fn test_extension_supported_signature_algorithms() -> Result<()> { + let raw_extension_supported_signature_algorithms = + vec![0x00, 0x08, 0x00, 0x06, 0x04, 0x03, 0x05, 0x03, 0x06, 0x03]; //0x00, 0x0d, + let parsed_extension_supported_signature_algorithms = ExtensionSupportedSignatureAlgorithms { + signature_hash_algorithms: vec![ + SignatureHashAlgorithm { + hash: HashAlgorithm::Sha256, + signature: SignatureAlgorithm::Ecdsa, + }, + SignatureHashAlgorithm { + hash: HashAlgorithm::Sha384, + signature: SignatureAlgorithm::Ecdsa, + }, + SignatureHashAlgorithm { + hash: HashAlgorithm::Sha512, + signature: SignatureAlgorithm::Ecdsa, + }, + ], + }; + + let mut raw = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); + parsed_extension_supported_signature_algorithms.marshal(&mut writer)?; + } + + assert_eq!( + raw, raw_extension_supported_signature_algorithms, + "extensionSupportedSignatureAlgorithms marshal: got {raw:?}, want {raw_extension_supported_signature_algorithms:?}" + ); + + let mut reader = BufReader::new(raw.as_slice()); + let new_extension_supported_signature_algorithms = + ExtensionSupportedSignatureAlgorithms::unmarshal(&mut reader)?; + + assert_eq!( + new_extension_supported_signature_algorithms, + parsed_extension_supported_signature_algorithms, + "extensionSupportedSignatureAlgorithms unmarshal: got {new_extension_supported_signature_algorithms:?}, want {parsed_extension_supported_signature_algorithms:?}" + ); + + Ok(()) +} diff --git a/vendor/dtls/src/extension/extension_use_extended_master_secret.rs b/vendor/dtls/src/extension/extension_use_extended_master_secret.rs new file mode 100644 index 00000000..d6a1d0a4 --- /dev/null +++ b/vendor/dtls/src/extension/extension_use_extended_master_secret.rs @@ -0,0 +1,39 @@ +#[cfg(test)] +mod extension_use_extended_master_secret_test; + +use super::*; + +const EXTENSION_USE_EXTENDED_MASTER_SECRET_HEADER_SIZE: usize = 4; + +/// ## Specifications +/// +/// * [RFC 8422] +/// +/// [RFC 8422]: https://tools.ietf.org/html/rfc8422 +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ExtensionUseExtendedMasterSecret { + pub(crate) supported: bool, +} + +impl ExtensionUseExtendedMasterSecret { + pub fn extension_value(&self) -> ExtensionValue { + ExtensionValue::UseExtendedMasterSecret + } + + pub fn size(&self) -> usize { + 2 + } + + pub fn marshal(&self, writer: &mut W) -> Result<()> { + // length + writer.write_u16::(0)?; + + Ok(writer.flush()?) + } + + pub fn unmarshal(reader: &mut R) -> Result { + let _ = reader.read_u16::()?; + + Ok(ExtensionUseExtendedMasterSecret { supported: true }) + } +} diff --git a/vendor/dtls/src/extension/extension_use_extended_master_secret/extension_use_extended_master_secret_test.rs b/vendor/dtls/src/extension/extension_use_extended_master_secret/extension_use_extended_master_secret_test.rs new file mode 100644 index 00000000..d2b0dd42 --- /dev/null +++ b/vendor/dtls/src/extension/extension_use_extended_master_secret/extension_use_extended_master_secret_test.rs @@ -0,0 +1,32 @@ +use std::io::{BufReader, BufWriter}; + +use super::*; + +#[test] +fn test_extension_use_extended_master_secret() -> Result<()> { + let raw_extension_use_extended_master_secret = vec![0x00, 0x00]; + let parsed_extension_use_extended_master_secret = + ExtensionUseExtendedMasterSecret { supported: true }; + + let mut raw = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); + parsed_extension_use_extended_master_secret.marshal(&mut writer)?; + } + + assert_eq!( + raw, raw_extension_use_extended_master_secret, + "extension_use_extended_master_secret marshal: got {raw:?}, want {raw_extension_use_extended_master_secret:?}" + ); + + let mut reader = BufReader::new(raw.as_slice()); + let new_extension_use_extended_master_secret = + ExtensionUseExtendedMasterSecret::unmarshal(&mut reader)?; + + assert_eq!( + new_extension_use_extended_master_secret, parsed_extension_use_extended_master_secret, + "extension_use_extended_master_secret unmarshal: got {new_extension_use_extended_master_secret:?}, want {parsed_extension_use_extended_master_secret:?}" + ); + + Ok(()) +} diff --git a/vendor/dtls/src/extension/extension_use_srtp.rs b/vendor/dtls/src/extension/extension_use_srtp.rs new file mode 100644 index 00000000..5b2dd8b0 --- /dev/null +++ b/vendor/dtls/src/extension/extension_use_srtp.rs @@ -0,0 +1,88 @@ +#[cfg(test)] +mod extension_use_srtp_test; + +use super::*; + +// SRTPProtectionProfile defines the parameters and options that are in effect for the SRTP processing +/// ## Specifications +/// +/// * [RFC 5764 §4.1.2] +/// +/// [RFC 5764 §4.1.2]: https://tools.ietf.org/html/rfc5764#section-4.1.2 +#[allow(non_camel_case_types)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum SrtpProtectionProfile { + Srtp_Aes128_Cm_Hmac_Sha1_80 = 0x0001, + Srtp_Aes128_Cm_Hmac_Sha1_32 = 0x0002, + Srtp_Aead_Aes_128_Gcm = 0x0007, + Srtp_Aead_Aes_256_Gcm = 0x0008, + Unsupported, +} + +impl From for SrtpProtectionProfile { + fn from(val: u16) -> Self { + match val { + 0x0001 => SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80, + 0x0002 => SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_32, + 0x0007 => SrtpProtectionProfile::Srtp_Aead_Aes_128_Gcm, + 0x0008 => SrtpProtectionProfile::Srtp_Aead_Aes_256_Gcm, + _ => SrtpProtectionProfile::Unsupported, + } + } +} + +const EXTENSION_USE_SRTPHEADER_SIZE: usize = 6; + +/// ## Specifications +/// +/// * [RFC 8422] +/// +/// [RFC 8422]: https://tools.ietf.org/html/rfc8422 +#[allow(non_camel_case_types)] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ExtensionUseSrtp { + pub(crate) protection_profiles: Vec, +} + +impl ExtensionUseSrtp { + pub fn extension_value(&self) -> ExtensionValue { + ExtensionValue::UseSrtp + } + + pub fn size(&self) -> usize { + 2 + 2 + self.protection_profiles.len() * 2 + 1 + } + + pub fn marshal(&self, writer: &mut W) -> Result<()> { + writer.write_u16::( + 2 + /* MKI Length */ 1 + 2 * self.protection_profiles.len() as u16, + )?; + writer.write_u16::(2 * self.protection_profiles.len() as u16)?; + for v in &self.protection_profiles { + writer.write_u16::(*v as u16)?; + } + + /* MKI Length */ + writer.write_u8(0x00)?; + + Ok(writer.flush()?) + } + + pub fn unmarshal(reader: &mut R) -> Result { + let _ = reader.read_u16::()?; + + let profile_count = reader.read_u16::()? as usize / 2; + let mut protection_profiles = vec![]; + for _ in 0..profile_count { + let protection_profile = reader.read_u16::()?.into(); + protection_profiles.push(protection_profile); + } + + /* MKI Length */ + let _ = reader.read_u8()?; + + Ok(ExtensionUseSrtp { + protection_profiles, + }) + } +} diff --git a/vendor/dtls/src/extension/extension_use_srtp/extension_use_srtp_test.rs b/vendor/dtls/src/extension/extension_use_srtp/extension_use_srtp_test.rs new file mode 100644 index 00000000..c58ba56a --- /dev/null +++ b/vendor/dtls/src/extension/extension_use_srtp/extension_use_srtp_test.rs @@ -0,0 +1,32 @@ +use std::io::{BufReader, BufWriter}; + +use super::*; + +#[test] +fn test_extension_use_srtp() -> Result<()> { + let raw_use_srtp = vec![0x00, 0x05, 0x00, 0x02, 0x00, 0x01, 0x00]; //0x00, 0x0e, + let parsed_use_srtp = ExtensionUseSrtp { + protection_profiles: vec![SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80], + }; + + let mut raw = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); + parsed_use_srtp.marshal(&mut writer)?; + } + + assert_eq!( + raw, raw_use_srtp, + "extensionUseSRTP marshal: got {raw:?}, want {raw_use_srtp:?}" + ); + + let mut reader = BufReader::new(raw.as_slice()); + let new_use_srtp = ExtensionUseSrtp::unmarshal(&mut reader)?; + + assert_eq!( + new_use_srtp, parsed_use_srtp, + "extensionUseSRTP unmarshal: got {new_use_srtp:?}, want {parsed_use_srtp:?}" + ); + + Ok(()) +} diff --git a/vendor/dtls/src/extension/mod.rs b/vendor/dtls/src/extension/mod.rs new file mode 100644 index 00000000..20300381 --- /dev/null +++ b/vendor/dtls/src/extension/mod.rs @@ -0,0 +1,130 @@ +pub mod extension_server_name; +pub mod extension_supported_elliptic_curves; +pub mod extension_supported_point_formats; +pub mod extension_supported_signature_algorithms; +pub mod extension_use_extended_master_secret; +pub mod extension_use_srtp; +pub mod renegotiation_info; + +use std::io::{Read, Write}; + +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use extension_server_name::*; +use extension_supported_elliptic_curves::*; +use extension_supported_point_formats::*; +use extension_supported_signature_algorithms::*; +use extension_use_extended_master_secret::*; +use extension_use_srtp::*; + +use crate::error::*; +use crate::extension::renegotiation_info::ExtensionRenegotiationInfo; + +// https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ExtensionValue { + ServerName = 0, + SupportedEllipticCurves = 10, + SupportedPointFormats = 11, + SupportedSignatureAlgorithms = 13, + UseSrtp = 14, + UseExtendedMasterSecret = 23, + RenegotiationInfo = 65281, + Unsupported, +} + +impl From for ExtensionValue { + fn from(val: u16) -> Self { + match val { + 0 => ExtensionValue::ServerName, + 10 => ExtensionValue::SupportedEllipticCurves, + 11 => ExtensionValue::SupportedPointFormats, + 13 => ExtensionValue::SupportedSignatureAlgorithms, + 14 => ExtensionValue::UseSrtp, + 23 => ExtensionValue::UseExtendedMasterSecret, + 65281 => ExtensionValue::RenegotiationInfo, + _ => ExtensionValue::Unsupported, + } + } +} + +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum Extension { + ServerName(ExtensionServerName), + SupportedEllipticCurves(ExtensionSupportedEllipticCurves), + SupportedPointFormats(ExtensionSupportedPointFormats), + SupportedSignatureAlgorithms(ExtensionSupportedSignatureAlgorithms), + UseSrtp(ExtensionUseSrtp), + UseExtendedMasterSecret(ExtensionUseExtendedMasterSecret), + RenegotiationInfo(ExtensionRenegotiationInfo), +} + +impl Extension { + pub fn extension_value(&self) -> ExtensionValue { + match self { + Extension::ServerName(ext) => ext.extension_value(), + Extension::SupportedEllipticCurves(ext) => ext.extension_value(), + Extension::SupportedPointFormats(ext) => ext.extension_value(), + Extension::SupportedSignatureAlgorithms(ext) => ext.extension_value(), + Extension::UseSrtp(ext) => ext.extension_value(), + Extension::UseExtendedMasterSecret(ext) => ext.extension_value(), + Extension::RenegotiationInfo(ext) => ext.extension_value(), + } + } + + pub fn size(&self) -> usize { + let mut len = 2; + + len += match self { + Extension::ServerName(ext) => ext.size(), + Extension::SupportedEllipticCurves(ext) => ext.size(), + Extension::SupportedPointFormats(ext) => ext.size(), + Extension::SupportedSignatureAlgorithms(ext) => ext.size(), + Extension::UseSrtp(ext) => ext.size(), + Extension::UseExtendedMasterSecret(ext) => ext.size(), + Extension::RenegotiationInfo(ext) => ext.size(), + }; + + len + } + + pub fn marshal(&self, writer: &mut W) -> Result<()> { + writer.write_u16::(self.extension_value() as u16)?; + match self { + Extension::ServerName(ext) => ext.marshal(writer), + Extension::SupportedEllipticCurves(ext) => ext.marshal(writer), + Extension::SupportedPointFormats(ext) => ext.marshal(writer), + Extension::SupportedSignatureAlgorithms(ext) => ext.marshal(writer), + Extension::UseSrtp(ext) => ext.marshal(writer), + Extension::UseExtendedMasterSecret(ext) => ext.marshal(writer), + Extension::RenegotiationInfo(ext) => ext.marshal(writer), + } + } + + pub fn unmarshal(reader: &mut R) -> Result { + let extension_value: ExtensionValue = reader.read_u16::()?.into(); + match extension_value { + ExtensionValue::ServerName => Ok(Extension::ServerName( + ExtensionServerName::unmarshal(reader)?, + )), + ExtensionValue::SupportedEllipticCurves => Ok(Extension::SupportedEllipticCurves( + ExtensionSupportedEllipticCurves::unmarshal(reader)?, + )), + ExtensionValue::SupportedPointFormats => Ok(Extension::SupportedPointFormats( + ExtensionSupportedPointFormats::unmarshal(reader)?, + )), + ExtensionValue::SupportedSignatureAlgorithms => { + Ok(Extension::SupportedSignatureAlgorithms( + ExtensionSupportedSignatureAlgorithms::unmarshal(reader)?, + )) + } + ExtensionValue::UseSrtp => Ok(Extension::UseSrtp(ExtensionUseSrtp::unmarshal(reader)?)), + ExtensionValue::UseExtendedMasterSecret => Ok(Extension::UseExtendedMasterSecret( + ExtensionUseExtendedMasterSecret::unmarshal(reader)?, + )), + ExtensionValue::RenegotiationInfo => Ok(Extension::RenegotiationInfo( + ExtensionRenegotiationInfo::unmarshal(reader)?, + )), + _ => Err(Error::ErrInvalidExtensionType), + } + } +} diff --git a/vendor/dtls/src/extension/renegotiation_info.rs b/vendor/dtls/src/extension/renegotiation_info.rs new file mode 100644 index 00000000..80ebbbec --- /dev/null +++ b/vendor/dtls/src/extension/renegotiation_info.rs @@ -0,0 +1,53 @@ +#[cfg(test)] +mod renegotiation_info_test; + +use super::*; +use crate::error::Error::ErrInvalidPacketLength; + +const RENEGOTIATION_INFO_HEADER_SIZE: usize = 5; + +/// RenegotiationInfo allows a Client/Server to +/// communicate their renegotiation support +/// +/// ## Specifications +/// +/// * [RFC 5746] +/// +/// [RFC 5746]: https://tools.ietf.org/html/rfc5746 +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ExtensionRenegotiationInfo { + pub(crate) renegotiated_connection: u8, +} + +impl ExtensionRenegotiationInfo { + // TypeValue returns the extension TypeValue + pub fn extension_value(&self) -> ExtensionValue { + ExtensionValue::RenegotiationInfo + } + + pub fn size(&self) -> usize { + 3 + } + + /// marshal encodes the extension + pub fn marshal(&self, writer: &mut W) -> Result<()> { + writer.write_u16::(1)?; //length + writer.write_u8(self.renegotiated_connection)?; + + Ok(writer.flush()?) + } + + /// Unmarshal populates the extension from encoded data + pub fn unmarshal(reader: &mut R) -> Result { + let l = reader.read_u16::()?; //length + if l != 1 { + return Err(ErrInvalidPacketLength); + } + + let renegotiated_connection = reader.read_u8()?; + + Ok(ExtensionRenegotiationInfo { + renegotiated_connection, + }) + } +} diff --git a/vendor/dtls/src/extension/renegotiation_info/renegotiation_info_test.rs b/vendor/dtls/src/extension/renegotiation_info/renegotiation_info_test.rs new file mode 100644 index 00000000..5266a94c --- /dev/null +++ b/vendor/dtls/src/extension/renegotiation_info/renegotiation_info_test.rs @@ -0,0 +1,26 @@ +use std::io::{BufReader, BufWriter}; + +use super::*; + +#[test] +fn test_renegotiation_info() -> Result<()> { + let extension = ExtensionRenegotiationInfo { + renegotiated_connection: 0, + }; + + let mut raw = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); + extension.marshal(&mut writer)?; + } + + let mut reader = BufReader::new(raw.as_slice()); + let new_extension = ExtensionRenegotiationInfo::unmarshal(&mut reader)?; + + assert_eq!( + new_extension.renegotiated_connection, + extension.renegotiated_connection + ); + + Ok(()) +} diff --git a/vendor/dtls/src/flight/flight0.rs b/vendor/dtls/src/flight/flight0.rs new file mode 100644 index 00000000..55805daa --- /dev/null +++ b/vendor/dtls/src/flight/flight0.rs @@ -0,0 +1,209 @@ +use std::fmt; +use std::sync::atomic::Ordering; + +use async_trait::async_trait; +use rand::Rng; + +use super::flight2::*; +use super::*; +use crate::config::*; +use crate::conn::*; +use crate::curve::named_curve::NamedCurve; +use crate::error::Error; +use crate::extension::*; +use crate::handshake::*; +use crate::record_layer::record_layer_header::*; +use crate::*; + +#[derive(Debug, PartialEq)] +pub(crate) struct Flight0; + +impl fmt::Display for Flight0 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Flight 0") + } +} + +#[async_trait] +impl Flight for Flight0 { + async fn parse( + &self, + _tx: &mut mpsc::Sender>, + state: &mut State, + cache: &HandshakeCache, + cfg: &HandshakeConfig, + ) -> Result, (Option, Option)> { + let (seq, msgs) = match cache + .full_pull_map( + 0, + &[HandshakeCachePullRule { + typ: HandshakeType::ClientHello, + epoch: cfg.initial_epoch, + is_client: true, + optional: false, + }], + ) + .await + { + Ok((seq, msgs)) => (seq, msgs), + Err(_) => return Err((None, None)), + }; + + state.handshake_recv_sequence = seq; + + if let Some(message) = msgs.get(&HandshakeType::ClientHello) { + // Validate type + let client_hello = match message { + HandshakeMessage::ClientHello(client_hello) => client_hello, + _ => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + None, + )) + } + }; + + if client_hello.version != PROTOCOL_VERSION1_2 { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::ProtocolVersion, + }), + Some(Error::ErrUnsupportedProtocolVersion), + )); + } + + state.remote_random = client_hello.random.clone(); + + if let Ok(id) = + find_matching_cipher_suite(&client_hello.cipher_suites, &cfg.local_cipher_suites) + { + if let Ok(cipher_suite) = cipher_suite_for_id(id) { + log::debug!( + "[handshake:{}] use cipher suite: {}", + srv_cli_str(state.is_client), + cipher_suite.to_string() + ); + let mut cs = state.cipher_suite.lock().await; + *cs = Some(cipher_suite); + } + } else { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InsufficientSecurity, + }), + Some(Error::ErrCipherSuiteNoIntersection), + )); + } + + for extension in &client_hello.extensions { + match extension { + Extension::SupportedEllipticCurves(e) => { + if e.elliptic_curves.is_empty() { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InsufficientSecurity, + }), + Some(Error::ErrNoSupportedEllipticCurves), + )); + } + for curve in e.elliptic_curves.iter() { + if curve != &NamedCurve::Unsupported { + state.named_curve = *curve; + break; + } + } + } + Extension::UseSrtp(e) => { + if let Ok(profile) = find_matching_srtp_profile( + &e.protection_profiles, + &cfg.local_srtp_protection_profiles, + ) { + state.srtp_protection_profile = profile; + } else { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InsufficientSecurity, + }), + Some(Error::ErrServerNoMatchingSrtpProfile), + )); + } + } + Extension::UseExtendedMasterSecret(_) => { + if cfg.extended_master_secret != ExtendedMasterSecretType::Disable { + state.extended_master_secret = true; + } + } + Extension::ServerName(e) => { + state.server_name.clone_from(&e.server_name); // remote server name + } + _ => {} + } + } + + if cfg.extended_master_secret == ExtendedMasterSecretType::Require + && !state.extended_master_secret + { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InsufficientSecurity, + }), + Some(Error::ErrServerRequiredButNoClientEms), + )); + } + + if state.local_keypair.is_none() { + state.local_keypair = match state.named_curve.generate_keypair() { + Ok(local_keypar) => Some(local_keypar), + Err(err) => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::IllegalParameter, + }), + Some(err), + )) + } + }; + } + + Ok(Box::new(Flight2 {})) + } else { + Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + None, + )) + } + } + + async fn generate( + &self, + state: &mut State, + _cache: &HandshakeCache, + _cfg: &HandshakeConfig, + ) -> Result, (Option, Option)> { + // Initialize + state.cookie = vec![0; COOKIE_LENGTH]; + rand::rng().fill(state.cookie.as_mut_slice()); + + //TODO: figure out difference between golang's atom store and rust atom store + let zero_epoch = 0; + state.local_epoch.store(zero_epoch, Ordering::SeqCst); + state.remote_epoch.store(zero_epoch, Ordering::SeqCst); + + state.named_curve = DEFAULT_NAMED_CURVE; + state.local_random.populate(); + + Ok(vec![]) + } +} diff --git a/vendor/dtls/src/flight/flight1.rs b/vendor/dtls/src/flight/flight1.rs new file mode 100644 index 00000000..0cb37e62 --- /dev/null +++ b/vendor/dtls/src/flight/flight1.rs @@ -0,0 +1,193 @@ +use std::fmt; +use std::sync::atomic::Ordering; + +use async_trait::async_trait; + +use super::flight3::*; +use super::*; +use crate::compression_methods::*; +use crate::config::*; +use crate::conn::*; +use crate::content::*; +use crate::curve::named_curve::*; +use crate::error::Error; +use crate::extension::extension_server_name::*; +use crate::extension::extension_supported_elliptic_curves::*; +use crate::extension::extension_supported_point_formats::*; +use crate::extension::extension_supported_signature_algorithms::*; +use crate::extension::extension_use_extended_master_secret::*; +use crate::extension::extension_use_srtp::*; +use crate::extension::renegotiation_info::ExtensionRenegotiationInfo; +use crate::extension::*; +use crate::handshake::handshake_message_client_hello::*; +use crate::handshake::*; +use crate::record_layer::record_layer_header::*; +use crate::record_layer::*; + +#[derive(Debug, PartialEq)] +pub(crate) struct Flight1; + +impl fmt::Display for Flight1 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Flight 1") + } +} + +#[async_trait] +impl Flight for Flight1 { + async fn parse( + &self, + tx: &mut mpsc::Sender>, + state: &mut State, + cache: &HandshakeCache, + cfg: &HandshakeConfig, + ) -> Result, (Option, Option)> { + // HelloVerifyRequest can be skipped by the server, + // so allow ServerHello during flight1 also + let (seq, msgs) = match cache + .full_pull_map( + state.handshake_recv_sequence, + &[ + HandshakeCachePullRule { + typ: HandshakeType::HelloVerifyRequest, + epoch: cfg.initial_epoch, + is_client: false, + optional: true, + }, + HandshakeCachePullRule { + typ: HandshakeType::ServerHello, + epoch: cfg.initial_epoch, + is_client: false, + optional: true, + }, + ], + ) + .await + { + // No valid message received. Keep reading + Ok((seq, msgs)) => (seq, msgs), + Err(_) => return Err((None, None)), + }; + + if msgs.contains_key(&HandshakeType::ServerHello) { + // Flight1 and flight2 were skipped. + // Parse as flight3. + let flight3 = Flight3 {}; + return flight3.parse(tx, state, cache, cfg).await; + } + + if let Some(message) = msgs.get(&HandshakeType::HelloVerifyRequest) { + // DTLS 1.2 clients must not assume that the server will use the protocol version + // specified in HelloVerifyRequest message. RFC 6347 Section 4.2.1 + let h = match message { + HandshakeMessage::HelloVerifyRequest(h) => h, + _ => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + None, + )) + } + }; + + if h.version != PROTOCOL_VERSION1_0 && h.version != PROTOCOL_VERSION1_2 { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::ProtocolVersion, + }), + Some(Error::ErrUnsupportedProtocolVersion), + )); + } + + state.cookie.clone_from(&h.cookie); + state.handshake_recv_sequence = seq; + Ok(Box::new(Flight3 {})) + } else { + Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + None, + )) + } + } + + async fn generate( + &self, + state: &mut State, + _cache: &HandshakeCache, + cfg: &HandshakeConfig, + ) -> Result, (Option, Option)> { + let zero_epoch = 0; + state.local_epoch.store(zero_epoch, Ordering::SeqCst); + state.remote_epoch.store(zero_epoch, Ordering::SeqCst); + + state.named_curve = DEFAULT_NAMED_CURVE; + state.cookie = vec![]; + state.local_random.populate(); + + let mut extensions = vec![ + Extension::SupportedSignatureAlgorithms(ExtensionSupportedSignatureAlgorithms { + signature_hash_algorithms: cfg.local_signature_schemes.clone(), + }), + Extension::RenegotiationInfo(ExtensionRenegotiationInfo { + renegotiated_connection: 0, + }), + ]; + + if cfg.local_psk_callback.is_none() { + extensions.extend_from_slice(&[ + Extension::SupportedEllipticCurves(ExtensionSupportedEllipticCurves { + elliptic_curves: vec![NamedCurve::P256, NamedCurve::X25519, NamedCurve::P384], + }), + Extension::SupportedPointFormats(ExtensionSupportedPointFormats { + point_formats: vec![ELLIPTIC_CURVE_POINT_FORMAT_UNCOMPRESSED], + }), + ]); + } + + if !cfg.local_srtp_protection_profiles.is_empty() { + extensions.push(Extension::UseSrtp(ExtensionUseSrtp { + protection_profiles: cfg.local_srtp_protection_profiles.clone(), + })); + } + + if cfg.extended_master_secret == ExtendedMasterSecretType::Request + || cfg.extended_master_secret == ExtendedMasterSecretType::Require + { + extensions.push(Extension::UseExtendedMasterSecret( + ExtensionUseExtendedMasterSecret { supported: true }, + )); + } + + if !cfg.server_name.is_empty() { + extensions.push(Extension::ServerName(ExtensionServerName { + server_name: cfg.server_name.clone(), + })); + } + + Ok(vec![Packet { + record: RecordLayer::new( + PROTOCOL_VERSION1_2, + 0, + Content::Handshake(Handshake::new(HandshakeMessage::ClientHello( + HandshakeMessageClientHello { + version: PROTOCOL_VERSION1_2, + random: state.local_random.clone(), + cookie: state.cookie.clone(), + + cipher_suites: cfg.local_cipher_suites.clone(), + compression_methods: default_compression_methods(), + extensions, + }, + ))), + ), + should_encrypt: false, + reset_local_sequence_number: false, + }]) + } +} diff --git a/vendor/dtls/src/flight/flight2.rs b/vendor/dtls/src/flight/flight2.rs new file mode 100644 index 00000000..9703c9aa --- /dev/null +++ b/vendor/dtls/src/flight/flight2.rs @@ -0,0 +1,131 @@ +use std::fmt; + +use async_trait::async_trait; + +use super::flight0::*; +use super::flight4::*; +use super::*; +use crate::content::*; +use crate::error::Error; +use crate::handshake::handshake_message_hello_verify_request::*; +use crate::handshake::*; +use crate::record_layer::record_layer_header::*; + +#[derive(Debug, PartialEq)] +pub(crate) struct Flight2; + +impl fmt::Display for Flight2 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Flight 2") + } +} + +#[async_trait] +impl Flight for Flight2 { + fn has_retransmit(&self) -> bool { + false + } + + async fn parse( + &self, + tx: &mut mpsc::Sender>, + state: &mut State, + cache: &HandshakeCache, + cfg: &HandshakeConfig, + ) -> Result, (Option, Option)> { + let (seq, msgs) = match cache + .full_pull_map( + state.handshake_recv_sequence, + &[HandshakeCachePullRule { + typ: HandshakeType::ClientHello, + epoch: cfg.initial_epoch, + is_client: true, + optional: false, + }], + ) + .await + { + // No valid message received. Keep reading + Ok((seq, msgs)) => (seq, msgs), + + // Client may retransmit the first ClientHello when HelloVerifyRequest is dropped. + // Parse as flight 0 in this case. + Err(_) => return Flight0 {}.parse(tx, state, cache, cfg).await, + }; + + state.handshake_recv_sequence = seq; + + if let Some(message) = msgs.get(&HandshakeType::ClientHello) { + // Validate type + let client_hello = match message { + HandshakeMessage::ClientHello(client_hello) => client_hello, + _ => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + None, + )) + } + }; + + if client_hello.version != PROTOCOL_VERSION1_2 { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::ProtocolVersion, + }), + Some(Error::ErrUnsupportedProtocolVersion), + )); + } + + if client_hello.cookie.is_empty() { + return Err((None, None)); + } + + if state.cookie != client_hello.cookie { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::AccessDenied, + }), + Some(Error::ErrCookieMismatch), + )); + } + + Ok(Box::new(Flight4 {})) + } else { + Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + None, + )) + } + } + + async fn generate( + &self, + state: &mut State, + _cache: &HandshakeCache, + _cfg: &HandshakeConfig, + ) -> Result, (Option, Option)> { + state.handshake_send_sequence = 0; + Ok(vec![Packet { + record: RecordLayer::new( + PROTOCOL_VERSION1_2, + 0, + Content::Handshake(Handshake::new(HandshakeMessage::HelloVerifyRequest( + HandshakeMessageHelloVerifyRequest { + version: PROTOCOL_VERSION1_2, + cookie: state.cookie.clone(), + }, + ))), + ), + should_encrypt: false, + reset_local_sequence_number: false, + }]) + } +} diff --git a/vendor/dtls/src/flight/flight3.rs b/vendor/dtls/src/flight/flight3.rs new file mode 100644 index 00000000..a4e2725c --- /dev/null +++ b/vendor/dtls/src/flight/flight3.rs @@ -0,0 +1,470 @@ +use std::fmt; + +use async_trait::async_trait; +use log::*; + +use super::flight5::*; +use super::*; +use crate::cipher_suite::cipher_suite_for_id; +use crate::compression_methods::*; +use crate::config::*; +use crate::content::*; +use crate::curve::named_curve::*; +use crate::error::Error; +use crate::extension::extension_server_name::*; +use crate::extension::extension_supported_elliptic_curves::*; +use crate::extension::extension_supported_point_formats::*; +use crate::extension::extension_supported_signature_algorithms::*; +use crate::extension::extension_use_extended_master_secret::*; +use crate::extension::extension_use_srtp::*; +use crate::extension::renegotiation_info::ExtensionRenegotiationInfo; +use crate::extension::*; +use crate::handshake::handshake_message_client_hello::*; +use crate::handshake::handshake_message_server_key_exchange::*; +use crate::handshake::*; +use crate::prf::{prf_pre_master_secret, prf_psk_pre_master_secret}; +use crate::record_layer::record_layer_header::*; +use crate::record_layer::*; +use crate::{find_matching_cipher_suite, find_matching_srtp_profile}; + +#[derive(Debug, PartialEq)] +pub(crate) struct Flight3; + +impl fmt::Display for Flight3 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Flight 3") + } +} + +#[async_trait] +impl Flight for Flight3 { + async fn parse( + &self, + _tx: &mut mpsc::Sender>, + state: &mut State, + cache: &HandshakeCache, + cfg: &HandshakeConfig, + ) -> Result, (Option, Option)> { + // Clients may receive multiple HelloVerifyRequest messages with different cookies. + // Clients SHOULD handle this by sending a new ClientHello with a cookie in response + // to the new HelloVerifyRequest. RFC 6347 Section 4.2.1 + if let Ok((seq, msgs)) = cache + .full_pull_map( + state.handshake_recv_sequence, + &[HandshakeCachePullRule { + typ: HandshakeType::HelloVerifyRequest, + epoch: cfg.initial_epoch, + is_client: false, + optional: true, + }], + ) + .await + { + if let Some(message) = msgs.get(&HandshakeType::HelloVerifyRequest) { + // DTLS 1.2 clients must not assume that the server will use the protocol version + // specified in HelloVerifyRequest message. RFC 6347 Section 4.2.1 + let h = match message { + HandshakeMessage::HelloVerifyRequest(h) => h, + _ => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + None, + )) + } + }; + + // DTLS 1.2 clients must not assume that the server will use the protocol version + // specified in HelloVerifyRequest message. RFC 6347 Section 4.2.1 + if h.version != PROTOCOL_VERSION1_0 && h.version != PROTOCOL_VERSION1_2 { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::ProtocolVersion, + }), + Some(Error::ErrUnsupportedProtocolVersion), + )); + } + + state.cookie.clone_from(&h.cookie); + state.handshake_recv_sequence = seq; + return Ok(Box::new(Flight3 {}) as Box); + } + } + + let result = if cfg.local_psk_callback.is_some() { + cache + .full_pull_map( + state.handshake_recv_sequence, + &[ + HandshakeCachePullRule { + typ: HandshakeType::ServerHello, + epoch: cfg.initial_epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::ServerKeyExchange, + epoch: cfg.initial_epoch, + is_client: false, + optional: true, + }, + HandshakeCachePullRule { + typ: HandshakeType::ServerHelloDone, + epoch: cfg.initial_epoch, + is_client: false, + optional: false, + }, + ], + ) + .await + } else { + cache + .full_pull_map( + state.handshake_recv_sequence, + &[ + HandshakeCachePullRule { + typ: HandshakeType::ServerHello, + epoch: cfg.initial_epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::Certificate, + epoch: cfg.initial_epoch, + is_client: false, + optional: true, + }, + HandshakeCachePullRule { + typ: HandshakeType::ServerKeyExchange, + epoch: cfg.initial_epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::CertificateRequest, + epoch: cfg.initial_epoch, + is_client: false, + optional: true, + }, + HandshakeCachePullRule { + typ: HandshakeType::ServerHelloDone, + epoch: cfg.initial_epoch, + is_client: false, + optional: false, + }, + ], + ) + .await + }; + + let (seq, msgs) = match result { + Ok((seq, msgs)) => (seq, msgs), + Err(_) => return Err((None, None)), + }; + + state.handshake_recv_sequence = seq; + + if let Some(message) = msgs.get(&HandshakeType::ServerHello) { + let h = match message { + HandshakeMessage::ServerHello(h) => h, + _ => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + None, + )) + } + }; + + if h.version != PROTOCOL_VERSION1_2 { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::ProtocolVersion, + }), + Some(Error::ErrUnsupportedProtocolVersion), + )); + } + + for extension in &h.extensions { + match extension { + Extension::UseSrtp(e) => { + let profile = match find_matching_srtp_profile( + &e.protection_profiles, + &cfg.local_srtp_protection_profiles, + ) { + Ok(profile) => profile, + Err(_) => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::IllegalParameter, + }), + Some(Error::ErrClientNoMatchingSrtpProfile), + )) + } + }; + state.srtp_protection_profile = profile; + } + Extension::UseExtendedMasterSecret(_) => { + if cfg.extended_master_secret != ExtendedMasterSecretType::Disable { + state.extended_master_secret = true; + } + } + _ => {} + }; + } + + if cfg.extended_master_secret == ExtendedMasterSecretType::Require + && !state.extended_master_secret + { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InsufficientSecurity, + }), + Some(Error::ErrClientRequiredButNoServerEms), + )); + } + if !cfg.local_srtp_protection_profiles.is_empty() + && state.srtp_protection_profile == SrtpProtectionProfile::Unsupported + { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InsufficientSecurity, + }), + Some(Error::ErrRequestedButNoSrtpExtension), + )); + } + if find_matching_cipher_suite(&[h.cipher_suite], &cfg.local_cipher_suites).is_err() { + debug!( + "[handshake:{}] use cipher suite: {}", + srv_cli_str(state.is_client), + h.cipher_suite + ); + + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InsufficientSecurity, + }), + Some(Error::ErrCipherSuiteNoIntersection), + )); + } + + let cipher_suite = match cipher_suite_for_id(h.cipher_suite) { + Ok(cipher_suite) => cipher_suite, + Err(_) => { + debug!( + "[handshake:{}] use cipher suite: {}", + srv_cli_str(state.is_client), + h.cipher_suite + ); + + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InsufficientSecurity, + }), + Some(Error::ErrInvalidCipherSuite), + )); + } + }; + + trace!( + "[handshake:{}] use cipher suite: {}", + srv_cli_str(state.is_client), + cipher_suite.to_string() + ); + { + let mut cs = state.cipher_suite.lock().await; + *cs = Some(cipher_suite); + } + state.remote_random = h.random.clone(); + } + + if let Some(message) = msgs.get(&HandshakeType::Certificate) { + let h = match message { + HandshakeMessage::Certificate(h) => h, + _ => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + None, + )) + } + }; + state.peer_certificates.clone_from(&h.certificate); + } + + if let Some(message) = msgs.get(&HandshakeType::ServerKeyExchange) { + let h = match message { + HandshakeMessage::ServerKeyExchange(h) => h, + _ => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + None, + )) + } + }; + + if let Err((alert, err)) = handle_server_key_exchange(state, cfg, h).await { + return Err((alert, err)); + } + } + + if let Some(message) = msgs.get(&HandshakeType::CertificateRequest) { + match message { + HandshakeMessage::CertificateRequest(_) => {} + _ => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + None, + )) + } + }; + state.remote_requested_certificate = true; + } + + Ok(Box::new(Flight5 {}) as Box) + } + + async fn generate( + &self, + state: &mut State, + _cache: &HandshakeCache, + cfg: &HandshakeConfig, + ) -> Result, (Option, Option)> { + let mut extensions = vec![ + Extension::SupportedSignatureAlgorithms(ExtensionSupportedSignatureAlgorithms { + signature_hash_algorithms: cfg.local_signature_schemes.clone(), + }), + Extension::RenegotiationInfo(ExtensionRenegotiationInfo { + renegotiated_connection: 0, + }), + ]; + + if cfg.local_psk_callback.is_none() { + extensions.extend_from_slice(&[ + Extension::SupportedEllipticCurves(ExtensionSupportedEllipticCurves { + elliptic_curves: vec![NamedCurve::P256, NamedCurve::X25519, NamedCurve::P384], + }), + Extension::SupportedPointFormats(ExtensionSupportedPointFormats { + point_formats: vec![ELLIPTIC_CURVE_POINT_FORMAT_UNCOMPRESSED], + }), + ]); + } + + if !cfg.local_srtp_protection_profiles.is_empty() { + extensions.push(Extension::UseSrtp(ExtensionUseSrtp { + protection_profiles: cfg.local_srtp_protection_profiles.clone(), + })); + } + + if cfg.extended_master_secret == ExtendedMasterSecretType::Request + || cfg.extended_master_secret == ExtendedMasterSecretType::Require + { + extensions.push(Extension::UseExtendedMasterSecret( + ExtensionUseExtendedMasterSecret { supported: true }, + )); + } + + if !cfg.server_name.is_empty() { + extensions.push(Extension::ServerName(ExtensionServerName { + server_name: cfg.server_name.clone(), + })); + } + + Ok(vec![Packet { + record: RecordLayer::new( + PROTOCOL_VERSION1_2, + 0, + Content::Handshake(Handshake::new(HandshakeMessage::ClientHello( + HandshakeMessageClientHello { + version: PROTOCOL_VERSION1_2, + random: state.local_random.clone(), + cookie: state.cookie.clone(), + + cipher_suites: cfg.local_cipher_suites.clone(), + compression_methods: default_compression_methods(), + extensions, + }, + ))), + ), + should_encrypt: false, + reset_local_sequence_number: false, + }]) + } +} + +pub(crate) async fn handle_server_key_exchange( + state: &mut State, + cfg: &HandshakeConfig, + h: &HandshakeMessageServerKeyExchange, +) -> Result<(), (Option, Option)> { + if let Some(local_psk_callback) = &cfg.local_psk_callback { + let psk = match local_psk_callback(&h.identity_hint).await { + Ok(psk) => psk, + Err(err) => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + Some(err), + )) + } + }; + + state.identity_hint.clone_from(&h.identity_hint); + state.pre_master_secret = prf_psk_pre_master_secret(&psk); + } else { + let local_keypair = match h.named_curve.generate_keypair() { + Ok(local_keypair) => local_keypair, + Err(err) => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + Some(err), + )) + } + }; + + state.pre_master_secret = match prf_pre_master_secret( + &h.public_key, + &local_keypair.private_key, + local_keypair.curve, + ) { + Ok(pre_master_secret) => pre_master_secret, + Err(err) => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + Some(err), + )) + } + }; + + state.local_keypair = Some(local_keypair); + } + + Ok(()) +} diff --git a/vendor/dtls/src/flight/flight4.rs b/vendor/dtls/src/flight/flight4.rs new file mode 100644 index 00000000..b93e78e4 --- /dev/null +++ b/vendor/dtls/src/flight/flight4.rs @@ -0,0 +1,853 @@ +use std::fmt; +use std::io::BufWriter; + +use async_trait::async_trait; +use log::*; + +use super::flight6::*; +use super::*; +use crate::cipher_suite::*; +use crate::client_certificate_type::*; +use crate::compression_methods::*; +use crate::config::*; +use crate::content::*; +use crate::crypto::*; +use crate::curve::named_curve::*; +use crate::curve::*; +use crate::error::Error; +use crate::extension::extension_supported_elliptic_curves::*; +use crate::extension::extension_supported_point_formats::*; +use crate::extension::extension_use_extended_master_secret::*; +use crate::extension::extension_use_srtp::*; +use crate::extension::renegotiation_info::ExtensionRenegotiationInfo; +use crate::extension::*; +use crate::handshake::handshake_message_certificate::*; +use crate::handshake::handshake_message_certificate_request::*; +use crate::handshake::handshake_message_server_hello::*; +use crate::handshake::handshake_message_server_hello_done::*; +use crate::handshake::handshake_message_server_key_exchange::*; +use crate::handshake::*; +use crate::prf::*; +use crate::record_layer::record_layer_header::*; +use crate::record_layer::*; +use crate::signature_hash_algorithm::*; + +#[derive(Debug, PartialEq)] +pub(crate) struct Flight4; + +impl fmt::Display for Flight4 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Flight 4") + } +} + +#[async_trait] +impl Flight for Flight4 { + async fn parse( + &self, + tx: &mut mpsc::Sender>, + state: &mut State, + cache: &HandshakeCache, + cfg: &HandshakeConfig, + ) -> Result, (Option, Option)> { + let (seq, msgs) = match cache + .full_pull_map( + state.handshake_recv_sequence, + &[ + HandshakeCachePullRule { + typ: HandshakeType::Certificate, + epoch: cfg.initial_epoch, + is_client: true, + optional: true, + }, + HandshakeCachePullRule { + typ: HandshakeType::ClientKeyExchange, + epoch: cfg.initial_epoch, + is_client: true, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::CertificateVerify, + epoch: cfg.initial_epoch, + is_client: true, + optional: true, + }, + ], + ) + .await + { + Ok((seq, msgs)) => (seq, msgs), + Err(_) => return Err((None, None)), + }; + + let client_key_exchange = if let Some(HandshakeMessage::ClientKeyExchange(h)) = + msgs.get(&HandshakeType::ClientKeyExchange) + { + h + } else { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + None, + )); + }; + + if let Some(message) = msgs.get(&HandshakeType::Certificate) { + let h = match message { + HandshakeMessage::Certificate(h) => h, + _ => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + None, + )) + } + }; + + state.peer_certificates.clone_from(&h.certificate); + trace!( + "[handshake] PeerCertificates4 {}", + state.peer_certificates.len() + ); + } + + if let Some(message) = msgs.get(&HandshakeType::CertificateVerify) { + let h = match message { + HandshakeMessage::CertificateVerify(h) => h, + _ => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + None, + )) + } + }; + + if state.peer_certificates.is_empty() { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::NoCertificate, + }), + Some(Error::ErrCertificateVerifyNoCertificate), + )); + } + + let plain_text = cache + .pull_and_merge(&[ + HandshakeCachePullRule { + typ: HandshakeType::ClientHello, + epoch: cfg.initial_epoch, + is_client: true, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::ServerHello, + epoch: cfg.initial_epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::Certificate, + epoch: cfg.initial_epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::ServerKeyExchange, + epoch: cfg.initial_epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::CertificateRequest, + epoch: cfg.initial_epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::ServerHelloDone, + epoch: cfg.initial_epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::Certificate, + epoch: cfg.initial_epoch, + is_client: true, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::ClientKeyExchange, + epoch: cfg.initial_epoch, + is_client: true, + optional: false, + }, + ]) + .await; + + // Verify that the pair of hash algorithm and signature is listed. + let mut valid_signature_scheme = false; + for ss in &cfg.local_signature_schemes { + if ss.hash == h.algorithm.hash && ss.signature == h.algorithm.signature { + valid_signature_scheme = true; + break; + } + } + if !valid_signature_scheme { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InsufficientSecurity, + }), + Some(Error::ErrNoAvailableSignatureSchemes), + )); + } + + if let Err(err) = verify_certificate_verify( + &plain_text, + &h.algorithm, + &h.signature, + &state.peer_certificates, + cfg.insecure_verification, + ) { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::BadCertificate, + }), + Some(err), + )); + } + + let mut chains = vec![]; + let mut verified = false; + if cfg.client_auth as u8 >= ClientAuthType::VerifyClientCertIfGiven as u8 { + if let Some(client_cert_verifier) = &cfg.client_cert_verifier { + chains = + match verify_client_cert(&state.peer_certificates, client_cert_verifier) { + Ok(chains) => chains, + Err(err) => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::BadCertificate, + }), + Some(err), + )) + } + }; + } else { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::BadCertificate, + }), + Some(Error::ErrInvalidCertificate), + )); + } + + verified = true + } + if let Some(verify_peer_certificate) = &cfg.verify_peer_certificate { + if let Err(err) = verify_peer_certificate(&state.peer_certificates, &chains) { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::BadCertificate, + }), + Some(err), + )); + } + } + state.peer_certificates_verified = verified + } else if !state.peer_certificates.is_empty() { + // A certificate was received, but we haven't seen a CertificateVerify + // keep reading until we receive one + return Err((None, None)); + } + + { + let mut cipher_suite = state.cipher_suite.lock().await; + if let Some(cipher_suite) = &mut *cipher_suite { + if !cipher_suite.is_initialized() { + let mut server_random = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(server_random.as_mut()); + let _ = state.local_random.marshal(&mut writer); + } + let mut client_random = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(client_random.as_mut()); + let _ = state.remote_random.marshal(&mut writer); + } + + let mut pre_master_secret = vec![]; + if let Some(local_psk_callback) = &cfg.local_psk_callback { + let psk = match local_psk_callback(&client_key_exchange.identity_hint).await + { + Ok(psk) => psk, + Err(err) => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + Some(err), + )) + } + }; + + state + .identity_hint + .clone_from(&client_key_exchange.identity_hint); + pre_master_secret = prf_psk_pre_master_secret(&psk); + } else if let Some(local_keypair) = &state.local_keypair { + pre_master_secret = match prf_pre_master_secret( + &client_key_exchange.public_key, + &local_keypair.private_key, + local_keypair.curve, + ) { + Ok(pre_master_secret) => pre_master_secret, + Err(err) => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::IllegalParameter, + }), + Some(err), + )) + } + }; + } + + if state.extended_master_secret { + let hf = cipher_suite.hash_func(); + let session_hash = + match cache.session_hash(hf, cfg.initial_epoch, &[]).await { + Ok(s) => s, + Err(err) => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + Some(err), + )) + } + }; + + state.master_secret = match prf_extended_master_secret( + &pre_master_secret, + &session_hash, + cipher_suite.hash_func(), + ) { + Ok(ms) => ms, + Err(err) => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + Some(err), + )) + } + }; + } else { + state.master_secret = match prf_master_secret( + &pre_master_secret, + &client_random, + &server_random, + cipher_suite.hash_func(), + ) { + Ok(ms) => ms, + Err(err) => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + Some(err), + )) + } + }; + } + + if let Err(err) = cipher_suite.init( + &state.master_secret, + &client_random, + &server_random, + false, + ) { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + Some(err), + )); + } + } + } + } + + // Now, encrypted packets can be handled + let (done_tx, mut done_rx) = mpsc::channel(1); + if let Err(err) = tx.send(done_tx).await { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + Some(Error::Other(err.to_string())), + )); + } + + done_rx.recv().await; + + let (seq, msgs) = match cache + .full_pull_map( + seq, + &[HandshakeCachePullRule { + typ: HandshakeType::Finished, + epoch: cfg.initial_epoch + 1, + is_client: true, + optional: false, + }], + ) + .await + { + Ok((seq, msgs)) => (seq, msgs), + // No valid message received. Keep reading + Err(_) => return Err((None, None)), + }; + + state.handshake_recv_sequence = seq; + + if let Some(HandshakeMessage::Finished(h)) = msgs.get(&HandshakeType::Finished) { + h + } else { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + None, + )); + }; + + match cfg.client_auth { + ClientAuthType::RequireAnyClientCert => { + trace!( + "{} peer_certificates.len() {}", + srv_cli_str(state.is_client), + state.peer_certificates.len(), + ); + if state.peer_certificates.is_empty() { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::NoCertificate, + }), + Some(Error::ErrClientCertificateRequired), + )); + } + } + ClientAuthType::VerifyClientCertIfGiven => { + if !state.peer_certificates.is_empty() && !state.peer_certificates_verified { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::BadCertificate, + }), + Some(Error::ErrClientCertificateNotVerified), + )); + } + } + ClientAuthType::RequireAndVerifyClientCert => { + if state.peer_certificates.is_empty() { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::NoCertificate, + }), + Some(Error::ErrClientCertificateRequired), + )); + } + if !state.peer_certificates_verified { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::BadCertificate, + }), + Some(Error::ErrClientCertificateNotVerified), + )); + } + } + ClientAuthType::NoClientCert | ClientAuthType::RequestClientCert => { + return Ok(Box::new(Flight6 {}) as Box); + } + } + + Ok(Box::new(Flight6 {}) as Box) + } + + async fn generate( + &self, + state: &mut State, + _cache: &HandshakeCache, + cfg: &HandshakeConfig, + ) -> Result, (Option, Option)> { + let mut extensions = vec![Extension::RenegotiationInfo(ExtensionRenegotiationInfo { + renegotiated_connection: 0, + })]; + if (cfg.extended_master_secret == ExtendedMasterSecretType::Request + || cfg.extended_master_secret == ExtendedMasterSecretType::Require) + && state.extended_master_secret + { + extensions.push(Extension::UseExtendedMasterSecret( + ExtensionUseExtendedMasterSecret { supported: true }, + )); + } + + if state.srtp_protection_profile != SrtpProtectionProfile::Unsupported { + extensions.push(Extension::UseSrtp(ExtensionUseSrtp { + protection_profiles: vec![state.srtp_protection_profile], + })); + } + + if cfg.local_psk_callback.is_none() { + extensions.extend_from_slice(&[ + Extension::SupportedEllipticCurves(ExtensionSupportedEllipticCurves { + elliptic_curves: vec![NamedCurve::P256, NamedCurve::X25519, NamedCurve::P384], + }), + Extension::SupportedPointFormats(ExtensionSupportedPointFormats { + point_formats: vec![ELLIPTIC_CURVE_POINT_FORMAT_UNCOMPRESSED], + }), + ]); + } + + let mut pkts = vec![Packet { + record: RecordLayer::new( + PROTOCOL_VERSION1_2, + 0, + Content::Handshake(Handshake::new(HandshakeMessage::ServerHello( + HandshakeMessageServerHello { + version: PROTOCOL_VERSION1_2, + random: state.local_random.clone(), + cipher_suite: { + let cipher_suite = state.cipher_suite.lock().await; + if let Some(cipher_suite) = &*cipher_suite { + cipher_suite.id() + } else { + CipherSuiteId::Unsupported + } + }, + compression_method: default_compression_methods().ids[0], + extensions, + }, + ))), + ), + should_encrypt: false, + reset_local_sequence_number: false, + }]; + + if cfg.local_psk_callback.is_none() { + let certificate = match cfg.get_certificate(&cfg.server_name) { + Ok(cert) => cert, + Err(err) => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::HandshakeFailure, + }), + Some(err), + )) + } + }; + + pkts.push(Packet { + record: RecordLayer::new( + PROTOCOL_VERSION1_2, + 0, + Content::Handshake(Handshake::new(HandshakeMessage::Certificate( + HandshakeMessageCertificate { + certificate: certificate + .certificate + .iter() + .map(|x| x.as_ref().to_owned()) + .collect(), + }, + ))), + ), + should_encrypt: false, + reset_local_sequence_number: false, + }); + + let mut server_random = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(server_random.as_mut()); + let _ = state.local_random.marshal(&mut writer); + } + let mut client_random = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(client_random.as_mut()); + let _ = state.remote_random.marshal(&mut writer); + } + + // Find compatible signature scheme + let signature_hash_algo = match select_signature_scheme( + &cfg.local_signature_schemes, + &certificate.private_key, + ) { + Ok(s) => s, + Err(err) => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InsufficientSecurity, + }), + Some(err), + )) + } + }; + + if let Some(local_keypair) = &state.local_keypair { + let signature = match generate_key_signature( + &client_random, + &server_random, + &local_keypair.public_key, + state.named_curve, + &certificate.private_key, /*, signature_hash_algo.hash*/ + ) { + Ok(s) => s, + Err(err) => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + Some(err), + )) + } + }; + + state.local_key_signature = signature; + + pkts.push(Packet { + record: RecordLayer::new( + PROTOCOL_VERSION1_2, + 0, + Content::Handshake(Handshake::new(HandshakeMessage::ServerKeyExchange( + HandshakeMessageServerKeyExchange { + identity_hint: vec![], + elliptic_curve_type: EllipticCurveType::NamedCurve, + named_curve: state.named_curve, + public_key: local_keypair.public_key.clone(), + algorithm: SignatureHashAlgorithm { + hash: signature_hash_algo.hash, + signature: signature_hash_algo.signature, + }, + signature: state.local_key_signature.clone(), + }, + ))), + ), + should_encrypt: false, + reset_local_sequence_number: false, + }); + } + + if cfg.client_auth as u8 > ClientAuthType::NoClientCert as u8 { + pkts.push(Packet { + record: RecordLayer::new( + PROTOCOL_VERSION1_2, + 0, + Content::Handshake(Handshake::new(HandshakeMessage::CertificateRequest( + HandshakeMessageCertificateRequest { + certificate_types: vec![ + ClientCertificateType::RsaSign, + ClientCertificateType::EcdsaSign, + ], + signature_hash_algorithms: cfg.local_signature_schemes.clone(), + }, + ))), + ), + should_encrypt: false, + reset_local_sequence_number: false, + }); + } + } else if let Some(local_psk_identity_hint) = &cfg.local_psk_identity_hint { + // To help the client in selecting which identity to use, the server + // can provide a "PSK identity hint" in the ServerKeyExchange message. + // If no hint is provided, the ServerKeyExchange message is omitted. + // + // https://tools.ietf.org/html/rfc4279#section-2 + pkts.push(Packet { + record: RecordLayer::new( + PROTOCOL_VERSION1_2, + 0, + Content::Handshake(Handshake::new(HandshakeMessage::ServerKeyExchange( + HandshakeMessageServerKeyExchange { + identity_hint: local_psk_identity_hint.clone(), + elliptic_curve_type: EllipticCurveType::Unsupported, + named_curve: NamedCurve::Unsupported, + public_key: vec![], + algorithm: SignatureHashAlgorithm { + hash: HashAlgorithm::Unsupported, + signature: SignatureAlgorithm::Unsupported, + }, + signature: vec![], + }, + ))), + ), + should_encrypt: false, + reset_local_sequence_number: false, + }); + } + + pkts.push(Packet { + record: RecordLayer::new( + PROTOCOL_VERSION1_2, + 0, + Content::Handshake(Handshake::new(HandshakeMessage::ServerHelloDone( + HandshakeMessageServerHelloDone {}, + ))), + ), + should_encrypt: false, + reset_local_sequence_number: false, + }); + + Ok(pkts) + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use tokio::sync::Mutex; + + use super::*; + use crate::error::Result; + + struct MockCipherSuite {} + + impl CipherSuite for MockCipherSuite { + fn to_string(&self) -> String { + "MockCipherSuite".into() + } + fn id(&self) -> CipherSuiteId { + unimplemented!(); + } + fn certificate_type(&self) -> ClientCertificateType { + unimplemented!(); + } + fn hash_func(&self) -> CipherSuiteHash { + unimplemented!(); + } + fn is_psk(&self) -> bool { + false + } + fn is_initialized(&self) -> bool { + panic!("is_initialized called with Certificate but not CertificateVerify"); + } + + // Generate the internal encryption state + fn init( + &mut self, + _master_secret: &[u8], + _client_random: &[u8], + _server_random: &[u8], + _is_client: bool, + ) -> Result<()> { + unimplemented!(); + } + + fn encrypt(&self, _pkt_rlh: &RecordLayerHeader, _raw: &[u8]) -> Result> { + unimplemented!(); + } + fn decrypt(&self, _input: &[u8]) -> Result> { + unimplemented!(); + } + } + + // Assert that if a client sends a certificate they must also send a `CertificateVerify` + // message. The `Flight4` must not interact with the `cipher_suite` if the `CertificateVerify` + // is missing. + #[tokio::test] + async fn test_flight4_process_certificateverify() { + let mut state = State { + cipher_suite: Arc::new(Mutex::new(Some(Box::new(MockCipherSuite {})))), + ..Default::default() + }; + + let raw_certificate = vec![ + 0x0b, 0x00, 0x01, 0x9b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x9b, 0x00, 0x01, + 0x98, 0x00, 0x01, 0x95, 0x30, 0x82, 0x01, 0x91, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, + 0x02, 0x01, 0x02, 0x02, 0x11, 0x01, 0x65, 0x03, 0x3f, 0x4d, 0x0b, 0x9a, 0x62, 0x91, + 0xdb, 0x4d, 0x28, 0x2c, 0x1f, 0xd6, 0x73, 0x32, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, + 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x30, 0x00, 0x30, 0x1e, 0x17, 0x0d, 0x32, 0x32, + 0x30, 0x35, 0x31, 0x35, 0x31, 0x38, 0x34, 0x33, 0x35, 0x35, 0x5a, 0x17, 0x0d, 0x32, + 0x32, 0x30, 0x36, 0x31, 0x35, 0x31, 0x38, 0x34, 0x33, 0x35, 0x35, 0x5a, 0x30, 0x00, + 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, + 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0xc3, + 0xb7, 0x13, 0x1a, 0x0a, 0xfc, 0xd0, 0x82, 0xf8, 0x94, 0x5e, 0xc0, 0x77, 0x07, 0x81, + 0x28, 0xc9, 0xcb, 0x08, 0x84, 0x50, 0x6b, 0xf0, 0x22, 0xe8, 0x79, 0xb9, 0x15, 0x33, + 0xc4, 0x56, 0xa1, 0xd3, 0x1b, 0x24, 0xe3, 0x61, 0xbd, 0x4d, 0x65, 0x80, 0x6b, 0x5d, + 0x96, 0x48, 0xa2, 0x44, 0x9e, 0xce, 0xe8, 0x65, 0xd6, 0x3c, 0xe0, 0x9b, 0x6b, 0xa1, + 0x36, 0x34, 0xb2, 0x39, 0xe2, 0x03, 0x00, 0xa3, 0x81, 0x92, 0x30, 0x81, 0x8f, 0x30, + 0x0e, 0x06, 0x03, 0x55, 0x1d, 0x0f, 0x01, 0x01, 0xff, 0x04, 0x04, 0x03, 0x02, 0x02, + 0xa4, 0x30, 0x1d, 0x06, 0x03, 0x55, 0x1d, 0x25, 0x04, 0x16, 0x30, 0x14, 0x06, 0x08, + 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x02, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, + 0x05, 0x07, 0x03, 0x01, 0x30, 0x0f, 0x06, 0x03, 0x55, 0x1d, 0x13, 0x01, 0x01, 0xff, + 0x04, 0x05, 0x30, 0x03, 0x01, 0x01, 0xff, 0x30, 0x1d, 0x06, 0x03, 0x55, 0x1d, 0x0e, + 0x04, 0x16, 0x04, 0x14, 0xb1, 0x1a, 0xe3, 0xeb, 0x6f, 0x7c, 0xc3, 0x8f, 0xba, 0x6f, + 0x1c, 0xe8, 0xf0, 0x23, 0x08, 0x50, 0x8d, 0x3c, 0xea, 0x31, 0x30, 0x2e, 0x06, 0x03, + 0x55, 0x1d, 0x11, 0x01, 0x01, 0xff, 0x04, 0x24, 0x30, 0x22, 0x82, 0x20, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, + 0x03, 0x47, 0x00, 0x30, 0x44, 0x02, 0x20, 0x06, 0x31, 0x43, 0xac, 0x03, 0x45, 0x79, + 0x3c, 0xd7, 0x5f, 0x6e, 0x6a, 0xf8, 0x0e, 0xfd, 0x35, 0x49, 0xee, 0x1b, 0xbc, 0x47, + 0xce, 0xe3, 0x39, 0xec, 0xe4, 0x62, 0xe1, 0x30, 0x1a, 0xa1, 0x89, 0x02, 0x20, 0x35, + 0xcd, 0x7a, 0x15, 0x68, 0x09, 0x50, 0x49, 0x9e, 0x3e, 0x05, 0xd7, 0xc2, 0x69, 0x3f, + 0x9c, 0x0c, 0x98, 0x92, 0x65, 0xec, 0xae, 0x44, 0xfe, 0xe5, 0x68, 0xb8, 0x09, 0x78, + 0x7f, 0x6b, 0x77, + ]; + + let raw_client_key_exchange = vec![ + 0x10, 0x00, 0x00, 0x21, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x20, 0x96, + 0xed, 0x0c, 0xee, 0xf3, 0x11, 0xb1, 0x9d, 0x8b, 0x1c, 0x02, 0x7f, 0x06, 0x7c, 0x57, + 0x7a, 0x14, 0xa6, 0x41, 0xde, 0x63, 0x57, 0x9e, 0xcd, 0x34, 0x54, 0xba, 0x37, 0x4d, + 0x34, 0x15, 0x18, + ]; + + let mut cache = HandshakeCache::new(); + cache + .push(raw_certificate, 0, 0, HandshakeType::Certificate, true) + .await; + cache + .push( + raw_client_key_exchange, + 0, + 1, + HandshakeType::ClientKeyExchange, + true, + ) + .await; + + let cfg = HandshakeConfig::default(); + + let (mut tx, _rx) = mpsc::channel::>(1); + + let f = Flight4 {}; + let res = f.parse(&mut tx, &mut state, &cache, &cfg).await; + assert!(res.is_err()); + } +} diff --git a/vendor/dtls/src/flight/flight5.rs b/vendor/dtls/src/flight/flight5.rs new file mode 100644 index 00000000..c794680f --- /dev/null +++ b/vendor/dtls/src/flight/flight5.rs @@ -0,0 +1,779 @@ +use std::fmt; +use std::io::{BufReader, BufWriter}; + +use async_trait::async_trait; + +use super::flight3::*; +use super::*; +use crate::change_cipher_spec::ChangeCipherSpec; +use crate::content::*; +use crate::crypto::*; +use crate::curve::named_curve::*; +use crate::curve::*; +use crate::error::Error; +use crate::handshake::handshake_message_certificate::*; +use crate::handshake::handshake_message_certificate_verify::*; +use crate::handshake::handshake_message_client_key_exchange::*; +use crate::handshake::handshake_message_finished::*; +use crate::handshake::handshake_message_server_key_exchange::*; +use crate::handshake::*; +use crate::prf::*; +use crate::record_layer::record_layer_header::*; +use crate::record_layer::*; +use crate::signature_hash_algorithm::*; + +#[derive(Debug, PartialEq)] +pub(crate) struct Flight5; + +impl fmt::Display for Flight5 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Flight 5") + } +} + +#[async_trait] +impl Flight for Flight5 { + fn is_last_recv_flight(&self) -> bool { + true + } + + async fn parse( + &self, + _tx: &mut mpsc::Sender>, + state: &mut State, + cache: &HandshakeCache, + cfg: &HandshakeConfig, + ) -> Result, (Option, Option)> { + let (_seq, msgs) = match cache + .full_pull_map( + state.handshake_recv_sequence, + &[HandshakeCachePullRule { + typ: HandshakeType::Finished, + epoch: cfg.initial_epoch + 1, + is_client: false, + optional: false, + }], + ) + .await + { + Ok((seq, msgs)) => (seq, msgs), + Err(_) => return Err((None, None)), + }; + + let finished = + if let Some(HandshakeMessage::Finished(h)) = msgs.get(&HandshakeType::Finished) { + h + } else { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + None, + )); + }; + + let plain_text = cache + .pull_and_merge(&[ + HandshakeCachePullRule { + typ: HandshakeType::ClientHello, + epoch: cfg.initial_epoch, + is_client: true, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::ServerHello, + epoch: cfg.initial_epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::Certificate, + epoch: cfg.initial_epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::ServerKeyExchange, + epoch: cfg.initial_epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::CertificateRequest, + epoch: cfg.initial_epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::ServerHelloDone, + epoch: cfg.initial_epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::Certificate, + epoch: cfg.initial_epoch, + is_client: true, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::ClientKeyExchange, + epoch: cfg.initial_epoch, + is_client: true, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::CertificateVerify, + epoch: cfg.initial_epoch, + is_client: true, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::Finished, + epoch: cfg.initial_epoch + 1, + is_client: true, + optional: false, + }, + ]) + .await; + + { + let cipher_suite = state.cipher_suite.lock().await; + if let Some(cipher_suite) = &*cipher_suite { + let expected_verify_data = match prf_verify_data_server( + &state.master_secret, + &plain_text, + cipher_suite.hash_func(), + ) { + Ok(d) => d, + Err(err) => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InsufficientSecurity, + }), + Some(err), + )) + } + }; + + if expected_verify_data != finished.verify_data { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::HandshakeFailure, + }), + Some(Error::ErrVerifyDataMismatch), + )); + } + } + } + + Ok(Box::new(Flight5 {})) + } + + async fn generate( + &self, + state: &mut State, + cache: &HandshakeCache, + cfg: &HandshakeConfig, + ) -> Result, (Option, Option)> { + let certificate = if !cfg.local_certificates.is_empty() { + let cert = match cfg.get_certificate(&cfg.server_name) { + Ok(cert) => cert, + Err(err) => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::HandshakeFailure, + }), + Some(err), + )) + } + }; + Some(cert) + } else { + None + }; + + let mut pkts = vec![]; + + if state.remote_requested_certificate { + pkts.push(Packet { + record: RecordLayer::new( + PROTOCOL_VERSION1_2, + 0, + Content::Handshake(Handshake::new(HandshakeMessage::Certificate( + HandshakeMessageCertificate { + certificate: if let Some(cert) = &certificate { + cert.certificate + .iter() + .map(|x| x.as_ref().to_owned()) + .collect() + } else { + vec![] + }, + }, + ))), + ), + should_encrypt: false, + reset_local_sequence_number: false, + }); + } + + let mut client_key_exchange = HandshakeMessageClientKeyExchange { + identity_hint: vec![], + public_key: vec![], + }; + if cfg.local_psk_callback.is_none() { + if let Some(local_keypair) = &state.local_keypair { + client_key_exchange + .public_key + .clone_from(&local_keypair.public_key); + } + } else if let Some(local_psk_identity_hint) = &cfg.local_psk_identity_hint { + client_key_exchange + .identity_hint + .clone_from(local_psk_identity_hint); + } + + pkts.push(Packet { + record: RecordLayer::new( + PROTOCOL_VERSION1_2, + 0, + Content::Handshake(Handshake::new(HandshakeMessage::ClientKeyExchange( + client_key_exchange, + ))), + ), + should_encrypt: false, + reset_local_sequence_number: false, + }); + + let server_key_exchange_data = cache + .pull_and_merge(&[HandshakeCachePullRule { + typ: HandshakeType::ServerKeyExchange, + epoch: cfg.initial_epoch, + is_client: false, + optional: false, + }]) + .await; + + let mut server_key_exchange = HandshakeMessageServerKeyExchange { + identity_hint: vec![], + elliptic_curve_type: EllipticCurveType::Unsupported, + named_curve: NamedCurve::Unsupported, + public_key: vec![], + algorithm: SignatureHashAlgorithm { + hash: HashAlgorithm::Unsupported, + signature: SignatureAlgorithm::Unsupported, + }, + signature: vec![], + }; + + // handshakeMessageServerKeyExchange is optional for PSK + if server_key_exchange_data.is_empty() { + if let Err((alert, err)) = + handle_server_key_exchange(state, cfg, &server_key_exchange).await + { + return Err((alert, err)); + } + } else { + let mut reader = BufReader::new(server_key_exchange_data.as_slice()); + let raw_handshake = match Handshake::unmarshal(&mut reader) { + Ok(h) => h, + Err(err) => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::UnexpectedMessage, + }), + Some(err), + )) + } + }; + + match raw_handshake.handshake_message { + HandshakeMessage::ServerKeyExchange(h) => server_key_exchange = h, + _ => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::UnexpectedMessage, + }), + Some(Error::ErrInvalidContentType), + )) + } + }; + } + + // Append not-yet-sent packets + let mut merged = vec![]; + let mut seq_pred = state.handshake_send_sequence as u16; + for p in &mut pkts { + let h = match &mut p.record.content { + Content::Handshake(h) => h, + _ => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + Some(Error::ErrInvalidContentType), + )) + } + }; + h.handshake_header.message_sequence = seq_pred; + seq_pred += 1; + + let mut raw = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); + if let Err(err) = h.marshal(&mut writer) { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + Some(err), + )); + } + } + + merged.extend_from_slice(&raw); + } + + if let Err((alert, err)) = + initialize_cipher_suite(state, cache, cfg, &server_key_exchange, &merged).await + { + return Err((alert, err)); + } + + // If the client has sent a certificate with signing ability, a digitally-signed + // CertificateVerify message is sent to explicitly verify possession of the + // private key in the certificate. + if state.remote_requested_certificate && !cfg.local_certificates.is_empty() { + let mut plain_text = cache + .pull_and_merge(&[ + HandshakeCachePullRule { + typ: HandshakeType::ClientHello, + epoch: cfg.initial_epoch, + is_client: true, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::ServerHello, + epoch: cfg.initial_epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::Certificate, + epoch: cfg.initial_epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::ServerKeyExchange, + epoch: cfg.initial_epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::CertificateRequest, + epoch: cfg.initial_epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::ServerHelloDone, + epoch: cfg.initial_epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::Certificate, + epoch: cfg.initial_epoch, + is_client: true, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::ClientKeyExchange, + epoch: cfg.initial_epoch, + is_client: true, + optional: false, + }, + ]) + .await; + + plain_text.extend_from_slice(&merged); + + // Find compatible signature scheme + let signature_hash_algo = match select_signature_scheme( + &cfg.local_signature_schemes, + &certificate.as_ref().unwrap().private_key, + ) { + Ok(s) => s, + Err(err) => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InsufficientSecurity, + }), + Some(err), + )) + } + }; + + let cert_verify = match generate_certificate_verify( + &plain_text, + &certificate.as_ref().unwrap().private_key, /*, signature_hash_algo.hash*/ + ) { + Ok(cert) => cert, + Err(err) => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + Some(err), + )) + } + }; + state.local_certificates_verify = cert_verify; + + let mut p = Packet { + record: RecordLayer::new( + PROTOCOL_VERSION1_2, + 0, + Content::Handshake(Handshake::new(HandshakeMessage::CertificateVerify( + HandshakeMessageCertificateVerify { + algorithm: signature_hash_algo, + signature: state.local_certificates_verify.clone(), + }, + ))), + ), + should_encrypt: false, + reset_local_sequence_number: false, + }; + + let h = match &mut p.record.content { + Content::Handshake(h) => h, + _ => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + Some(Error::ErrInvalidContentType), + )) + } + }; + h.handshake_header.message_sequence = seq_pred; + + // seqPred++ // this is the last use of seqPred + + let mut raw = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); + if let Err(err) = h.marshal(&mut writer) { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + Some(err), + )); + } + } + merged.extend_from_slice(&raw); + + pkts.push(p); + } + + pkts.push(Packet { + record: RecordLayer::new( + PROTOCOL_VERSION1_2, + 0, + Content::ChangeCipherSpec(ChangeCipherSpec {}), + ), + should_encrypt: false, + reset_local_sequence_number: false, + }); + + if state.local_verify_data.is_empty() { + let mut plain_text = cache + .pull_and_merge(&[ + HandshakeCachePullRule { + typ: HandshakeType::ClientHello, + epoch: cfg.initial_epoch, + is_client: true, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::ServerHello, + epoch: cfg.initial_epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::Certificate, + epoch: cfg.initial_epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::ServerKeyExchange, + epoch: cfg.initial_epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::CertificateRequest, + epoch: cfg.initial_epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::ServerHelloDone, + epoch: cfg.initial_epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::Certificate, + epoch: cfg.initial_epoch, + is_client: true, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::ClientKeyExchange, + epoch: cfg.initial_epoch, + is_client: true, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::CertificateVerify, + epoch: cfg.initial_epoch, + is_client: true, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::Finished, + epoch: cfg.initial_epoch + 1, + is_client: true, + optional: false, + }, + ]) + .await; + + plain_text.extend_from_slice(&merged); + + let cipher_suite = state.cipher_suite.lock().await; + if let Some(cipher_suite) = &*cipher_suite { + state.local_verify_data = match prf_verify_data_client( + &state.master_secret, + &plain_text, + cipher_suite.hash_func(), + ) { + Ok(data) => data, + Err(err) => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + Some(err), + )) + } + }; + } + } + + pkts.push(Packet { + record: RecordLayer::new( + PROTOCOL_VERSION1_2, + 1, + Content::Handshake(Handshake::new(HandshakeMessage::Finished( + HandshakeMessageFinished { + verify_data: state.local_verify_data.clone(), + }, + ))), + ), + should_encrypt: true, + reset_local_sequence_number: true, + }); + + Ok(pkts) + } +} +async fn initialize_cipher_suite( + state: &mut State, + cache: &HandshakeCache, + cfg: &HandshakeConfig, + h: &HandshakeMessageServerKeyExchange, + sending_plain_text: &[u8], +) -> Result<(), (Option, Option)> { + let mut cipher_suite = state.cipher_suite.lock().await; + + if let Some(cipher_suite) = &*cipher_suite { + if cipher_suite.is_initialized() { + return Ok(()); + } + } + + let mut client_random = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(client_random.as_mut()); + let _ = state.local_random.marshal(&mut writer); + } + let mut server_random = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(server_random.as_mut()); + let _ = state.remote_random.marshal(&mut writer); + } + + if let Some(cipher_suite) = &*cipher_suite { + if state.extended_master_secret { + let session_hash = match cache + .session_hash( + cipher_suite.hash_func(), + cfg.initial_epoch, + sending_plain_text, + ) + .await + { + Ok(s) => s, + Err(err) => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + Some(err), + )) + } + }; + + state.master_secret = match prf_extended_master_secret( + &state.pre_master_secret, + &session_hash, + cipher_suite.hash_func(), + ) { + Ok(m) => m, + Err(err) => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::IllegalParameter, + }), + Some(err), + )) + } + }; + } else { + state.master_secret = match prf_master_secret( + &state.pre_master_secret, + &client_random, + &server_random, + cipher_suite.hash_func(), + ) { + Ok(m) => m, + Err(err) => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + Some(err), + )) + } + }; + } + } + + if cfg.local_psk_callback.is_none() { + // Verify that the pair of hash algorithm and signiture is listed. + let mut valid_signature_scheme = false; + for ss in &cfg.local_signature_schemes { + if ss.hash == h.algorithm.hash && ss.signature == h.algorithm.signature { + valid_signature_scheme = true; + break; + } + } + if !valid_signature_scheme { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InsufficientSecurity, + }), + Some(Error::ErrNoAvailableSignatureSchemes), + )); + } + + let expected_msg = + value_key_message(&client_random, &server_random, &h.public_key, h.named_curve); + if let Err(err) = verify_key_signature( + &expected_msg, + &h.algorithm, + &h.signature, + &state.peer_certificates, + cfg.insecure_verification, + ) { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::BadCertificate, + }), + Some(err), + )); + } + + let mut chains = vec![]; + if !cfg.insecure_skip_verify { + chains = match verify_server_cert( + &state.peer_certificates, + &cfg.server_cert_verifier, + &cfg.server_name, + ) { + Ok(chains) => chains, + Err(err) => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::BadCertificate, + }), + Some(err), + )) + } + } + } + if let Some(verify_peer_certificate) = &cfg.verify_peer_certificate { + if let Err(err) = verify_peer_certificate(&state.peer_certificates, &chains) { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::BadCertificate, + }), + Some(err), + )); + } + } + } + + if let Some(cipher_suite) = &mut *cipher_suite { + if let Err(err) = + cipher_suite.init(&state.master_secret, &client_random, &server_random, true) + { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + Some(err), + )); + } + } + + Ok(()) +} diff --git a/vendor/dtls/src/flight/flight6.rs b/vendor/dtls/src/flight/flight6.rs new file mode 100644 index 00000000..1e9b0036 --- /dev/null +++ b/vendor/dtls/src/flight/flight6.rs @@ -0,0 +1,190 @@ +use std::fmt; + +use async_trait::async_trait; + +use super::*; +use crate::change_cipher_spec::*; +use crate::content::*; +use crate::handshake::handshake_message_finished::*; +use crate::handshake::*; +use crate::prf::*; +use crate::record_layer::record_layer_header::*; + +#[derive(Debug, PartialEq)] +pub(crate) struct Flight6; + +impl fmt::Display for Flight6 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Flight 6") + } +} + +#[async_trait] +impl Flight for Flight6 { + fn is_last_send_flight(&self) -> bool { + true + } + + async fn parse( + &self, + _tx: &mut mpsc::Sender>, + state: &mut State, + cache: &HandshakeCache, + cfg: &HandshakeConfig, + ) -> Result, (Option, Option)> { + let (_, msgs) = match cache + .full_pull_map( + state.handshake_recv_sequence - 1, + &[HandshakeCachePullRule { + typ: HandshakeType::Finished, + epoch: cfg.initial_epoch + 1, + is_client: true, + optional: false, + }], + ) + .await + { + Ok((seq, msgs)) => (seq, msgs), + // No valid message received. Keep reading + Err(_) => return Err((None, None)), + }; + + if let Some(message) = msgs.get(&HandshakeType::Finished) { + match message { + HandshakeMessage::Finished(_) => {} + _ => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + None, + )) + } + }; + } + + // Other party retransmitted the last flight. + Ok(Box::new(Flight6 {})) + } + + async fn generate( + &self, + state: &mut State, + cache: &HandshakeCache, + cfg: &HandshakeConfig, + ) -> Result, (Option, Option)> { + let mut pkts = vec![Packet { + record: RecordLayer::new( + PROTOCOL_VERSION1_2, + 0, + Content::ChangeCipherSpec(ChangeCipherSpec {}), + ), + should_encrypt: false, + reset_local_sequence_number: false, + }]; + + if state.local_verify_data.is_empty() { + let plain_text = cache + .pull_and_merge(&[ + HandshakeCachePullRule { + typ: HandshakeType::ClientHello, + epoch: cfg.initial_epoch, + is_client: true, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::ServerHello, + epoch: cfg.initial_epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::Certificate, + epoch: cfg.initial_epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::ServerKeyExchange, + epoch: cfg.initial_epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::CertificateRequest, + epoch: cfg.initial_epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::ServerHelloDone, + epoch: cfg.initial_epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::Certificate, + epoch: cfg.initial_epoch, + is_client: true, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::ClientKeyExchange, + epoch: cfg.initial_epoch, + is_client: true, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::CertificateVerify, + epoch: cfg.initial_epoch, + is_client: true, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::Finished, + epoch: cfg.initial_epoch + 1, + is_client: true, + optional: false, + }, + ]) + .await; + + let cipher_suite = state.cipher_suite.lock().await; + if let Some(cipher_suite) = &*cipher_suite { + state.local_verify_data = match prf_verify_data_server( + &state.master_secret, + &plain_text, + cipher_suite.hash_func(), + ) { + Ok(data) => data, + Err(err) => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + Some(err), + )) + } + }; + } + } + + pkts.push(Packet { + record: RecordLayer::new( + PROTOCOL_VERSION1_2, + 1, + Content::Handshake(Handshake::new(HandshakeMessage::Finished( + HandshakeMessageFinished { + verify_data: state.local_verify_data.clone(), + }, + ))), + ), + should_encrypt: true, + reset_local_sequence_number: true, + }); + + Ok(pkts) + } +} diff --git a/vendor/dtls/src/flight/mod.rs b/vendor/dtls/src/flight/mod.rs new file mode 100644 index 00000000..8e6b41e6 --- /dev/null +++ b/vendor/dtls/src/flight/mod.rs @@ -0,0 +1,87 @@ +pub(crate) mod flight0; +pub(crate) mod flight1; +pub(crate) mod flight2; +pub(crate) mod flight3; +pub(crate) mod flight4; +pub(crate) mod flight5; +pub(crate) mod flight6; + +use std::fmt; + +use async_trait::async_trait; +use tokio::sync::mpsc; + +use crate::alert::*; +use crate::error::Error; +use crate::handshake::handshake_cache::*; +use crate::handshaker::*; +use crate::record_layer::*; +use crate::state::*; + +/* + DTLS messages are grouped into a series of message flights, according + to the diagrams below. Although each Flight of messages may consist + of a number of messages, they should be viewed as monolithic for the + purpose of timeout and retransmission. + https://tools.ietf.org/html/rfc4347#section-4.2.4 + Client Server + ------ ------ + Waiting Flight 0 + + ClientHello --------> Flight 1 + + <------- HelloVerifyRequest Flight 2 + + ClientHello --------> Flight 3 + + ServerHello \ + Certificate* \ + ServerKeyExchange* Flight 4 + CertificateRequest* / + <-------- ServerHelloDone / + + Certificate* \ + ClientKeyExchange \ + CertificateVerify* Flight 5 + [ChangeCipherSpec] / + Finished --------> / + + [ChangeCipherSpec] \ Flight 6 + <-------- Finished / + +*/ + +#[derive(Clone, Debug)] +pub(crate) struct Packet { + pub(crate) record: RecordLayer, + pub(crate) should_encrypt: bool, + pub(crate) reset_local_sequence_number: bool, +} + +#[async_trait] +pub(crate) trait Flight: fmt::Display + fmt::Debug { + fn is_last_send_flight(&self) -> bool { + false + } + fn is_last_recv_flight(&self) -> bool { + false + } + fn has_retransmit(&self) -> bool { + true + } + + async fn parse( + &self, + tx: &mut mpsc::Sender>, + state: &mut State, + cache: &HandshakeCache, + cfg: &HandshakeConfig, + ) -> Result, (Option, Option)>; + + async fn generate( + &self, + state: &mut State, + cache: &HandshakeCache, + cfg: &HandshakeConfig, + ) -> Result, (Option, Option)>; +} diff --git a/vendor/dtls/src/fragment_buffer/fragment_buffer_test.rs b/vendor/dtls/src/fragment_buffer/fragment_buffer_test.rs new file mode 100644 index 00000000..0e909080 --- /dev/null +++ b/vendor/dtls/src/fragment_buffer/fragment_buffer_test.rs @@ -0,0 +1,174 @@ +use super::*; + +#[test] +fn test_fragment_buffer() -> Result<()> { + let tests = vec![ + ( + "Single Fragment", + vec![vec![ + 0x16, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x03, + 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xfe, 0xff, 0x00, + ]], + vec![vec![ + 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xfe, 0xff, + 0x00, + ]], + 0, + ), + ( + "Single Fragment Epoch 3", + vec![vec![ + 0x16, 0xfe, 0xff, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x03, + 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xfe, 0xff, 0x00, + ]], + vec![vec![ + 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xfe, 0xff, + 0x00, + ]], + 3, + ), + ( + "Multiple Fragments", + vec![ + vec![ + 0x16, 0xfe, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x81, + 0x0b, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, + 0x01, 0x02, 0x03, 0x04, + ], + vec![ + 0x16, 0xfe, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x81, + 0x0b, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x05, + 0x06, 0x07, 0x08, 0x09, + ], + vec![ + 0x16, 0xfe, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x81, + 0x0b, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x05, 0x0A, + 0x0B, 0x0C, 0x0D, 0x0E, + ], + ], + vec![vec![ + 0x0b, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x01, + 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, + ]], + 0, + ), + ( + "Multiple Unordered Fragments", + vec![ + vec![ + 0x16, 0xfe, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x81, + 0x0b, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, + 0x01, 0x02, 0x03, 0x04, + ], + vec![ + 0x16, 0xfe, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x81, + 0x0b, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x05, 0x0A, + 0x0B, 0x0C, 0x0D, 0x0E, + ], + vec![ + 0x16, 0xfe, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x81, + 0x0b, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x05, + 0x06, 0x07, 0x08, 0x09, + ], + ], + vec![vec![ + 0x0b, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x01, + 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, + ]], + 0, + ), + ( + "Multiple Handshakes in Single Fragment", + vec![vec![ + 0x16, 0xfe, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x30, /* record header */ + 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xfe, 0xff, + 0x01, 0x01, /*handshake msg 1*/ + 0x03, 0x00, 0x00, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xfe, 0xff, + 0x01, 0x01, /*handshake msg 2*/ + 0x03, 0x00, 0x00, 0x04, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xfe, 0xff, + 0x01, 0x01, /*handshake msg 3*/ + ]], + vec![ + vec![ + 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xfe, + 0xff, 0x01, 0x01, + ], + vec![ + 0x03, 0x00, 0x00, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xfe, + 0xff, 0x01, 0x01, + ], + vec![ + 0x03, 0x00, 0x00, 0x04, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xfe, + 0xff, 0x01, 0x01, + ], + ], + 0, + ), + // Ensure zero length fragments don't cause an infinite recursive loop which in turn causes + // a stack overflow. + ( + "Zero length fragment", + vec![vec![ + 0x16, 0xfe, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]], + vec![vec![ + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, + ]], + 0, + ), + ]; + + for (name, inputs, expects, expected_epoch) in tests { + let mut fragment_buffer = FragmentBuffer::new(); + for frag in inputs { + let status = fragment_buffer.push(&frag)?; + assert!( + status, + "fragment_buffer didn't accept fragments for '{name}'" + ); + } + + for expected in expects { + let (out, epoch) = fragment_buffer.pop()?; + assert_eq!( + out, expected, + "fragment_buffer '{name}' push/pop: got {out:?}, want {expected:?}" + ); + + assert_eq!( + epoch, expected_epoch, + "fragment_buffer returned wrong epoch: got {epoch}, want {expected_epoch}" + ); + } + + let result = fragment_buffer.pop(); + assert!( + result.is_err(), + "fragment_buffer popped single buffer multiple times for '{name}'" + ); + } + + Ok(()) +} + +#[test] +fn test_fragment_buffer_overflow() -> Result<()> { + let mut fragment_buffer = FragmentBuffer::new(); + + fragment_buffer.push(&[ + 0x16, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x03, 0x00, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xfe, 0xff, 0x00, + ])?; + + let big_buffer = vec![0; 2_000_000]; + let result = fragment_buffer.push(&big_buffer); + + assert!( + result.is_err(), + "Pushing a buffer of size 2MB should have caused FragmentBuffer::push to return an error" + ); + + Ok(()) +} diff --git a/vendor/dtls/src/fragment_buffer/mod.rs b/vendor/dtls/src/fragment_buffer/mod.rs new file mode 100644 index 00000000..d375bc2c --- /dev/null +++ b/vendor/dtls/src/fragment_buffer/mod.rs @@ -0,0 +1,160 @@ +#[cfg(test)] +mod fragment_buffer_test; + +use std::collections::HashMap; +use std::io::{BufWriter, Cursor}; + +use crate::content::*; +use crate::error::*; +use crate::handshake::handshake_header::*; +use crate::record_layer::record_layer_header::*; + +// 2 mb max buffer size +const FRAGMENT_BUFFER_MAX_SIZE: usize = 2_000_000; + +pub(crate) struct Fragment { + record_layer_header: RecordLayerHeader, + handshake_header: HandshakeHeader, + data: Vec, +} + +pub(crate) struct FragmentBuffer { + // map of MessageSequenceNumbers that hold slices of fragments + cache: HashMap>, + + current_message_sequence_number: u16, +} + +impl FragmentBuffer { + pub fn new() -> Self { + FragmentBuffer { + cache: HashMap::new(), + current_message_sequence_number: 0, + } + } + + // Attempts to push a DTLS packet to the FragmentBuffer + // when it returns true it means the FragmentBuffer has inserted and the buffer shouldn't be handled + // when an error returns it is fatal, and the DTLS connection should be stopped + pub fn push(&mut self, mut buf: &[u8]) -> Result { + let current_size = self.size(); + if current_size + buf.len() >= FRAGMENT_BUFFER_MAX_SIZE { + return Err(Error::ErrFragmentBufferOverflow { + new_size: current_size + buf.len(), + max_size: FRAGMENT_BUFFER_MAX_SIZE, + }); + } + + let mut reader = Cursor::new(buf); + let record_layer_header = RecordLayerHeader::unmarshal(&mut reader)?; + + // Fragment isn't a handshake, we don't need to handle it + if record_layer_header.content_type != ContentType::Handshake { + return Ok(false); + } + + buf = &buf[RECORD_LAYER_HEADER_SIZE..]; + while !buf.is_empty() { + let mut reader = Cursor::new(buf); + let handshake_header = HandshakeHeader::unmarshal(&mut reader)?; + + self.cache + .entry(handshake_header.message_sequence) + .or_default(); + + // end index should be the length of handshake header but if the handshake + // was fragmented, we should keep them all + let mut end = HANDSHAKE_HEADER_LENGTH + handshake_header.length as usize; + if end > buf.len() { + end = buf.len(); + } + + // Discard all headers, when rebuilding the packet we will re-build + let data = buf[HANDSHAKE_HEADER_LENGTH..end].to_vec(); + + if let Some(x) = self.cache.get_mut(&handshake_header.message_sequence) { + x.push(Fragment { + record_layer_header, + handshake_header, + data, + }); + } + buf = &buf[end..]; + } + + Ok(true) + } + + pub fn pop(&mut self) -> Result<(Vec, u16)> { + let seq_num = self.current_message_sequence_number; + if !self.cache.contains_key(&seq_num) { + return Err(Error::ErrEmptyFragment); + } + + let (content, epoch) = if let Some(frags) = self.cache.get_mut(&seq_num) { + let mut raw_message = vec![]; + // Recursively collect up + if !append_message(0, frags, &mut raw_message) { + return Err(Error::ErrEmptyFragment); + } + + let mut first_header = frags[0].handshake_header; + first_header.fragment_offset = 0; + first_header.fragment_length = first_header.length; + + let mut raw_header = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(raw_header.as_mut()); + if first_header.marshal(&mut writer).is_err() { + return Err(Error::ErrEmptyFragment); + } + } + + let message_epoch = frags[0].record_layer_header.epoch; + + raw_header.extend_from_slice(&raw_message); + + (raw_header, message_epoch) + } else { + return Err(Error::ErrEmptyFragment); + }; + + self.cache.remove(&seq_num); + self.current_message_sequence_number += 1; + + Ok((content, epoch)) + } + + fn size(&self) -> usize { + self.cache + .values() + .map(|fragment| fragment.iter().map(|f| f.data.len()).sum::()) + .sum() + } +} + +fn append_message(target_offset: u32, frags: &[Fragment], raw_message: &mut Vec) -> bool { + for f in frags { + if f.handshake_header.fragment_offset == target_offset { + let fragment_end = + f.handshake_header.fragment_offset + f.handshake_header.fragment_length; + + // NB: Order here is important, the `f.handshake_header.fragment_length != 0` + // MUST come before the recursive call. + if fragment_end != f.handshake_header.length + && f.handshake_header.fragment_length != 0 + && !append_message(fragment_end, frags, raw_message) + { + return false; + } + + let mut message = vec![]; + message.extend_from_slice(&f.data); + message.extend_from_slice(raw_message); + *raw_message = message; + return true; + } + } + + false +} diff --git a/vendor/dtls/src/handshake/handshake_cache.rs b/vendor/dtls/src/handshake/handshake_cache.rs new file mode 100644 index 00000000..38667bb2 --- /dev/null +++ b/vendor/dtls/src/handshake/handshake_cache.rs @@ -0,0 +1,241 @@ +#[cfg(test)] +mod handshake_cache_test; + +use std::collections::HashMap; +use std::io::BufReader; +use std::sync::Arc; + +use sha2::{Digest, Sha256}; +use tokio::sync::Mutex; + +use crate::cipher_suite::*; +use crate::handshake::*; + +#[derive(Clone, Debug)] +pub(crate) struct HandshakeCacheItem { + typ: HandshakeType, + is_client: bool, + epoch: u16, + message_sequence: u16, + data: Vec, +} + +#[derive(Copy, Clone, Debug)] +pub(crate) struct HandshakeCachePullRule { + pub(crate) typ: HandshakeType, + pub(crate) epoch: u16, + pub(crate) is_client: bool, + pub(crate) optional: bool, +} + +#[derive(Clone)] +pub(crate) struct HandshakeCache { + cache: Arc>>, +} + +impl HandshakeCache { + pub(crate) fn new() -> Self { + HandshakeCache { + cache: Arc::new(Mutex::new(vec![])), + } + } + + pub(crate) async fn push( + &mut self, + data: Vec, + epoch: u16, + message_sequence: u16, + typ: HandshakeType, + is_client: bool, + ) -> bool { + let mut cache = self.cache.lock().await; + + for i in &*cache { + if i.message_sequence == message_sequence && i.is_client == is_client { + return false; + } + } + + cache.push(HandshakeCacheItem { + typ, + is_client, + epoch, + message_sequence, + data, + }); + + true + } + + // returns a list handshakes that match the requested rules + // the list will contain null entries for rules that can't be satisfied + // multiple entries may match a rule, but only the last match is returned (ie ClientHello with cookies) + pub(crate) async fn pull(&self, rules: &[HandshakeCachePullRule]) -> Vec { + let cache = self.cache.lock().await; + + let mut out = vec![]; + for r in rules { + let mut item: Option = None; + for c in &*cache { + if c.typ == r.typ && c.is_client == r.is_client && c.epoch == r.epoch { + if let Some(x) = &item { + if x.message_sequence < c.message_sequence { + item = Some(c.clone()); + } + } else { + item = Some(c.clone()); + } + } + } + + if let Some(c) = item { + out.push(c); + } + } + + out + } + + // full_pull_map pulls all handshakes between rules[0] to rules[len(rules)-1] as map. + pub(crate) async fn full_pull_map( + &self, + start_seq: isize, + rules: &[HandshakeCachePullRule], + ) -> Result<(isize, HashMap)> { + let cache = self.cache.lock().await; + + let mut ci = HashMap::new(); + for r in rules { + let mut item: Option = None; + for c in &*cache { + if c.typ == r.typ && c.is_client == r.is_client && c.epoch == r.epoch { + if let Some(x) = &item { + if x.message_sequence < c.message_sequence { + item = Some(c.clone()); + } + } else { + item = Some(c.clone()); + } + } + } + if !r.optional && item.is_none() { + // Missing mandatory message. + return Err(Error::Other("Missing mandatory message".to_owned())); + } + + if let Some(c) = item { + ci.insert(r.typ, c); + } + } + + let mut out = HashMap::new(); + let mut seq = start_seq; + for r in rules { + let t = r.typ; + if let Some(i) = ci.get(&t) { + let mut reader = BufReader::new(i.data.as_slice()); + let raw_handshake = Handshake::unmarshal(&mut reader)?; + if seq as u16 != raw_handshake.handshake_header.message_sequence { + // There is a gap. Some messages are not arrived. + return Err(Error::Other( + "There is a gap. Some messages are not arrived.".to_owned(), + )); + } + seq += 1; + out.insert(t, raw_handshake.handshake_message); + } + } + + Ok((seq, out)) + } + + // pull_and_merge calls pull and then merges the results, ignoring any null entries + pub(crate) async fn pull_and_merge(&self, rules: &[HandshakeCachePullRule]) -> Vec { + let mut merged = vec![]; + + for p in &self.pull(rules).await { + merged.extend_from_slice(&p.data); + } + + merged + } + + // session_hash returns the session hash for Extended Master Secret support + // https://tools.ietf.org/html/draft-ietf-tls-session-hash-06#section-4 + pub(crate) async fn session_hash( + &self, + hf: CipherSuiteHash, + epoch: u16, + additional: &[u8], + ) -> Result> { + let mut merged = vec![]; + + // Order defined by https://tools.ietf.org/html/rfc5246#section-7.3 + let handshake_buffer = self + .pull(&[ + HandshakeCachePullRule { + typ: HandshakeType::ClientHello, + epoch, + is_client: true, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::ServerHello, + epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::Certificate, + epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::ServerKeyExchange, + epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::CertificateRequest, + epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::ServerHelloDone, + epoch, + is_client: false, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::Certificate, + epoch, + is_client: true, + optional: false, + }, + HandshakeCachePullRule { + typ: HandshakeType::ClientKeyExchange, + epoch, + is_client: true, + optional: false, + }, + ]) + .await; + + for p in &handshake_buffer { + merged.extend_from_slice(&p.data); + } + + merged.extend_from_slice(additional); + + let mut hasher = match hf { + CipherSuiteHash::Sha256 => Sha256::new(), + }; + hasher.update(&merged); + let result = hasher.finalize(); + + Ok(result.as_slice().to_vec()) + } +} diff --git a/vendor/dtls/src/handshake/handshake_cache/handshake_cache_test.rs b/vendor/dtls/src/handshake/handshake_cache/handshake_cache_test.rs new file mode 100644 index 00000000..b17391b1 --- /dev/null +++ b/vendor/dtls/src/handshake/handshake_cache/handshake_cache_test.rs @@ -0,0 +1,658 @@ +use super::*; + +#[tokio::test] +async fn test_handshake_cache_single_push() -> Result<()> { + let tests = vec![ + ( + "Single Push", + vec![HandshakeCacheItem { + typ: 0.into(), + is_client: true, + epoch: 0, + message_sequence: 0, + data: vec![0x00], + }], + vec![HandshakeCachePullRule { + typ: 0.into(), + epoch: 0, + is_client: true, + optional: false, + }], + vec![0x00], + ), + ( + "Multi Push", + vec![ + HandshakeCacheItem { + typ: 0.into(), + is_client: true, + epoch: 0, + message_sequence: 0, + data: vec![0x00], + }, + HandshakeCacheItem { + typ: 1.into(), + is_client: true, + epoch: 0, + message_sequence: 1, + data: vec![0x01], + }, + HandshakeCacheItem { + typ: 2.into(), + is_client: true, + epoch: 0, + message_sequence: 2, + data: vec![0x02], + }, + ], + vec![ + HandshakeCachePullRule { + typ: 0.into(), + epoch: 0, + is_client: true, + optional: false, + }, + HandshakeCachePullRule { + typ: 1.into(), + epoch: 0, + is_client: true, + optional: false, + }, + HandshakeCachePullRule { + typ: 2.into(), + epoch: 0, + is_client: true, + optional: false, + }, + ], + vec![0x00, 0x01, 0x02], + ), + ( + "Multi Push, Rules set order", + vec![ + HandshakeCacheItem { + typ: 2.into(), + is_client: true, + epoch: 0, + message_sequence: 2, + data: vec![0x02], + }, + HandshakeCacheItem { + typ: 0.into(), + is_client: true, + epoch: 0, + message_sequence: 0, + data: vec![0x00], + }, + HandshakeCacheItem { + typ: 1.into(), + is_client: true, + epoch: 0, + message_sequence: 1, + data: vec![0x01], + }, + ], + vec![ + HandshakeCachePullRule { + typ: 0.into(), + epoch: 0, + is_client: true, + optional: false, + }, + HandshakeCachePullRule { + typ: 1.into(), + epoch: 0, + is_client: true, + optional: false, + }, + HandshakeCachePullRule { + typ: 2.into(), + epoch: 0, + is_client: true, + optional: false, + }, + ], + vec![0x00, 0x01, 0x02], + ), + ( + "Multi Push, Dupe Seqnum", + vec![ + HandshakeCacheItem { + typ: 0.into(), + is_client: true, + epoch: 0, + message_sequence: 0, + data: vec![0x00], + }, + HandshakeCacheItem { + typ: 1.into(), + is_client: true, + epoch: 0, + message_sequence: 1, + data: vec![0x01], + }, + HandshakeCacheItem { + typ: 1.into(), + is_client: true, + epoch: 0, + message_sequence: 1, + data: vec![0x01], + }, + ], + vec![ + HandshakeCachePullRule { + typ: 0.into(), + epoch: 0, + is_client: true, + optional: false, + }, + HandshakeCachePullRule { + typ: 1.into(), + epoch: 0, + is_client: true, + optional: false, + }, + ], + vec![0x00, 0x01], + ), + ( + "Multi Push, Dupe Seqnum Client/Server", + vec![ + HandshakeCacheItem { + typ: 0.into(), + is_client: true, + epoch: 0, + message_sequence: 0, + data: vec![0x00], + }, + HandshakeCacheItem { + typ: 1.into(), + is_client: true, + epoch: 0, + message_sequence: 1, + data: vec![0x01], + }, + HandshakeCacheItem { + typ: 1.into(), + is_client: false, + epoch: 0, + message_sequence: 1, + data: vec![0x02], + }, + ], + vec![ + HandshakeCachePullRule { + typ: 0.into(), + epoch: 0, + is_client: true, + optional: false, + }, + HandshakeCachePullRule { + typ: 1.into(), + epoch: 0, + is_client: true, + optional: false, + }, + HandshakeCachePullRule { + typ: 1.into(), + epoch: 0, + is_client: false, + optional: false, + }, + ], + vec![0x00, 0x01, 0x02], + ), + ( + "Multi Push, Dupe Seqnum with Unique HandshakeType", + vec![ + HandshakeCacheItem { + typ: 1.into(), + is_client: true, + epoch: 0, + message_sequence: 0, + data: vec![0x00], + }, + HandshakeCacheItem { + typ: 2.into(), + is_client: true, + epoch: 0, + message_sequence: 1, + data: vec![0x01], + }, + HandshakeCacheItem { + typ: 3.into(), + is_client: false, + epoch: 0, + message_sequence: 0, + data: vec![0x02], + }, + ], + vec![ + HandshakeCachePullRule { + typ: 1.into(), + epoch: 0, + is_client: true, + optional: false, + }, + HandshakeCachePullRule { + typ: 2.into(), + epoch: 0, + is_client: true, + optional: false, + }, + HandshakeCachePullRule { + typ: 3.into(), + epoch: 0, + is_client: false, + optional: false, + }, + ], + vec![0x00, 0x01, 0x02], + ), + ( + "Multi Push, Wrong epoch", + vec![ + HandshakeCacheItem { + typ: 1.into(), + is_client: true, + epoch: 0, + message_sequence: 0, + data: vec![0x00], + }, + HandshakeCacheItem { + typ: 2.into(), + is_client: true, + epoch: 1, + message_sequence: 1, + data: vec![0x01], + }, + HandshakeCacheItem { + typ: 2.into(), + is_client: true, + epoch: 0, + message_sequence: 2, + data: vec![0x11], + }, + HandshakeCacheItem { + typ: 3.into(), + is_client: false, + epoch: 0, + message_sequence: 0, + data: vec![0x02], + }, + HandshakeCacheItem { + typ: 3.into(), + is_client: false, + epoch: 1, + message_sequence: 0, + data: vec![0x12], + }, + HandshakeCacheItem { + typ: 3.into(), + is_client: false, + epoch: 2, + message_sequence: 0, + data: vec![0x12], + }, + ], + vec![ + HandshakeCachePullRule { + typ: 1.into(), + epoch: 0, + is_client: true, + optional: false, + }, + HandshakeCachePullRule { + typ: 2.into(), + epoch: 1, + is_client: true, + optional: false, + }, + HandshakeCachePullRule { + typ: 3.into(), + epoch: 0, + is_client: false, + optional: false, + }, + ], + vec![0x00, 0x01, 0x02], + ), + ]; + + for (name, inputs, rules, expected) in tests { + let mut h = HandshakeCache::new(); + for i in inputs { + h.push(i.data, i.epoch, i.message_sequence, i.typ, i.is_client) + .await; + } + let verify_data = h.pull_and_merge(&rules).await; + assert_eq!( + verify_data, expected, + "handshakeCache '{name}' exp:{expected:?} actual {verify_data:?}", + ); + } + + Ok(()) +} + +#[tokio::test] +async fn test_handshake_cache_session_hash() -> Result<()> { + let tests = vec![ + ( + "Standard Handshake", + vec![ + HandshakeCacheItem { + typ: HandshakeType::ClientHello, + is_client: true, + epoch: 0, + message_sequence: 0, + data: vec![0x00], + }, + HandshakeCacheItem { + typ: HandshakeType::ServerHello, + is_client: false, + epoch: 0, + message_sequence: 1, + data: vec![0x01], + }, + HandshakeCacheItem { + typ: HandshakeType::Certificate, + is_client: false, + epoch: 0, + message_sequence: 2, + data: vec![0x02], + }, + HandshakeCacheItem { + typ: HandshakeType::ServerKeyExchange, + is_client: false, + epoch: 0, + message_sequence: 3, + data: vec![0x03], + }, + HandshakeCacheItem { + typ: HandshakeType::ServerHelloDone, + is_client: false, + epoch: 0, + message_sequence: 4, + data: vec![0x04], + }, + HandshakeCacheItem { + typ: HandshakeType::ClientKeyExchange, + is_client: true, + epoch: 0, + message_sequence: 5, + data: vec![0x05], + }, + ], + vec![ + 0x17, 0xe8, 0x8d, 0xb1, 0x87, 0xaf, 0xd6, 0x2c, 0x16, 0xe5, 0xde, 0xbf, 0x3e, 0x65, + 0x27, 0xcd, 0x00, 0x6b, 0xc0, 0x12, 0xbc, 0x90, 0xb5, 0x1a, 0x81, 0x0c, 0xd8, 0x0c, + 0x2d, 0x51, 0x1f, 0x43, + ], + ), + ( + "Handshake With Client Cert Request", + vec![ + HandshakeCacheItem { + typ: HandshakeType::ClientHello, + is_client: true, + epoch: 0, + message_sequence: 0, + data: vec![0x00], + }, + HandshakeCacheItem { + typ: HandshakeType::ServerHello, + is_client: false, + epoch: 0, + message_sequence: 1, + data: vec![0x01], + }, + HandshakeCacheItem { + typ: HandshakeType::Certificate, + is_client: false, + epoch: 0, + message_sequence: 2, + data: vec![0x02], + }, + HandshakeCacheItem { + typ: HandshakeType::ServerKeyExchange, + is_client: false, + epoch: 0, + message_sequence: 3, + data: vec![0x03], + }, + HandshakeCacheItem { + typ: HandshakeType::CertificateRequest, + is_client: false, + epoch: 0, + message_sequence: 4, + data: vec![0x04], + }, + HandshakeCacheItem { + typ: HandshakeType::ServerHelloDone, + is_client: false, + epoch: 0, + message_sequence: 5, + data: vec![0x05], + }, + HandshakeCacheItem { + typ: HandshakeType::ClientKeyExchange, + is_client: true, + epoch: 0, + message_sequence: 6, + data: vec![0x06], + }, + ], + vec![ + 0x57, 0x35, 0x5a, 0xc3, 0x30, 0x3c, 0x14, 0x8f, 0x11, 0xae, 0xf7, 0xcb, 0x17, 0x94, + 0x56, 0xb9, 0x23, 0x2c, 0xde, 0x33, 0xa8, 0x18, 0xdf, 0xda, 0x2c, 0x2f, 0xcb, 0x93, + 0x25, 0x74, 0x9a, 0x6b, + ], + ), + ( + "Handshake Ignores after ClientKeyExchange", + vec![ + HandshakeCacheItem { + typ: HandshakeType::ClientHello, + is_client: true, + epoch: 0, + message_sequence: 0, + data: vec![0x00], + }, + HandshakeCacheItem { + typ: HandshakeType::ServerHello, + is_client: false, + epoch: 0, + message_sequence: 1, + data: vec![0x01], + }, + HandshakeCacheItem { + typ: HandshakeType::Certificate, + is_client: false, + epoch: 0, + message_sequence: 2, + data: vec![0x02], + }, + HandshakeCacheItem { + typ: HandshakeType::ServerKeyExchange, + is_client: false, + epoch: 0, + message_sequence: 3, + data: vec![0x03], + }, + HandshakeCacheItem { + typ: HandshakeType::CertificateRequest, + is_client: false, + epoch: 0, + message_sequence: 4, + data: vec![0x04], + }, + HandshakeCacheItem { + typ: HandshakeType::ServerHelloDone, + is_client: false, + epoch: 0, + message_sequence: 5, + data: vec![0x05], + }, + HandshakeCacheItem { + typ: HandshakeType::ClientKeyExchange, + is_client: true, + epoch: 0, + message_sequence: 6, + data: vec![0x06], + }, + HandshakeCacheItem { + typ: HandshakeType::CertificateVerify, + is_client: true, + epoch: 0, + message_sequence: 7, + data: vec![0x07], + }, + HandshakeCacheItem { + typ: HandshakeType::Finished, + is_client: true, + epoch: 1, + message_sequence: 7, + data: vec![0x08], + }, + HandshakeCacheItem { + typ: HandshakeType::Finished, + is_client: false, + epoch: 1, + message_sequence: 7, + data: vec![0x09], + }, + ], + vec![ + 0x57, 0x35, 0x5a, 0xc3, 0x30, 0x3c, 0x14, 0x8f, 0x11, 0xae, 0xf7, 0xcb, 0x17, 0x94, + 0x56, 0xb9, 0x23, 0x2c, 0xde, 0x33, 0xa8, 0x18, 0xdf, 0xda, 0x2c, 0x2f, 0xcb, 0x93, + 0x25, 0x74, 0x9a, 0x6b, + ], + ), + ( + "Handshake Ignores wrong epoch", + vec![ + HandshakeCacheItem { + typ: HandshakeType::ClientHello, + is_client: true, + epoch: 0, + message_sequence: 0, + data: vec![0x00], + }, + HandshakeCacheItem { + typ: HandshakeType::ServerHello, + is_client: false, + epoch: 0, + message_sequence: 1, + data: vec![0x01], + }, + HandshakeCacheItem { + typ: HandshakeType::Certificate, + is_client: false, + epoch: 0, + message_sequence: 2, + data: vec![0x02], + }, + HandshakeCacheItem { + typ: HandshakeType::ServerKeyExchange, + is_client: false, + epoch: 0, + message_sequence: 3, + data: vec![0x03], + }, + HandshakeCacheItem { + typ: HandshakeType::CertificateRequest, + is_client: false, + epoch: 0, + message_sequence: 4, + data: vec![0x04], + }, + HandshakeCacheItem { + typ: HandshakeType::ServerHelloDone, + is_client: false, + epoch: 0, + message_sequence: 5, + data: vec![0x05], + }, + HandshakeCacheItem { + typ: HandshakeType::ClientKeyExchange, + is_client: true, + epoch: 0, + message_sequence: 6, + data: vec![0x06], + }, + HandshakeCacheItem { + typ: HandshakeType::CertificateVerify, + is_client: true, + epoch: 0, + message_sequence: 7, + data: vec![0x07], + }, + HandshakeCacheItem { + typ: HandshakeType::Finished, + is_client: true, + epoch: 0, + message_sequence: 7, + data: vec![0xf0], + }, + HandshakeCacheItem { + typ: HandshakeType::Finished, + is_client: false, + epoch: 0, + message_sequence: 7, + data: vec![0xf1], + }, + HandshakeCacheItem { + typ: HandshakeType::Finished, + is_client: true, + epoch: 1, + message_sequence: 7, + data: vec![0x08], + }, + HandshakeCacheItem { + typ: HandshakeType::Finished, + is_client: false, + epoch: 1, + message_sequence: 7, + data: vec![0x09], + }, + HandshakeCacheItem { + typ: HandshakeType::Finished, + is_client: true, + epoch: 0, + message_sequence: 7, + data: vec![0xf0], + }, + HandshakeCacheItem { + typ: HandshakeType::Finished, + is_client: false, + epoch: 0, + message_sequence: 7, + data: vec![0xf1], + }, + ], + vec![ + 0x57, 0x35, 0x5a, 0xc3, 0x30, 0x3c, 0x14, 0x8f, 0x11, 0xae, 0xf7, 0xcb, 0x17, 0x94, + 0x56, 0xb9, 0x23, 0x2c, 0xde, 0x33, 0xa8, 0x18, 0xdf, 0xda, 0x2c, 0x2f, 0xcb, 0x93, + 0x25, 0x74, 0x9a, 0x6b, + ], + ), + ]; + + for (name, inputs, expected) in tests { + let mut h = HandshakeCache::new(); + for i in inputs { + h.push(i.data, i.epoch, i.message_sequence, i.typ, i.is_client) + .await; + } + + let verify_data = h.session_hash(CipherSuiteHash::Sha256, 0, &[]).await?; + + assert_eq!( + verify_data, expected, + "handshakeCacheSessionHassh '{name}' exp: {expected:?} actual {verify_data:?}" + ); + } + + Ok(()) +} diff --git a/vendor/dtls/src/handshake/handshake_header.rs b/vendor/dtls/src/handshake/handshake_header.rs new file mode 100644 index 00000000..70c7610a --- /dev/null +++ b/vendor/dtls/src/handshake/handshake_header.rs @@ -0,0 +1,50 @@ +use std::io::{Read, Write}; + +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; + +use super::*; + +// msg_len for Handshake messages assumes an extra 12 bytes for +// sequence, Fragment and version information +pub(crate) const HANDSHAKE_HEADER_LENGTH: usize = 12; + +#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] +pub struct HandshakeHeader { + pub(crate) handshake_type: HandshakeType, + pub(crate) length: u32, // uint24 in spec + pub(crate) message_sequence: u16, + pub(crate) fragment_offset: u32, // uint24 in spec + pub(crate) fragment_length: u32, // uint24 in spec +} + +impl HandshakeHeader { + pub fn size(&self) -> usize { + 1 + 3 + 2 + 3 + 3 + } + + pub fn marshal(&self, writer: &mut W) -> Result<()> { + writer.write_u8(self.handshake_type as u8)?; + writer.write_u24::(self.length)?; + writer.write_u16::(self.message_sequence)?; + writer.write_u24::(self.fragment_offset)?; + writer.write_u24::(self.fragment_length)?; + + Ok(writer.flush()?) + } + + pub fn unmarshal(reader: &mut R) -> Result { + let handshake_type = reader.read_u8()?.into(); + let length = reader.read_u24::()?; + let message_sequence = reader.read_u16::()?; + let fragment_offset = reader.read_u24::()?; + let fragment_length = reader.read_u24::()?; + + Ok(HandshakeHeader { + handshake_type, + length, + message_sequence, + fragment_offset, + fragment_length, + }) + } +} diff --git a/vendor/dtls/src/handshake/handshake_message_certificate.rs b/vendor/dtls/src/handshake/handshake_message_certificate.rs new file mode 100644 index 00000000..c4e0034e --- /dev/null +++ b/vendor/dtls/src/handshake/handshake_message_certificate.rs @@ -0,0 +1,70 @@ +use std::io::{Read, Write}; + +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; + +use super::*; + +#[cfg(test)] +mod handshake_message_certificate_test; + +const HANDSHAKE_MESSAGE_CERTIFICATE_LENGTH_FIELD_SIZE: usize = 3; + +#[derive(PartialEq, Eq, Debug, Clone)] +pub struct HandshakeMessageCertificate { + pub(crate) certificate: Vec>, +} + +impl HandshakeMessageCertificate { + pub fn handshake_type(&self) -> HandshakeType { + HandshakeType::Certificate + } + + pub fn size(&self) -> usize { + let mut len = 3; + + for r in &self.certificate { + len += HANDSHAKE_MESSAGE_CERTIFICATE_LENGTH_FIELD_SIZE + r.len(); + } + + len + } + + pub fn marshal(&self, writer: &mut W) -> Result<()> { + let mut payload_size = 0; + for r in &self.certificate { + payload_size += HANDSHAKE_MESSAGE_CERTIFICATE_LENGTH_FIELD_SIZE + r.len(); + } + + // Total Payload Size + writer.write_u24::(payload_size as u32)?; + + for r in &self.certificate { + // Certificate Length + writer.write_u24::(r.len() as u32)?; + + // Certificate body + writer.write_all(r)?; + } + + Ok(writer.flush()?) + } + + pub fn unmarshal(reader: &mut R) -> Result { + let mut certificate: Vec> = vec![]; + + let payload_size = reader.read_u24::()? as usize; + let mut offset = 0; + while offset < payload_size { + let certificate_len = reader.read_u24::()? as usize; + offset += HANDSHAKE_MESSAGE_CERTIFICATE_LENGTH_FIELD_SIZE; + + let mut buf = vec![0; certificate_len]; + reader.read_exact(&mut buf)?; + offset += certificate_len; + + certificate.push(buf); + } + + Ok(HandshakeMessageCertificate { certificate }) + } +} diff --git a/vendor/dtls/src/handshake/handshake_message_certificate/handshake_message_certificate_test.rs b/vendor/dtls/src/handshake/handshake_message_certificate/handshake_message_certificate_test.rs new file mode 100644 index 00000000..582d16df --- /dev/null +++ b/vendor/dtls/src/handshake/handshake_message_certificate/handshake_message_certificate_test.rs @@ -0,0 +1,79 @@ +use std::io::{BufReader, BufWriter}; + +use super::*; + +#[test] +fn test_handshake_message_certificate() -> Result<()> { + let raw_certificate = vec![ + 0x00, 0x01, 0x8c, 0x00, 0x01, 0x89, 0x30, 0x82, 0x01, 0x85, 0x30, 0x82, 0x01, 0x2b, 0x02, + 0x14, 0x7d, 0x00, 0xcf, 0x07, 0xfc, 0xe2, 0xb6, 0xb8, 0x3f, 0x72, 0xeb, 0x11, 0x36, 0x1b, + 0xf6, 0x39, 0xf1, 0x3c, 0x33, 0x41, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, + 0x04, 0x03, 0x02, 0x30, 0x45, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, + 0x02, 0x41, 0x55, 0x31, 0x13, 0x30, 0x11, 0x06, 0x03, 0x55, 0x04, 0x08, 0x0c, 0x0a, 0x53, + 0x6f, 0x6d, 0x65, 0x2d, 0x53, 0x74, 0x61, 0x74, 0x65, 0x31, 0x21, 0x30, 0x1f, 0x06, 0x03, + 0x55, 0x04, 0x0a, 0x0c, 0x18, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x20, 0x57, + 0x69, 0x64, 0x67, 0x69, 0x74, 0x73, 0x20, 0x50, 0x74, 0x79, 0x20, 0x4c, 0x74, 0x64, 0x30, + 0x1e, 0x17, 0x0d, 0x31, 0x38, 0x31, 0x30, 0x32, 0x35, 0x30, 0x38, 0x35, 0x31, 0x31, 0x32, + 0x5a, 0x17, 0x0d, 0x31, 0x39, 0x31, 0x30, 0x32, 0x35, 0x30, 0x38, 0x35, 0x31, 0x31, 0x32, + 0x5a, 0x30, 0x45, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x41, + 0x55, 0x31, 0x13, 0x30, 0x11, 0x06, 0x03, 0x55, 0x04, 0x08, 0x0c, 0x0a, 0x53, 0x6f, 0x6d, + 0x65, 0x2d, 0x53, 0x74, 0x61, 0x74, 0x65, 0x31, 0x21, 0x30, 0x1f, 0x06, 0x03, 0x55, 0x04, + 0x0a, 0x0c, 0x18, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x20, 0x57, 0x69, 0x64, + 0x67, 0x69, 0x74, 0x73, 0x20, 0x50, 0x74, 0x79, 0x20, 0x4c, 0x74, 0x64, 0x30, 0x59, 0x30, + 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, + 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0xf9, 0xb1, 0x62, 0xd6, 0x07, 0xae, + 0xc3, 0x36, 0x34, 0xf5, 0xa3, 0x09, 0x39, 0x86, 0xe7, 0x3b, 0x59, 0xf7, 0x4a, 0x1d, 0xf4, + 0x97, 0x4f, 0x91, 0x40, 0x56, 0x1b, 0x3d, 0x6c, 0x5a, 0x38, 0x10, 0x15, 0x58, 0xf5, 0xa4, + 0xcc, 0xdf, 0xd5, 0xf5, 0x4a, 0x35, 0x40, 0x0f, 0x9f, 0x54, 0xb7, 0xe9, 0xe2, 0xae, 0x63, + 0x83, 0x6a, 0x4c, 0xfc, 0xc2, 0x5f, 0x78, 0xa0, 0xbb, 0x46, 0x54, 0xa4, 0xda, 0x30, 0x0a, + 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x03, 0x48, 0x00, 0x30, 0x45, + 0x02, 0x20, 0x47, 0x1a, 0x5f, 0x58, 0x2a, 0x74, 0x33, 0x6d, 0xed, 0xac, 0x37, 0x21, 0xfa, + 0x76, 0x5a, 0x4d, 0x78, 0x68, 0x1a, 0xdd, 0x80, 0xa4, 0xd4, 0xb7, 0x7f, 0x7d, 0x78, 0xb3, + 0xfb, 0xf3, 0x95, 0xfb, 0x02, 0x21, 0x00, 0xc0, 0x73, 0x30, 0xda, 0x2b, 0xc0, 0x0c, 0x9e, + 0xb2, 0x25, 0x0d, 0x46, 0xb0, 0xbc, 0x66, 0x7f, 0x71, 0x66, 0xbf, 0x16, 0xb3, 0x80, 0x78, + 0xd0, 0x0c, 0xef, 0xcc, 0xf5, 0xc1, 0x15, 0x0f, 0x58, + ]; + + let mut reader = BufReader::new(raw_certificate.as_slice()); + let c = HandshakeMessageCertificate::unmarshal(&mut reader)?; + //TODO: add x509 parse + // certificate, err := x509.ParseCertificate(c.certificate[0]) + // if err != nil { + // t.Error(err) + // } + // copyCertificatePrivateMembers(certificate, parsedCertificate) + // if !reflect.DeepEqual(certificate, parsedCertificate) { + // t.Errorf("handshakeMessageCertificate unmarshal: got %#v, want %#v", c, parsedCertificate) + // } + + let mut raw = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); + c.marshal(&mut writer)?; + } + assert_eq!( + raw, raw_certificate, + "handshakeMessageCertificate marshal: got {raw:?}, want {raw_certificate:?}" + ); + + Ok(()) +} + +#[test] +fn test_empty_handshake_message_certificate() -> Result<()> { + let raw_certificate = vec![0x00, 0x00, 0x00]; + + let expected_certificate = HandshakeMessageCertificate { + certificate: vec![], + }; + + let mut reader = BufReader::new(raw_certificate.as_slice()); + let c = HandshakeMessageCertificate::unmarshal(&mut reader)?; + + assert_eq!( + c, expected_certificate, + "handshakeMessageCertificate unmarshal: got {c:?}, want {expected_certificate:?}", + ); + + Ok(()) +} diff --git a/vendor/dtls/src/handshake/handshake_message_certificate_request.rs b/vendor/dtls/src/handshake/handshake_message_certificate_request.rs new file mode 100644 index 00000000..f48cecbb --- /dev/null +++ b/vendor/dtls/src/handshake/handshake_message_certificate_request.rs @@ -0,0 +1,77 @@ +#[cfg(test)] +mod handshake_message_certificate_request_test; + +use std::io::{Read, Write}; + +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; + +use super::*; +use crate::client_certificate_type::*; +use crate::signature_hash_algorithm::*; + +/* +A non-anonymous server can optionally request a certificate from +the client, if appropriate for the selected cipher suite. This +message, if sent, will immediately follow the ServerKeyExchange +message (if it is sent; otherwise, this message follows the +server's Certificate message). +*/ +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HandshakeMessageCertificateRequest { + pub(crate) certificate_types: Vec, + pub(crate) signature_hash_algorithms: Vec, +} + +const HANDSHAKE_MESSAGE_CERTIFICATE_REQUEST_MIN_LENGTH: usize = 5; + +impl HandshakeMessageCertificateRequest { + pub fn handshake_type(&self) -> HandshakeType { + HandshakeType::CertificateRequest + } + + pub fn size(&self) -> usize { + 1 + self.certificate_types.len() + 2 + self.signature_hash_algorithms.len() * 2 + 2 + } + + pub fn marshal(&self, writer: &mut W) -> Result<()> { + writer.write_u8(self.certificate_types.len() as u8)?; + for v in &self.certificate_types { + writer.write_u8(*v as u8)?; + } + + writer.write_u16::(2 * self.signature_hash_algorithms.len() as u16)?; + for v in &self.signature_hash_algorithms { + writer.write_u8(v.hash as u8)?; + writer.write_u8(v.signature as u8)?; + } + + writer.write_all(&[0x00, 0x00])?; // Distinguished Names Length + + Ok(writer.flush()?) + } + + pub fn unmarshal(reader: &mut R) -> Result { + let certificate_types_length = reader.read_u8()?; + + let mut certificate_types = vec![]; + for _ in 0..certificate_types_length { + let cert_type = reader.read_u8()?.into(); + certificate_types.push(cert_type); + } + + let signature_hash_algorithms_length = reader.read_u16::()?; + + let mut signature_hash_algorithms = vec![]; + for _ in (0..signature_hash_algorithms_length).step_by(2) { + let hash = reader.read_u8()?.into(); + let signature = reader.read_u8()?.into(); + + signature_hash_algorithms.push(SignatureHashAlgorithm { hash, signature }); + } + + Ok(HandshakeMessageCertificateRequest { + certificate_types, + signature_hash_algorithms, + }) + } +} diff --git a/vendor/dtls/src/handshake/handshake_message_certificate_request/handshake_message_certificate_request_test.rs b/vendor/dtls/src/handshake/handshake_message_certificate_request/handshake_message_certificate_request_test.rs new file mode 100644 index 00000000..f261cde5 --- /dev/null +++ b/vendor/dtls/src/handshake/handshake_message_certificate_request/handshake_message_certificate_request_test.rs @@ -0,0 +1,64 @@ +use std::io::{BufReader, BufWriter}; + +use super::*; +use crate::signature_hash_algorithm::*; + +#[test] +fn test_handshake_message_certificate_request() -> Result<()> { + let raw_certificate_request = vec![ + 0x02, 0x01, 0x40, 0x00, 0x0C, 0x04, 0x03, 0x04, 0x01, 0x05, 0x03, 0x05, 0x01, 0x06, 0x01, + 0x02, 0x01, 0x00, 0x00, + ]; + + let parsed_certificate_request = HandshakeMessageCertificateRequest { + certificate_types: vec![ + ClientCertificateType::RsaSign, + ClientCertificateType::EcdsaSign, + ], + signature_hash_algorithms: vec![ + SignatureHashAlgorithm { + hash: HashAlgorithm::Sha256, + signature: SignatureAlgorithm::Ecdsa, + }, + SignatureHashAlgorithm { + hash: HashAlgorithm::Sha256, + signature: SignatureAlgorithm::Rsa, + }, + SignatureHashAlgorithm { + hash: HashAlgorithm::Sha384, + signature: SignatureAlgorithm::Ecdsa, + }, + SignatureHashAlgorithm { + hash: HashAlgorithm::Sha384, + signature: SignatureAlgorithm::Rsa, + }, + SignatureHashAlgorithm { + hash: HashAlgorithm::Sha512, + signature: SignatureAlgorithm::Rsa, + }, + SignatureHashAlgorithm { + hash: HashAlgorithm::Sha1, + signature: SignatureAlgorithm::Rsa, + }, + ], + }; + + let mut reader = BufReader::new(raw_certificate_request.as_slice()); + let c = HandshakeMessageCertificateRequest::unmarshal(&mut reader)?; + assert_eq!( + c, parsed_certificate_request, + "parsedCertificateRequest unmarshal: got {c:?}, want {parsed_certificate_request:?}" + ); + + let mut raw = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); + c.marshal(&mut writer)?; + } + assert_eq!( + raw, raw_certificate_request, + "parsedCertificateRequest marshal: got {raw:?}, want {raw_certificate_request:?}" + ); + + Ok(()) +} diff --git a/vendor/dtls/src/handshake/handshake_message_certificate_verify.rs b/vendor/dtls/src/handshake/handshake_message_certificate_verify.rs new file mode 100644 index 00000000..4b535ce1 --- /dev/null +++ b/vendor/dtls/src/handshake/handshake_message_certificate_verify.rs @@ -0,0 +1,52 @@ +#[cfg(test)] +mod handshake_message_certificate_verify_test; + +use std::io::{Read, Write}; + +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; + +use super::*; +use crate::signature_hash_algorithm::*; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HandshakeMessageCertificateVerify { + pub(crate) algorithm: SignatureHashAlgorithm, + pub(crate) signature: Vec, +} + +const HANDSHAKE_MESSAGE_CERTIFICATE_VERIFY_MIN_LENGTH: usize = 4; + +impl HandshakeMessageCertificateVerify { + pub fn handshake_type(&self) -> HandshakeType { + HandshakeType::CertificateVerify + } + + pub fn size(&self) -> usize { + 1 + 1 + 2 + self.signature.len() + } + + pub fn marshal(&self, writer: &mut W) -> Result<()> { + writer.write_u8(self.algorithm.hash as u8)?; + writer.write_u8(self.algorithm.signature as u8)?; + writer.write_u16::(self.signature.len() as u16)?; + writer.write_all(&self.signature)?; + + Ok(writer.flush()?) + } + + pub fn unmarshal(reader: &mut R) -> Result { + let hash_algorithm = reader.read_u8()?.into(); + let signature_algorithm = reader.read_u8()?.into(); + let signature_length = reader.read_u16::()? as usize; + let mut signature = vec![0; signature_length]; + reader.read_exact(&mut signature)?; + + Ok(HandshakeMessageCertificateVerify { + algorithm: SignatureHashAlgorithm { + hash: hash_algorithm, + signature: signature_algorithm, + }, + signature, + }) + } +} diff --git a/vendor/dtls/src/handshake/handshake_message_certificate_verify/handshake_message_certificate_verify_test.rs b/vendor/dtls/src/handshake/handshake_message_certificate_verify/handshake_message_certificate_verify_test.rs new file mode 100644 index 00000000..b52cebc5 --- /dev/null +++ b/vendor/dtls/src/handshake/handshake_message_certificate_verify/handshake_message_certificate_verify_test.rs @@ -0,0 +1,40 @@ +use std::io::{BufReader, BufWriter}; + +use super::*; + +#[test] +fn test_handshake_message_certificate_request() -> Result<()> { + let raw_certificate_verify = vec![ + 0x04, 0x03, 0x00, 0x47, 0x30, 0x45, 0x02, 0x20, 0x6b, 0x63, 0x17, 0xad, 0xbe, 0xb7, 0x7b, + 0x0f, 0x86, 0x73, 0x39, 0x1e, 0xba, 0xb3, 0x50, 0x9c, 0xce, 0x9c, 0xe4, 0x8b, 0xe5, 0x13, + 0x07, 0x59, 0x18, 0x1f, 0xe5, 0xa0, 0x2b, 0xca, 0xa6, 0xad, 0x02, 0x21, 0x00, 0xd3, 0xb5, + 0x01, 0xbe, 0x87, 0x6c, 0x04, 0xa1, 0xdc, 0x28, 0xaa, 0x5f, 0xf7, 0x1e, 0x9c, 0xc0, 0x1e, + 0x00, 0x2c, 0xe5, 0x94, 0xbb, 0x03, 0x0e, 0xf1, 0xcb, 0x28, 0x22, 0x33, 0x23, 0x88, 0xad, + ]; + let parsed_certificate_verify = HandshakeMessageCertificateVerify { + algorithm: SignatureHashAlgorithm { + hash: raw_certificate_verify[0].into(), + signature: raw_certificate_verify[1].into(), + }, + signature: raw_certificate_verify[4..].to_vec(), + }; + + let mut reader = BufReader::new(raw_certificate_verify.as_slice()); + let c = HandshakeMessageCertificateVerify::unmarshal(&mut reader)?; + assert_eq!( + c, parsed_certificate_verify, + "handshakeMessageCertificate unmarshal: got {c:?}, want {parsed_certificate_verify:?}" + ); + + let mut raw = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); + c.marshal(&mut writer)?; + } + assert_eq!( + raw, raw_certificate_verify, + "handshakeMessageCertificateVerify marshal: got {raw:?}, want {raw_certificate_verify:?}" + ); + + Ok(()) +} diff --git a/vendor/dtls/src/handshake/handshake_message_client_hello.rs b/vendor/dtls/src/handshake/handshake_message_client_hello.rs new file mode 100644 index 00000000..f98340e0 --- /dev/null +++ b/vendor/dtls/src/handshake/handshake_message_client_hello.rs @@ -0,0 +1,196 @@ +#[cfg(test)] +mod handshake_message_client_hello_test; + +use std::fmt; +use std::io::{BufReader, BufWriter}; + +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; + +use super::handshake_random::*; +use super::*; +use crate::cipher_suite::*; +use crate::compression_methods::*; +use crate::extension::*; +use crate::record_layer::record_layer_header::*; + +/* +When a client first connects to a server it is required to send +the client hello as its first message. The client can also send a +client hello in response to a hello request or on its own +initiative in order to renegotiate the security parameters in an +existing connection. +*/ +#[derive(Clone)] +pub struct HandshakeMessageClientHello { + pub(crate) version: ProtocolVersion, + pub(crate) random: HandshakeRandom, + pub(crate) cookie: Vec, + + pub(crate) cipher_suites: Vec, + pub(crate) compression_methods: CompressionMethods, + pub(crate) extensions: Vec, +} + +impl PartialEq for HandshakeMessageClientHello { + fn eq(&self, other: &Self) -> bool { + if !(self.version == other.version + && self.random == other.random + && self.cookie == other.cookie + && self.compression_methods == other.compression_methods + && self.extensions == other.extensions + && self.cipher_suites.len() == other.cipher_suites.len()) + { + return false; + } + + for i in 0..self.cipher_suites.len() { + if self.cipher_suites[i] != other.cipher_suites[i] { + return false; + } + } + + true + } +} + +impl fmt::Debug for HandshakeMessageClientHello { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut cipher_suites_str = String::new(); + for cipher_suite in &self.cipher_suites { + cipher_suites_str += &cipher_suite.to_string(); + cipher_suites_str += " "; + } + let s = [ + format!("version: {:?} random: {:?}", self.version, self.random), + format!("cookie: {:?}", self.cookie), + format!("cipher_suites: {cipher_suites_str:?}"), + format!("compression_methods: {:?}", self.compression_methods), + format!("extensions: {:?}", self.extensions), + ]; + write!(f, "{}", s.join(" ")) + } +} + +const HANDSHAKE_MESSAGE_CLIENT_HELLO_VARIABLE_WIDTH_START: usize = 34; + +impl HandshakeMessageClientHello { + pub fn handshake_type(&self) -> HandshakeType { + HandshakeType::ClientHello + } + + pub fn size(&self) -> usize { + let mut len = 0; + + len += 2; // version.major+minor + len += self.random.size(); + + // SessionID + len += 1; + + len += 1 + self.cookie.len(); + + len += 2 + 2 * self.cipher_suites.len(); + + len += self.compression_methods.size(); + + len += 2; + for extension in &self.extensions { + len += extension.size(); + } + + len + } + + pub fn marshal(&self, writer: &mut W) -> Result<()> { + if self.cookie.len() > 255 { + return Err(Error::ErrCookieTooLong); + } + + writer.write_u8(self.version.major)?; + writer.write_u8(self.version.minor)?; + self.random.marshal(writer)?; + + // SessionID + writer.write_u8(0x00)?; + + writer.write_u8(self.cookie.len() as u8)?; + writer.write_all(&self.cookie)?; + + writer.write_u16::(2 * self.cipher_suites.len() as u16)?; + for cipher_suite in &self.cipher_suites { + writer.write_u16::(*cipher_suite as u16)?; + } + + self.compression_methods.marshal(writer)?; + + let mut extension_buffer = vec![]; + { + let mut extension_writer = BufWriter::<&mut Vec>::new(extension_buffer.as_mut()); + for extension in &self.extensions { + extension.marshal(&mut extension_writer)?; + } + } + + writer.write_u16::(extension_buffer.len() as u16)?; + writer.write_all(&extension_buffer)?; + + Ok(writer.flush()?) + } + + pub fn unmarshal(reader: &mut R) -> Result { + let major = reader.read_u8()?; + let minor = reader.read_u8()?; + let random = HandshakeRandom::unmarshal(reader)?; + + // Session ID + reader.read_u8()?; + + let cookie_len = reader.read_u8()? as usize; + let mut cookie = vec![0; cookie_len]; + reader.read_exact(&mut cookie)?; + + let cipher_suites_len = reader.read_u16::()? as usize / 2; + let mut cipher_suites = vec![]; + for _ in 0..cipher_suites_len { + let id: CipherSuiteId = reader.read_u16::()?.into(); + //let cipher_suite = cipher_suite_for_id(id)?; + cipher_suites.push(id); + } + + let compression_methods = CompressionMethods::unmarshal(reader)?; + let mut extensions = vec![]; + + let extension_buffer_len = reader.read_u16::()? as usize; + let mut extension_buffer = vec![0u8; extension_buffer_len]; + reader.read_exact(&mut extension_buffer)?; + + let mut offset = 0; + while offset < extension_buffer_len { + let mut extension_reader = BufReader::new(&extension_buffer[offset..]); + if let Ok(extension) = Extension::unmarshal(&mut extension_reader) { + extensions.push(extension); + } else { + log::warn!( + "Unsupported Extension Type {} {}", + extension_buffer[offset], + extension_buffer[offset + 1] + ); + } + + let extension_len = + u16::from_be_bytes([extension_buffer[offset + 2], extension_buffer[offset + 3]]) + as usize; + offset += 4 + extension_len; + } + + Ok(HandshakeMessageClientHello { + version: ProtocolVersion { major, minor }, + random, + cookie, + + cipher_suites, + compression_methods, + extensions, + }) + } +} diff --git a/vendor/dtls/src/handshake/handshake_message_client_hello/handshake_message_client_hello_test.rs b/vendor/dtls/src/handshake/handshake_message_client_hello/handshake_message_client_hello_test.rs new file mode 100644 index 00000000..ba426031 --- /dev/null +++ b/vendor/dtls/src/handshake/handshake_message_client_hello/handshake_message_client_hello_test.rs @@ -0,0 +1,75 @@ +use std::io::{BufReader, BufWriter}; +use std::time::{Duration, SystemTime}; + +use super::*; +use crate::curve::named_curve::*; +use crate::extension::extension_supported_elliptic_curves::*; + +#[test] +fn test_handshake_message_client_hello() -> Result<()> { + let raw_client_hello = vec![ + 0xfe, 0xfd, 0xb6, 0x2f, 0xce, 0x5c, 0x42, 0x54, 0xff, 0x86, 0xe1, 0x24, 0x41, 0x91, 0x42, + 0x62, 0x15, 0xad, 0x16, 0xc9, 0x15, 0x8d, 0x95, 0x71, 0x8a, 0xbb, 0x22, 0xd7, 0x47, 0xec, + 0xd8, 0x3d, 0xdc, 0x4b, 0x00, 0x14, 0xe6, 0x14, 0x3a, 0x1b, 0x04, 0xea, 0x9e, 0x7a, 0x14, + 0xd6, 0x6c, 0x57, 0xd0, 0x0e, 0x32, 0x85, 0x76, 0x18, 0xde, 0xd8, 0x00, 0x04, 0xc0, 0x2b, + 0xc0, 0x0a, 0x01, 0x00, 0x00, 0x08, 0x00, 0x0a, 0x00, 0x04, 0x00, 0x02, 0x00, 0x1d, + ]; + + let gmt_unix_time = if let Some(unix_time) = + SystemTime::UNIX_EPOCH.checked_add(Duration::new(3056586332u64, 0)) + { + unix_time + } else { + SystemTime::UNIX_EPOCH + }; + let parsed_client_hello = HandshakeMessageClientHello { + version: ProtocolVersion { + major: 0xFE, + minor: 0xFD, + }, + random: HandshakeRandom { + gmt_unix_time, + random_bytes: [ + 0x42, 0x54, 0xff, 0x86, 0xe1, 0x24, 0x41, 0x91, 0x42, 0x62, 0x15, 0xad, 0x16, 0xc9, + 0x15, 0x8d, 0x95, 0x71, 0x8a, 0xbb, 0x22, 0xd7, 0x47, 0xec, 0xd8, 0x3d, 0xdc, 0x4b, + ], + }, + cookie: vec![ + 0xe6, 0x14, 0x3a, 0x1b, 0x04, 0xea, 0x9e, 0x7a, 0x14, 0xd6, 0x6c, 0x57, 0xd0, 0x0e, + 0x32, 0x85, 0x76, 0x18, 0xde, 0xd8, + ], + cipher_suites: vec![ + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256, + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha, + //Box::::default(), + //Box::::default(), + ], + compression_methods: CompressionMethods { + ids: vec![CompressionMethodId::Null], + }, + extensions: vec![Extension::SupportedEllipticCurves( + ExtensionSupportedEllipticCurves { + elliptic_curves: vec![NamedCurve::X25519], + }, + )], + }; + + let mut reader = BufReader::new(raw_client_hello.as_slice()); + let c = HandshakeMessageClientHello::unmarshal(&mut reader)?; + assert_eq!( + c, parsed_client_hello, + "handshakeMessageClientHello unmarshal: got {c:?}, want {parsed_client_hello:?}" + ); + + let mut raw = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); + c.marshal(&mut writer)?; + } + assert_eq!( + raw, raw_client_hello, + "handshakeMessageClientHello marshal: got {raw:?}, want {raw_client_hello:?}" + ); + + Ok(()) +} diff --git a/vendor/dtls/src/handshake/handshake_message_client_key_exchange.rs b/vendor/dtls/src/handshake/handshake_message_client_key_exchange.rs new file mode 100644 index 00000000..4c027ff8 --- /dev/null +++ b/vendor/dtls/src/handshake/handshake_message_client_key_exchange.rs @@ -0,0 +1,70 @@ +#[cfg(test)] +mod handshake_message_client_key_exchange_test; + +use std::io::{Read, Write}; + +use byteorder::{BigEndian, WriteBytesExt}; + +use super::*; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HandshakeMessageClientKeyExchange { + pub(crate) identity_hint: Vec, + pub(crate) public_key: Vec, +} + +impl HandshakeMessageClientKeyExchange { + pub fn handshake_type(&self) -> HandshakeType { + HandshakeType::ClientKeyExchange + } + + pub fn size(&self) -> usize { + if !self.public_key.is_empty() { + 1 + self.public_key.len() + } else { + 2 + self.identity_hint.len() + } + } + + pub fn marshal(&self, writer: &mut W) -> Result<()> { + if (!self.identity_hint.is_empty() && !self.public_key.is_empty()) + || (self.identity_hint.is_empty() && self.public_key.is_empty()) + { + return Err(Error::ErrInvalidClientKeyExchange); + } + + if !self.public_key.is_empty() { + writer.write_u8(self.public_key.len() as u8)?; + writer.write_all(&self.public_key)?; + } else { + writer.write_u16::(self.identity_hint.len() as u16)?; + writer.write_all(&self.identity_hint)?; + } + + Ok(writer.flush()?) + } + + pub fn unmarshal(reader: &mut R) -> Result { + let mut data = vec![]; + reader.read_to_end(&mut data)?; + + // If parsed as PSK return early and only populate PSK Identity Hint + let psk_length = ((data[0] as u16) << 8) | data[1] as u16; + if data.len() == psk_length as usize + 2 { + return Ok(HandshakeMessageClientKeyExchange { + identity_hint: data[2..].to_vec(), + public_key: vec![], + }); + } + + let public_key_length = data[0] as usize; + if data.len() != public_key_length + 1 { + return Err(Error::ErrBufferTooSmall); + } + + Ok(HandshakeMessageClientKeyExchange { + identity_hint: vec![], + public_key: data[1..].to_vec(), + }) + } +} diff --git a/vendor/dtls/src/handshake/handshake_message_client_key_exchange/handshake_message_client_key_exchange_test.rs b/vendor/dtls/src/handshake/handshake_message_client_key_exchange/handshake_message_client_key_exchange_test.rs new file mode 100644 index 00000000..0b9d6c17 --- /dev/null +++ b/vendor/dtls/src/handshake/handshake_message_client_key_exchange/handshake_message_client_key_exchange_test.rs @@ -0,0 +1,35 @@ +use std::io::{BufReader, BufWriter}; + +use super::*; + +#[test] +fn test_handshake_message_client_key_exchange() -> Result<()> { + let raw_client_key_exchange = vec![ + 0x20, 0x26, 0x78, 0x4a, 0x78, 0x70, 0xc1, 0xf9, 0x71, 0xea, 0x50, 0x4a, 0xb5, 0xbb, 0x00, + 0x76, 0x02, 0x05, 0xda, 0xf7, 0xd0, 0x3f, 0xe3, 0xf7, 0x4e, 0x8a, 0x14, 0x6f, 0xb7, 0xe0, + 0xc0, 0xff, 0x54, + ]; + let parsed_client_key_exchange = HandshakeMessageClientKeyExchange { + identity_hint: vec![], + public_key: raw_client_key_exchange[1..].to_vec(), + }; + + let mut reader = BufReader::new(raw_client_key_exchange.as_slice()); + let c = HandshakeMessageClientKeyExchange::unmarshal(&mut reader)?; + assert_eq!( + c, parsed_client_key_exchange, + "parsedCertificateRequest unmarshal: got {c:?}, want {parsed_client_key_exchange:?}" + ); + + let mut raw = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); + c.marshal(&mut writer)?; + } + assert_eq!( + raw, raw_client_key_exchange, + "handshakeMessageClientKeyExchange marshal: got {raw:?}, want {raw_client_key_exchange:?}" + ); + + Ok(()) +} diff --git a/vendor/dtls/src/handshake/handshake_message_finished.rs b/vendor/dtls/src/handshake/handshake_message_finished.rs new file mode 100644 index 00000000..d20feb6e --- /dev/null +++ b/vendor/dtls/src/handshake/handshake_message_finished.rs @@ -0,0 +1,34 @@ +#[cfg(test)] +mod handshake_message_finished_test; + +use std::io::{Read, Write}; + +use super::*; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HandshakeMessageFinished { + pub(crate) verify_data: Vec, +} + +impl HandshakeMessageFinished { + pub fn handshake_type(&self) -> HandshakeType { + HandshakeType::Finished + } + + pub fn size(&self) -> usize { + self.verify_data.len() + } + + pub fn marshal(&self, writer: &mut W) -> Result<()> { + writer.write_all(&self.verify_data)?; + + Ok(writer.flush()?) + } + + pub fn unmarshal(reader: &mut R) -> Result { + let mut verify_data: Vec = vec![]; + reader.read_to_end(&mut verify_data)?; + + Ok(HandshakeMessageFinished { verify_data }) + } +} diff --git a/vendor/dtls/src/handshake/handshake_message_finished/handshake_message_finished_test.rs b/vendor/dtls/src/handshake/handshake_message_finished/handshake_message_finished_test.rs new file mode 100644 index 00000000..7980ee90 --- /dev/null +++ b/vendor/dtls/src/handshake/handshake_message_finished/handshake_message_finished_test.rs @@ -0,0 +1,32 @@ +use std::io::{BufReader, BufWriter}; + +use super::*; + +#[test] +fn test_handshake_message_finished() -> Result<()> { + let raw_finished = vec![ + 0x01, 0x01, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + ]; + let parsed_finished = HandshakeMessageFinished { + verify_data: raw_finished.clone(), + }; + + let mut reader = BufReader::new(raw_finished.as_slice()); + let c = HandshakeMessageFinished::unmarshal(&mut reader)?; + assert_eq!( + c, parsed_finished, + "handshakeMessageFinished unmarshal: got {c:?}, want {parsed_finished:?}" + ); + + let mut raw = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); + c.marshal(&mut writer)?; + } + assert_eq!( + raw, raw_finished, + "handshakeMessageFinished marshal: got {raw:?}, want {raw_finished:?}" + ); + + Ok(()) +} diff --git a/vendor/dtls/src/handshake/handshake_message_hello_verify_request.rs b/vendor/dtls/src/handshake/handshake_message_hello_verify_request.rs new file mode 100644 index 00000000..4e8eb99a --- /dev/null +++ b/vendor/dtls/src/handshake/handshake_message_hello_verify_request.rs @@ -0,0 +1,75 @@ +#[cfg(test)] +mod handshake_message_hello_verify_request_test; + +use std::io::{Read, Write}; + +use byteorder::{ReadBytesExt, WriteBytesExt}; + +use super::*; +use crate::record_layer::record_layer_header::*; + +/* + The definition of HelloVerifyRequest is as follows: + + struct { + ProtocolVersion server_version; + opaque cookie<0..2^8-1>; + } HelloVerifyRequest; + + The HelloVerifyRequest message type is hello_verify_request(3). + + When the client sends its ClientHello message to the server, the server + MAY respond with a HelloVerifyRequest message. This message contains + a stateless cookie generated using the technique of [PHOTURIS]. The + client MUST retransmit the ClientHello with the cookie added. +*/ +/// ## Specifications +/// +/// * [RFC 6347 §4.2.1] +/// +/// [RFC 6347 §4.2.1]: https://tools.ietf.org/html/rfc6347#section-4.2.1 +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HandshakeMessageHelloVerifyRequest { + pub(crate) version: ProtocolVersion, + pub(crate) cookie: Vec, +} + +impl HandshakeMessageHelloVerifyRequest { + pub fn handshake_type(&self) -> HandshakeType { + HandshakeType::HelloVerifyRequest + } + + pub fn size(&self) -> usize { + 1 + 1 + 1 + self.cookie.len() + } + + pub fn marshal(&self, writer: &mut W) -> Result<()> { + if self.cookie.len() > 255 { + return Err(Error::ErrCookieTooLong); + } + + writer.write_u8(self.version.major)?; + writer.write_u8(self.version.minor)?; + writer.write_u8(self.cookie.len() as u8)?; + writer.write_all(&self.cookie)?; + + Ok(writer.flush()?) + } + + pub fn unmarshal(reader: &mut R) -> Result { + let major = reader.read_u8()?; + let minor = reader.read_u8()?; + let cookie_length = reader.read_u8()?; + let mut cookie = vec![]; + reader.read_to_end(&mut cookie)?; + + if cookie.len() < cookie_length as usize { + return Err(Error::ErrBufferTooSmall); + } + + Ok(HandshakeMessageHelloVerifyRequest { + version: ProtocolVersion { major, minor }, + cookie, + }) + } +} diff --git a/vendor/dtls/src/handshake/handshake_message_hello_verify_request/handshake_message_hello_verify_request_test.rs b/vendor/dtls/src/handshake/handshake_message_hello_verify_request/handshake_message_hello_verify_request_test.rs new file mode 100644 index 00000000..5a25ce00 --- /dev/null +++ b/vendor/dtls/src/handshake/handshake_message_hello_verify_request/handshake_message_hello_verify_request_test.rs @@ -0,0 +1,40 @@ +use std::io::{BufReader, BufWriter}; + +use super::*; + +#[test] +fn test_handshake_message_hello_verify_request() -> Result<()> { + let raw_hello_verify_request = vec![ + 0xfe, 0xff, 0x14, 0x25, 0xfb, 0xee, 0xb3, 0x7c, 0x95, 0xcf, 0x00, 0xeb, 0xad, 0xe2, 0xef, + 0xc7, 0xfd, 0xbb, 0xed, 0xf7, 0x1f, 0x6c, 0xcd, + ]; + let parsed_hello_verify_request = HandshakeMessageHelloVerifyRequest { + version: ProtocolVersion { + major: 0xFE, + minor: 0xFF, + }, + cookie: vec![ + 0x25, 0xfb, 0xee, 0xb3, 0x7c, 0x95, 0xcf, 0x00, 0xeb, 0xad, 0xe2, 0xef, 0xc7, 0xfd, + 0xbb, 0xed, 0xf7, 0x1f, 0x6c, 0xcd, + ], + }; + + let mut reader = BufReader::new(raw_hello_verify_request.as_slice()); + let c = HandshakeMessageHelloVerifyRequest::unmarshal(&mut reader)?; + assert_eq!( + c, parsed_hello_verify_request, + "parsed_hello_verify_request unmarshal: got {c:?}, want {parsed_hello_verify_request:?}" + ); + + let mut raw = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); + c.marshal(&mut writer)?; + } + assert_eq!( + raw, raw_hello_verify_request, + "parsed_hello_verify_request marshal: got {raw:?}, want {raw_hello_verify_request:?}" + ); + + Ok(()) +} diff --git a/vendor/dtls/src/handshake/handshake_message_server_hello.rs b/vendor/dtls/src/handshake/handshake_message_server_hello.rs new file mode 100644 index 00000000..6be32e45 --- /dev/null +++ b/vendor/dtls/src/handshake/handshake_message_server_hello.rs @@ -0,0 +1,151 @@ +#[cfg(test)] +mod handshake_message_server_hello_test; + +use std::fmt; +use std::io::{BufReader, BufWriter}; + +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; + +use super::handshake_random::*; +use super::*; +use crate::cipher_suite::*; +use crate::compression_methods::*; +use crate::extension::*; +use crate::record_layer::record_layer_header::*; + +/* +The server will send this message in response to a ClientHello +message when it was able to find an acceptable set of algorithms. +If it cannot find such a match, it will respond with a handshake +failure alert. +https://tools.ietf.org/html/rfc5246#section-7.4.1.3 +*/ +#[derive(Clone)] +pub struct HandshakeMessageServerHello { + pub(crate) version: ProtocolVersion, + pub(crate) random: HandshakeRandom, + + pub(crate) cipher_suite: CipherSuiteId, + pub(crate) compression_method: CompressionMethodId, + pub(crate) extensions: Vec, +} + +impl PartialEq for HandshakeMessageServerHello { + fn eq(&self, other: &Self) -> bool { + self.version == other.version + && self.random == other.random + && self.compression_method == other.compression_method + && self.extensions == other.extensions + && self.cipher_suite == other.cipher_suite + } +} + +impl fmt::Debug for HandshakeMessageServerHello { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = [ + format!("version: {:?} random: {:?}", self.version, self.random), + format!("cipher_suites: {:?}", self.cipher_suite), + format!("compression_method: {:?}", self.compression_method), + format!("extensions: {:?}", self.extensions), + ]; + write!(f, "{}", s.join(" ")) + } +} + +impl HandshakeMessageServerHello { + pub fn handshake_type(&self) -> HandshakeType { + HandshakeType::ServerHello + } + + pub fn size(&self) -> usize { + let mut len = 2 + self.random.size(); + + // SessionID + len += 1; + + len += 2; + + len += 1; + + len += 2; + for extension in &self.extensions { + len += extension.size(); + } + + len + } + + pub fn marshal(&self, writer: &mut W) -> Result<()> { + writer.write_u8(self.version.major)?; + writer.write_u8(self.version.minor)?; + self.random.marshal(writer)?; + + // SessionID + writer.write_u8(0x00)?; + + writer.write_u16::(self.cipher_suite as u16)?; + + writer.write_u8(self.compression_method as u8)?; + + let mut extension_buffer = vec![]; + { + let mut extension_writer = BufWriter::<&mut Vec>::new(extension_buffer.as_mut()); + for extension in &self.extensions { + extension.marshal(&mut extension_writer)?; + } + } + + writer.write_u16::(extension_buffer.len() as u16)?; + writer.write_all(&extension_buffer)?; + + Ok(writer.flush()?) + } + + pub fn unmarshal(reader: &mut R) -> Result { + let major = reader.read_u8()?; + let minor = reader.read_u8()?; + let random = HandshakeRandom::unmarshal(reader)?; + + // Session ID + let session_id_len = reader.read_u8()? as usize; + let mut session_id_buffer = vec![0u8; session_id_len]; + reader.read_exact(&mut session_id_buffer)?; + + let cipher_suite: CipherSuiteId = reader.read_u16::()?.into(); + + let compression_method = reader.read_u8()?.into(); + let mut extensions = vec![]; + + let extension_buffer_len = reader.read_u16::()? as usize; + let mut extension_buffer = vec![0u8; extension_buffer_len]; + reader.read_exact(&mut extension_buffer)?; + + let mut offset = 0; + while offset < extension_buffer_len { + let mut extension_reader = BufReader::new(&extension_buffer[offset..]); + if let Ok(extension) = Extension::unmarshal(&mut extension_reader) { + extensions.push(extension); + } else { + log::warn!( + "Unsupported Extension Type {} {}", + extension_buffer[offset], + extension_buffer[offset + 1] + ); + } + + let extension_len = + u16::from_be_bytes([extension_buffer[offset + 2], extension_buffer[offset + 3]]) + as usize; + offset += 4 + extension_len; + } + + Ok(HandshakeMessageServerHello { + version: ProtocolVersion { major, minor }, + random, + + cipher_suite, + compression_method, + extensions, + }) + } +} diff --git a/vendor/dtls/src/handshake/handshake_message_server_hello/handshake_message_server_hello_test.rs b/vendor/dtls/src/handshake/handshake_message_server_hello/handshake_message_server_hello_test.rs new file mode 100644 index 00000000..ba906e06 --- /dev/null +++ b/vendor/dtls/src/handshake/handshake_message_server_hello/handshake_message_server_hello_test.rs @@ -0,0 +1,56 @@ +use std::io::{BufReader, BufWriter}; +use std::time::{Duration, SystemTime}; + +use super::*; + +#[test] +fn test_handshake_message_server_hello() -> Result<()> { + let raw_server_hello = vec![ + 0xfe, 0xfd, 0x21, 0x63, 0x32, 0x21, 0x81, 0x0e, 0x98, 0x6c, 0x85, 0x3d, 0xa4, 0x39, 0xaf, + 0x5f, 0xd6, 0x5c, 0xcc, 0x20, 0x7f, 0x7c, 0x78, 0xf1, 0x5f, 0x7e, 0x1c, 0xb7, 0xa1, 0x1e, + 0xcf, 0x63, 0x84, 0x28, 0x00, 0xc0, 0x2b, 0x00, 0x00, 0x00, + ]; + + let gmt_unix_time = if let Some(unix_time) = + SystemTime::UNIX_EPOCH.checked_add(Duration::new(560149025u64, 0)) + { + unix_time + } else { + SystemTime::UNIX_EPOCH + }; + let parsed_server_hello = HandshakeMessageServerHello { + version: ProtocolVersion { + major: 0xFE, + minor: 0xFD, + }, + random: HandshakeRandom { + gmt_unix_time, + random_bytes: [ + 0x81, 0x0e, 0x98, 0x6c, 0x85, 0x3d, 0xa4, 0x39, 0xaf, 0x5f, 0xd6, 0x5c, 0xcc, 0x20, + 0x7f, 0x7c, 0x78, 0xf1, 0x5f, 0x7e, 0x1c, 0xb7, 0xa1, 0x1e, 0xcf, 0x63, 0x84, 0x28, + ], + }, + cipher_suite: CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256, + compression_method: CompressionMethodId::Null, + extensions: vec![], + }; + + let mut reader = BufReader::new(raw_server_hello.as_slice()); + let c = HandshakeMessageServerHello::unmarshal(&mut reader)?; + assert_eq!( + c, parsed_server_hello, + "handshakeMessageServerHello unmarshal: got {c:?}, want {parsed_server_hello:?}" + ); + + let mut raw = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); + c.marshal(&mut writer)?; + } + assert_eq!( + raw, raw_server_hello, + "handshakeMessageServerHello marshal: got {raw:?}, want {raw_server_hello:?}" + ); + + Ok(()) +} diff --git a/vendor/dtls/src/handshake/handshake_message_server_hello_done.rs b/vendor/dtls/src/handshake/handshake_message_server_hello_done.rs new file mode 100644 index 00000000..bae58f21 --- /dev/null +++ b/vendor/dtls/src/handshake/handshake_message_server_hello_done.rs @@ -0,0 +1,27 @@ +#[cfg(test)] +mod handshake_message_server_hello_done_test; + +use std::io::{Read, Write}; + +use super::*; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HandshakeMessageServerHelloDone; + +impl HandshakeMessageServerHelloDone { + pub fn handshake_type(&self) -> HandshakeType { + HandshakeType::ServerHelloDone + } + + pub fn size(&self) -> usize { + 0 + } + + pub fn marshal(&self, _writer: &mut W) -> Result<()> { + Ok(()) + } + + pub fn unmarshal(_reader: &mut R) -> Result { + Ok(HandshakeMessageServerHelloDone {}) + } +} diff --git a/vendor/dtls/src/handshake/handshake_message_server_hello_done/handshake_message_server_hello_done_test.rs b/vendor/dtls/src/handshake/handshake_message_server_hello_done/handshake_message_server_hello_done_test.rs new file mode 100644 index 00000000..b9d3b835 --- /dev/null +++ b/vendor/dtls/src/handshake/handshake_message_server_hello_done/handshake_message_server_hello_done_test.rs @@ -0,0 +1,28 @@ +use std::io::{BufReader, BufWriter}; + +use super::*; + +#[test] +fn test_handshake_message_server_hello_done() -> Result<()> { + let raw_server_hello_done = vec![]; + let parsed_server_hello_done = HandshakeMessageServerHelloDone {}; + + let mut reader = BufReader::new(raw_server_hello_done.as_slice()); + let c = HandshakeMessageServerHelloDone::unmarshal(&mut reader)?; + assert_eq!( + c, parsed_server_hello_done, + "handshakeMessageServerHelloDone unmarshal: got {c:?}, want {parsed_server_hello_done:?}" + ); + + let mut raw = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); + c.marshal(&mut writer)?; + } + assert_eq!( + raw, raw_server_hello_done, + "handshakeMessageServerHelloDone marshal: got {raw:?}, want {raw_server_hello_done:?}" + ); + + Ok(()) +} diff --git a/vendor/dtls/src/handshake/handshake_message_server_key_exchange.rs b/vendor/dtls/src/handshake/handshake_message_server_key_exchange.rs new file mode 100644 index 00000000..e8438427 --- /dev/null +++ b/vendor/dtls/src/handshake/handshake_message_server_key_exchange.rs @@ -0,0 +1,133 @@ +#[cfg(test)] +mod handshake_message_server_key_exchange_test; + +use std::io::{Read, Write}; + +use byteorder::{BigEndian, WriteBytesExt}; + +use super::*; +use crate::curve::named_curve::*; +use crate::curve::*; +use crate::signature_hash_algorithm::*; + +// Structure supports ECDH and PSK +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HandshakeMessageServerKeyExchange { + pub(crate) identity_hint: Vec, + + pub(crate) elliptic_curve_type: EllipticCurveType, + pub(crate) named_curve: NamedCurve, + pub(crate) public_key: Vec, + pub(crate) algorithm: SignatureHashAlgorithm, + pub(crate) signature: Vec, +} + +impl HandshakeMessageServerKeyExchange { + pub fn handshake_type(&self) -> HandshakeType { + HandshakeType::ServerKeyExchange + } + + pub fn size(&self) -> usize { + if !self.identity_hint.is_empty() { + 2 + self.identity_hint.len() + } else { + 1 + 2 + 1 + self.public_key.len() + 2 + 2 + self.signature.len() + } + } + + pub fn marshal(&self, writer: &mut W) -> Result<()> { + if !self.identity_hint.is_empty() { + writer.write_u16::(self.identity_hint.len() as u16)?; + writer.write_all(&self.identity_hint)?; + return Ok(writer.flush()?); + } + + writer.write_u8(self.elliptic_curve_type as u8)?; + writer.write_u16::(self.named_curve as u16)?; + + writer.write_u8(self.public_key.len() as u8)?; + writer.write_all(&self.public_key)?; + + writer.write_u8(self.algorithm.hash as u8)?; + writer.write_u8(self.algorithm.signature as u8)?; + + writer.write_u16::(self.signature.len() as u16)?; + writer.write_all(&self.signature)?; + + Ok(writer.flush()?) + } + + pub fn unmarshal(reader: &mut R) -> Result { + let mut data = vec![]; + reader.read_to_end(&mut data)?; + + // If parsed as PSK return early and only populate PSK Identity Hint + let psk_length = ((data[0] as u16) << 8) | data[1] as u16; + if data.len() == psk_length as usize + 2 { + return Ok(HandshakeMessageServerKeyExchange { + identity_hint: data[2..].to_vec(), + + elliptic_curve_type: EllipticCurveType::Unsupported, + named_curve: NamedCurve::Unsupported, + public_key: vec![], + algorithm: SignatureHashAlgorithm { + hash: HashAlgorithm::Unsupported, + signature: SignatureAlgorithm::Unsupported, + }, + signature: vec![], + }); + } + + let elliptic_curve_type = data[0].into(); + if data[1..].len() < 2 { + return Err(Error::ErrBufferTooSmall); + } + + let named_curve = (((data[1] as u16) << 8) | data[2] as u16).into(); + if data.len() < 4 { + return Err(Error::ErrBufferTooSmall); + } + + let public_key_length = data[3] as usize; + let mut offset = 4 + public_key_length; + if data.len() < offset { + return Err(Error::ErrBufferTooSmall); + } + let public_key = data[4..offset].to_vec(); + if data.len() <= offset { + return Err(Error::ErrBufferTooSmall); + } + + let hash_algorithm = data[offset].into(); + offset += 1; + if data.len() <= offset { + return Err(Error::ErrBufferTooSmall); + } + + let signature_algorithm = data[offset].into(); + offset += 1; + if data.len() < offset + 2 { + return Err(Error::ErrBufferTooSmall); + } + + let signature_length = (((data[offset] as u16) << 8) | data[offset + 1] as u16) as usize; + offset += 2; + if data.len() < offset + signature_length { + return Err(Error::ErrBufferTooSmall); + } + let signature = data[offset..offset + signature_length].to_vec(); + + Ok(HandshakeMessageServerKeyExchange { + identity_hint: vec![], + + elliptic_curve_type, + named_curve, + public_key, + algorithm: SignatureHashAlgorithm { + hash: hash_algorithm, + signature: signature_algorithm, + }, + signature, + }) + } +} diff --git a/vendor/dtls/src/handshake/handshake_message_server_key_exchange/handshake_message_server_key_exchange_test.rs b/vendor/dtls/src/handshake/handshake_message_server_key_exchange/handshake_message_server_key_exchange_test.rs new file mode 100644 index 00000000..4fd7adf5 --- /dev/null +++ b/vendor/dtls/src/handshake/handshake_message_server_key_exchange/handshake_message_server_key_exchange_test.rs @@ -0,0 +1,50 @@ +use std::io::{BufReader, BufWriter}; + +use super::*; + +#[test] +fn test_handshake_message_server_key_exchange() -> Result<()> { + let raw_server_key_exchange = vec![ + 0x03, 0x00, 0x1d, 0x41, 0x04, 0x0c, 0xb9, 0xa3, 0xb9, 0x90, 0x71, 0x35, 0x4a, 0x08, 0x66, + 0xaf, 0xd6, 0x88, 0x58, 0x29, 0x69, 0x98, 0xf1, 0x87, 0x0f, 0xb5, 0xa8, 0xcd, 0x92, 0xf6, + 0x2b, 0x08, 0x0c, 0xd4, 0x16, 0x5b, 0xcc, 0x81, 0xf2, 0x58, 0x91, 0x8e, 0x62, 0xdf, 0xc1, + 0xec, 0x72, 0xe8, 0x47, 0x24, 0x42, 0x96, 0xb8, 0x7b, 0xee, 0xe7, 0x0d, 0xdc, 0x44, 0xec, + 0xf3, 0x97, 0x6b, 0x1b, 0x45, 0x28, 0xac, 0x3f, 0x35, 0x02, 0x03, 0x00, 0x47, 0x30, 0x45, + 0x02, 0x21, 0x00, 0xb2, 0x0b, 0x22, 0x95, 0x3d, 0x56, 0x57, 0x6a, 0x3f, 0x85, 0x30, 0x6f, + 0x55, 0xc3, 0xf4, 0x24, 0x1b, 0x21, 0x07, 0xe5, 0xdf, 0xba, 0x24, 0x02, 0x68, 0x95, 0x1f, + 0x6e, 0x13, 0xbd, 0x9f, 0xaa, 0x02, 0x20, 0x49, 0x9c, 0x9d, 0xdf, 0x84, 0x60, 0x33, 0x27, + 0x96, 0x9e, 0x58, 0x6d, 0x72, 0x13, 0xe7, 0x3a, 0xe8, 0xdf, 0x43, 0x75, 0xc7, 0xb9, 0x37, + 0x6e, 0x90, 0xe5, 0x3b, 0x81, 0xd4, 0xda, 0x68, 0xcd, + ]; + let parsed_server_key_exchange = HandshakeMessageServerKeyExchange { + identity_hint: vec![], + elliptic_curve_type: EllipticCurveType::NamedCurve, + named_curve: NamedCurve::X25519, + public_key: raw_server_key_exchange[4..69].to_vec(), + algorithm: SignatureHashAlgorithm { + hash: HashAlgorithm::Sha1, + signature: SignatureAlgorithm::Ecdsa, + }, + + signature: raw_server_key_exchange[73..144].to_vec(), + }; + + let mut reader = BufReader::new(raw_server_key_exchange.as_slice()); + let c = HandshakeMessageServerKeyExchange::unmarshal(&mut reader)?; + assert_eq!( + c, parsed_server_key_exchange, + "handshakeMessageServerKeyExchange unmarshal: got {c:?}, want {parsed_server_key_exchange:?}" + ); + + let mut raw = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); + c.marshal(&mut writer)?; + } + assert_eq!( + raw, raw_server_key_exchange, + "handshakeMessageServerKeyExchange marshal: got {raw:?}, want {raw_server_key_exchange:?}" + ); + + Ok(()) +} diff --git a/vendor/dtls/src/handshake/handshake_random.rs b/vendor/dtls/src/handshake/handshake_random.rs new file mode 100644 index 00000000..505ab06e --- /dev/null +++ b/vendor/dtls/src/handshake/handshake_random.rs @@ -0,0 +1,71 @@ +use std::io::{self, Read, Write}; +use std::time::{Duration, SystemTime}; + +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use rand::Rng; + +pub const RANDOM_BYTES_LENGTH: usize = 28; +pub const HANDSHAKE_RANDOM_LENGTH: usize = RANDOM_BYTES_LENGTH + 4; + +/// ## Specifications +/// +/// * [RFC 4346 §7.4.1.2] +/// +/// [RFC 4346 §7.4.1.2]: https://tools.ietf.org/html/rfc4346#section-7.4.1.2 +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HandshakeRandom { + pub gmt_unix_time: SystemTime, + pub random_bytes: [u8; RANDOM_BYTES_LENGTH], +} + +impl Default for HandshakeRandom { + fn default() -> Self { + HandshakeRandom { + gmt_unix_time: SystemTime::UNIX_EPOCH, + random_bytes: [0u8; RANDOM_BYTES_LENGTH], + } + } +} + +impl HandshakeRandom { + pub fn size(&self) -> usize { + 4 + RANDOM_BYTES_LENGTH + } + + pub fn marshal(&self, writer: &mut W) -> io::Result<()> { + let secs = match self.gmt_unix_time.duration_since(SystemTime::UNIX_EPOCH) { + Ok(d) => d.as_secs() as u32, + Err(_) => 0, + }; + writer.write_u32::(secs)?; + writer.write_all(&self.random_bytes)?; + + writer.flush() + } + + pub fn unmarshal(reader: &mut R) -> io::Result { + let secs = reader.read_u32::()?; + let gmt_unix_time = if let Some(unix_time) = + SystemTime::UNIX_EPOCH.checked_add(Duration::new(secs as u64, 0)) + { + unix_time + } else { + SystemTime::UNIX_EPOCH + }; + + let mut random_bytes = [0u8; RANDOM_BYTES_LENGTH]; + reader.read_exact(&mut random_bytes)?; + + Ok(HandshakeRandom { + gmt_unix_time, + random_bytes, + }) + } + + // populate fills the HandshakeRandom with random values + // may be called multiple times + pub fn populate(&mut self) { + self.gmt_unix_time = SystemTime::now(); + rand::rng().fill(&mut self.random_bytes); + } +} diff --git a/vendor/dtls/src/handshake/handshake_test.rs b/vendor/dtls/src/handshake/handshake_test.rs new file mode 100644 index 00000000..a61da45c --- /dev/null +++ b/vendor/dtls/src/handshake/handshake_test.rs @@ -0,0 +1,70 @@ +use std::io::{BufReader, BufWriter}; +use std::time::{Duration, SystemTime}; + +use super::*; +use crate::compression_methods::*; +use crate::handshake::handshake_message_client_hello::*; +use crate::handshake::handshake_random::HandshakeRandom; +use crate::record_layer::record_layer_header::ProtocolVersion; + +#[test] +fn test_handshake_message() -> Result<()> { + let raw_handshake_message = vec![ + 0x01, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x29, 0xfe, 0xfd, 0xb6, + 0x2f, 0xce, 0x5c, 0x42, 0x54, 0xff, 0x86, 0xe1, 0x24, 0x41, 0x91, 0x42, 0x62, 0x15, 0xad, + 0x16, 0xc9, 0x15, 0x8d, 0x95, 0x71, 0x8a, 0xbb, 0x22, 0xd7, 0x47, 0xec, 0xd8, 0x3d, 0xdc, + 0x4b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + let parsed_handshake = Handshake { + handshake_header: HandshakeHeader { + handshake_type: HandshakeType::ClientHello, + length: 0x29, + message_sequence: 0, + fragment_offset: 0, + fragment_length: 0x29, + }, + handshake_message: HandshakeMessage::ClientHello(HandshakeMessageClientHello { + version: ProtocolVersion { + major: 0xFE, + minor: 0xFD, + }, + random: HandshakeRandom { + gmt_unix_time: if let Some(unix_time) = + SystemTime::UNIX_EPOCH.checked_add(Duration::new(3056586332u64, 0)) + { + unix_time + } else { + SystemTime::UNIX_EPOCH + }, + random_bytes: [ + 0x42, 0x54, 0xff, 0x86, 0xe1, 0x24, 0x41, 0x91, 0x42, 0x62, 0x15, 0xad, 0x16, + 0xc9, 0x15, 0x8d, 0x95, 0x71, 0x8a, 0xbb, 0x22, 0xd7, 0x47, 0xec, 0xd8, 0x3d, + 0xdc, 0x4b, + ], + }, + cookie: vec![], + cipher_suites: vec![], + compression_methods: CompressionMethods { ids: vec![] }, + extensions: vec![], + }), + }; + + let mut reader = BufReader::new(raw_handshake_message.as_slice()); + let h = Handshake::unmarshal(&mut reader)?; + assert_eq!( + h, parsed_handshake, + "handshakeMessageClientHello unmarshal: got {h:?}, want {parsed_handshake:?}" + ); + + let mut raw = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(raw.as_mut()); + h.marshal(&mut writer)?; + } + assert_eq!( + raw, raw_handshake_message, + "handshakeMessageClientHello marshal: got {raw:?}, want {raw_handshake_message:?}" + ); + + Ok(()) +} diff --git a/vendor/dtls/src/handshake/mod.rs b/vendor/dtls/src/handshake/mod.rs new file mode 100644 index 00000000..0b0b3bff --- /dev/null +++ b/vendor/dtls/src/handshake/mod.rs @@ -0,0 +1,242 @@ +pub mod handshake_cache; +pub mod handshake_header; +pub mod handshake_message_certificate; +pub mod handshake_message_certificate_request; +pub mod handshake_message_certificate_verify; +pub mod handshake_message_client_hello; +pub mod handshake_message_client_key_exchange; +pub mod handshake_message_finished; +pub mod handshake_message_hello_verify_request; +pub mod handshake_message_server_hello; +pub mod handshake_message_server_hello_done; +pub mod handshake_message_server_key_exchange; +pub mod handshake_random; + +#[cfg(test)] +mod handshake_test; + +use std::fmt; +use std::io::{Read, Write}; + +use handshake_header::*; +use handshake_message_certificate::*; +use handshake_message_certificate_request::*; +use handshake_message_certificate_verify::*; +use handshake_message_client_hello::*; +use handshake_message_client_key_exchange::*; +use handshake_message_finished::*; +use handshake_message_hello_verify_request::*; +use handshake_message_server_hello::*; +use handshake_message_server_hello_done::*; +use handshake_message_server_key_exchange::*; + +use super::content::*; +use super::error::*; + +/// ## Specifications +/// +/// * [RFC 5246 §7.4] +/// +/// [RFC 5246 §7.4]: https://tools.ietf.org/html/rfc5246#section-7.4 +#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub enum HandshakeType { + HelloRequest = 0, + ClientHello = 1, + ServerHello = 2, + HelloVerifyRequest = 3, + Certificate = 11, + ServerKeyExchange = 12, + CertificateRequest = 13, + ServerHelloDone = 14, + CertificateVerify = 15, + ClientKeyExchange = 16, + Finished = 20, + #[default] + Invalid, +} + +impl fmt::Display for HandshakeType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + HandshakeType::HelloRequest => write!(f, "HelloRequest"), + HandshakeType::ClientHello => write!(f, "ClientHello"), + HandshakeType::ServerHello => write!(f, "ServerHello"), + HandshakeType::HelloVerifyRequest => write!(f, "HelloVerifyRequest"), + HandshakeType::Certificate => write!(f, "Certificate"), + HandshakeType::ServerKeyExchange => write!(f, "ServerKeyExchange"), + HandshakeType::CertificateRequest => write!(f, "CertificateRequest"), + HandshakeType::ServerHelloDone => write!(f, "ServerHelloDone"), + HandshakeType::CertificateVerify => write!(f, "CertificateVerify"), + HandshakeType::ClientKeyExchange => write!(f, "ClientKeyExchange"), + HandshakeType::Finished => write!(f, "Finished"), + HandshakeType::Invalid => write!(f, "Invalid"), + } + } +} + +impl From for HandshakeType { + fn from(val: u8) -> Self { + match val { + 0 => HandshakeType::HelloRequest, + 1 => HandshakeType::ClientHello, + 2 => HandshakeType::ServerHello, + 3 => HandshakeType::HelloVerifyRequest, + 11 => HandshakeType::Certificate, + 12 => HandshakeType::ServerKeyExchange, + 13 => HandshakeType::CertificateRequest, + 14 => HandshakeType::ServerHelloDone, + 15 => HandshakeType::CertificateVerify, + 16 => HandshakeType::ClientKeyExchange, + 20 => HandshakeType::Finished, + _ => HandshakeType::Invalid, + } + } +} + +#[derive(PartialEq, Debug, Clone)] +pub enum HandshakeMessage { + //HelloRequest(errNotImplemented), + ClientHello(HandshakeMessageClientHello), + ServerHello(HandshakeMessageServerHello), + HelloVerifyRequest(HandshakeMessageHelloVerifyRequest), + Certificate(HandshakeMessageCertificate), + ServerKeyExchange(HandshakeMessageServerKeyExchange), + CertificateRequest(HandshakeMessageCertificateRequest), + ServerHelloDone(HandshakeMessageServerHelloDone), + CertificateVerify(HandshakeMessageCertificateVerify), + ClientKeyExchange(HandshakeMessageClientKeyExchange), + Finished(HandshakeMessageFinished), +} + +impl HandshakeMessage { + pub fn handshake_type(&self) -> HandshakeType { + match self { + HandshakeMessage::ClientHello(msg) => msg.handshake_type(), + HandshakeMessage::ServerHello(msg) => msg.handshake_type(), + HandshakeMessage::HelloVerifyRequest(msg) => msg.handshake_type(), + HandshakeMessage::Certificate(msg) => msg.handshake_type(), + HandshakeMessage::ServerKeyExchange(msg) => msg.handshake_type(), + HandshakeMessage::CertificateRequest(msg) => msg.handshake_type(), + HandshakeMessage::ServerHelloDone(msg) => msg.handshake_type(), + HandshakeMessage::CertificateVerify(msg) => msg.handshake_type(), + HandshakeMessage::ClientKeyExchange(msg) => msg.handshake_type(), + HandshakeMessage::Finished(msg) => msg.handshake_type(), + } + } + + pub fn size(&self) -> usize { + match self { + HandshakeMessage::ClientHello(msg) => msg.size(), + HandshakeMessage::ServerHello(msg) => msg.size(), + HandshakeMessage::HelloVerifyRequest(msg) => msg.size(), + HandshakeMessage::Certificate(msg) => msg.size(), + HandshakeMessage::ServerKeyExchange(msg) => msg.size(), + HandshakeMessage::CertificateRequest(msg) => msg.size(), + HandshakeMessage::ServerHelloDone(msg) => msg.size(), + HandshakeMessage::CertificateVerify(msg) => msg.size(), + HandshakeMessage::ClientKeyExchange(msg) => msg.size(), + HandshakeMessage::Finished(msg) => msg.size(), + } + } + + pub fn marshal(&self, writer: &mut W) -> Result<()> { + match self { + HandshakeMessage::ClientHello(msg) => msg.marshal(writer)?, + HandshakeMessage::ServerHello(msg) => msg.marshal(writer)?, + HandshakeMessage::HelloVerifyRequest(msg) => msg.marshal(writer)?, + HandshakeMessage::Certificate(msg) => msg.marshal(writer)?, + HandshakeMessage::ServerKeyExchange(msg) => msg.marshal(writer)?, + HandshakeMessage::CertificateRequest(msg) => msg.marshal(writer)?, + HandshakeMessage::ServerHelloDone(msg) => msg.marshal(writer)?, + HandshakeMessage::CertificateVerify(msg) => msg.marshal(writer)?, + HandshakeMessage::ClientKeyExchange(msg) => msg.marshal(writer)?, + HandshakeMessage::Finished(msg) => msg.marshal(writer)?, + } + + Ok(()) + } +} + +// The handshake protocol is responsible for selecting a cipher spec and +// generating a master secret, which together comprise the primary +// cryptographic parameters associated with a secure session. The +// handshake protocol can also optionally authenticate parties who have +// certificates signed by a trusted certificate authority. +// https://tools.ietf.org/html/rfc5246#section-7.3 +#[derive(PartialEq, Debug, Clone)] +pub struct Handshake { + pub(crate) handshake_header: HandshakeHeader, + pub(crate) handshake_message: HandshakeMessage, +} + +impl Handshake { + pub fn new(handshake_message: HandshakeMessage) -> Self { + Handshake { + handshake_header: HandshakeHeader { + handshake_type: handshake_message.handshake_type(), + length: handshake_message.size() as u32, + message_sequence: 0, + fragment_offset: 0, + fragment_length: handshake_message.size() as u32, + }, + handshake_message, + } + } + + pub fn content_type(&self) -> ContentType { + ContentType::Handshake + } + + pub fn size(&self) -> usize { + self.handshake_header.size() + self.handshake_message.size() + } + + pub fn marshal(&self, writer: &mut W) -> Result<()> { + self.handshake_header.marshal(writer)?; + self.handshake_message.marshal(writer)?; + Ok(()) + } + + pub fn unmarshal(reader: &mut R) -> Result { + let handshake_header = HandshakeHeader::unmarshal(reader)?; + + let handshake_message = match handshake_header.handshake_type { + HandshakeType::ClientHello => { + HandshakeMessage::ClientHello(HandshakeMessageClientHello::unmarshal(reader)?) + } + HandshakeType::ServerHello => { + HandshakeMessage::ServerHello(HandshakeMessageServerHello::unmarshal(reader)?) + } + HandshakeType::HelloVerifyRequest => HandshakeMessage::HelloVerifyRequest( + HandshakeMessageHelloVerifyRequest::unmarshal(reader)?, + ), + HandshakeType::Certificate => { + HandshakeMessage::Certificate(HandshakeMessageCertificate::unmarshal(reader)?) + } + HandshakeType::ServerKeyExchange => HandshakeMessage::ServerKeyExchange( + HandshakeMessageServerKeyExchange::unmarshal(reader)?, + ), + HandshakeType::CertificateRequest => HandshakeMessage::CertificateRequest( + HandshakeMessageCertificateRequest::unmarshal(reader)?, + ), + HandshakeType::ServerHelloDone => HandshakeMessage::ServerHelloDone( + HandshakeMessageServerHelloDone::unmarshal(reader)?, + ), + HandshakeType::CertificateVerify => HandshakeMessage::CertificateVerify( + HandshakeMessageCertificateVerify::unmarshal(reader)?, + ), + HandshakeType::ClientKeyExchange => HandshakeMessage::ClientKeyExchange( + HandshakeMessageClientKeyExchange::unmarshal(reader)?, + ), + HandshakeType::Finished => { + HandshakeMessage::Finished(HandshakeMessageFinished::unmarshal(reader)?) + } + _ => return Err(Error::ErrNotImplemented), + }; + + Ok(Handshake { + handshake_header, + handshake_message, + }) + } +} diff --git a/vendor/dtls/src/handshaker.rs b/vendor/dtls/src/handshaker.rs new file mode 100644 index 00000000..b80ba4ac --- /dev/null +++ b/vendor/dtls/src/handshaker.rs @@ -0,0 +1,425 @@ +use std::collections::HashMap; +use std::fmt; +use std::sync::Arc; + +use log::*; + +use crate::cipher_suite::*; +use crate::config::*; +use crate::conn::*; +use crate::content::*; +use crate::crypto::*; +use crate::error::*; +use crate::extension::extension_use_srtp::*; +use crate::signature_hash_algorithm::*; + +use crate::certificate_verifier::{CertificateVerifier, RootCertStore}; +use rustls_pki_types::CertificateDer; + +//use std::io::BufWriter; + +// [RFC6347 Section-4.2.4] +// +-----------+ +// +---> | PREPARING | <--------------------+ +// | +-----------+ | +// | | | +// | | Buffer next flight | +// | | | +// | \|/ | +// | +-----------+ | +// | | SENDING |<------------------+ | Send +// | +-----------+ | | HelloRequest +// Receive | | | | +// next | | Send flight | | or +// flight | +--------+ | | +// | | | Set retransmit timer | | Receive +// | | \|/ | | HelloRequest +// | | +-----------+ | | Send +// +--)--| WAITING |-------------------+ | ClientHello +// | | +-----------+ Timer expires | | +// | | | | | +// | | +------------------------+ | +// Receive | | Send Read retransmit | +// last | | last | +// flight | | flight | +// | | | +// \|/\|/ | +// +-----------+ | +// | FINISHED | -------------------------------+ +// +-----------+ +// | /|\ +// | | +// +---+ +// Read retransmit +// Retransmit last flight + +#[derive(Copy, Clone, PartialEq)] +pub(crate) enum HandshakeState { + Errored, + Preparing, + Sending, + Waiting, + Finished, +} + +impl fmt::Display for HandshakeState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + HandshakeState::Errored => write!(f, "Errored"), + HandshakeState::Preparing => write!(f, "Preparing"), + HandshakeState::Sending => write!(f, "Sending"), + HandshakeState::Waiting => write!(f, "Waiting"), + HandshakeState::Finished => write!(f, "Finished"), + } + } +} + +pub(crate) type VerifyPeerCertificateFn = + Arc], &[CertificateDer<'static>]) -> Result<()>) + Send + Sync>; + +pub(crate) struct HandshakeConfig { + pub(crate) local_psk_callback: Option, + pub(crate) local_psk_identity_hint: Option>, + pub(crate) local_cipher_suites: Vec, // Available CipherSuites + pub(crate) local_signature_schemes: Vec, // Available signature schemes + pub(crate) extended_master_secret: ExtendedMasterSecretType, // Policy for the Extended Master Support extension + pub(crate) local_srtp_protection_profiles: Vec, // Available SRTPProtectionProfiles, if empty no SRTP support + pub(crate) server_name: String, + pub(crate) client_auth: ClientAuthType, // If we are a client should we request a client certificate + pub(crate) local_certificates: Vec, + pub(crate) name_to_certificate: HashMap, + pub(crate) insecure_skip_verify: bool, + pub(crate) insecure_verification: bool, + pub(crate) verify_peer_certificate: Option, + pub(crate) server_cert_verifier: Arc, + pub(crate) client_cert_verifier: Option>, + pub(crate) retransmit_interval: tokio::time::Duration, + pub(crate) initial_epoch: u16, + //log logging.LeveledLogger + //mu sync.Mutex +} + +pub fn gen_self_signed_root_cert() -> RootCertStore { + let mut certs = RootCertStore::empty(); + certs + .add( + rcgen::generate_simple_self_signed(vec![]) + .unwrap() + .cert + .der() + .to_owned(), + ) + .unwrap(); + certs +} + +impl Default for HandshakeConfig { + fn default() -> Self { + HandshakeConfig { + local_psk_callback: None, + local_psk_identity_hint: None, + local_cipher_suites: vec![], + local_signature_schemes: vec![], + extended_master_secret: ExtendedMasterSecretType::Disable, + local_srtp_protection_profiles: vec![], + server_name: String::new(), + client_auth: ClientAuthType::NoClientCert, + local_certificates: vec![], + name_to_certificate: HashMap::new(), + insecure_skip_verify: false, + insecure_verification: false, + verify_peer_certificate: None, + server_cert_verifier: Arc::new(CertificateVerifier::new(RootCertStore::empty())), + client_cert_verifier: None, + retransmit_interval: tokio::time::Duration::from_secs(0), + initial_epoch: 0, + } + } +} + +impl HandshakeConfig { + pub(crate) fn get_certificate(&self, server_name: &str) -> Result { + //TODO + /*if self.name_to_certificate.is_empty() { + let mut name_to_certificate = HashMap::new(); + for cert in &self.local_certificates { + if let Ok((_rem, x509_cert)) = x509_parser::parse_x509_der(&cert.certificate) { + if let Some(a) = x509_cert.tbs_certificate.subject.iter_common_name().next() { + let common_name = match a.attr_value.as_str() { + Ok(cn) => cn.to_lowercase(), + Err(err) => return Err(Error::new(err.to_string())), + }; + name_to_certificate.insert(common_name, cert.clone()); + } + if let Some((_, sans)) = x509_cert.tbs_certificate.subject_alternative_name() { + for gn in &sans.general_names { + match gn { + x509_parser::extensions::GeneralName::DNSName(san) => { + let san = san.to_lowercase(); + name_to_certificate.insert(san, cert.clone()); + } + _ => {} + } + } + } + } else { + continue; + } + } + self.name_to_certificate = name_to_certificate; + }*/ + + if self.local_certificates.is_empty() { + return Err(Error::ErrNoCertificates); + } + + if self.local_certificates.len() == 1 { + // There's only one choice, so no point doing any work. + return Ok(self.local_certificates[0].clone()); + } + + if server_name.is_empty() { + return Ok(self.local_certificates[0].clone()); + } + + let lower = server_name.to_lowercase(); + let name = lower.trim_end_matches('.'); + + if let Some(cert) = self.name_to_certificate.get(name) { + return Ok(cert.clone()); + } + + // try replacing labels in the name with wildcards until we get a + // match. + let mut labels: Vec<&str> = name.split_terminator('.').collect(); + for i in 0..labels.len() { + labels[i] = "*"; + let candidate = labels.join("."); + if let Some(cert) = self.name_to_certificate.get(&candidate) { + return Ok(cert.clone()); + } + } + + // If nothing matches, return the first certificate. + Ok(self.local_certificates[0].clone()) + } +} + +pub(crate) fn srv_cli_str(is_client: bool) -> String { + if is_client { + return "client".to_owned(); + } + "server".to_owned() +} + +impl DTLSConn { + pub(crate) async fn handshake(&mut self, mut state: HandshakeState) -> Result<()> { + loop { + trace!( + "[handshake:{}] {}: {}", + srv_cli_str(self.state.is_client), + self.current_flight, + state + ); + + if state == HandshakeState::Finished && !self.is_handshake_completed_successfully() { + self.set_handshake_completed_successfully(); + self.handshake_done_tx.take(); // drop it by take + return Ok(()); + } + + state = match state { + HandshakeState::Preparing => self.prepare().await?, + HandshakeState::Sending => self.send().await?, + HandshakeState::Waiting => self.wait().await?, + HandshakeState::Finished => self.finish().await?, + _ => return Err(Error::ErrInvalidFsmTransition), + }; + } + } + + async fn prepare(&mut self) -> Result { + self.flights = None; + + // Prepare flights + self.retransmit = self.current_flight.has_retransmit(); + + let result = self + .current_flight + .generate(&mut self.state, &self.cache, &self.cfg) + .await; + + match result { + Err((a, mut err)) => { + if let Some(a) = a { + let alert_err = self.notify(a.alert_level, a.alert_description).await; + + if let Err(alert_err) = alert_err { + if err.is_some() { + err = Some(alert_err); + } + } + } + if let Some(err) = err { + return Err(err); + } + } + Ok(pkts) => { + /*if !pkts.is_empty() { + let mut s = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(s.as_mut()); + pkts[0].record.content.marshal(&mut writer)?; + } + trace!( + "[handshake:{}] {}: {:?}", + srv_cli_str(self.state.is_client), + self.current_flight.to_string(), + s, + ); + }*/ + self.flights = Some(pkts) + } + }; + + let epoch = self.cfg.initial_epoch; + let mut next_epoch = epoch; + if let Some(pkts) = &mut self.flights { + for p in pkts { + p.record.record_layer_header.epoch += epoch; + if p.record.record_layer_header.epoch > next_epoch { + next_epoch = p.record.record_layer_header.epoch; + } + if let Content::Handshake(h) = &mut p.record.content { + h.handshake_header.message_sequence = self.state.handshake_send_sequence as u16; + self.state.handshake_send_sequence += 1; + } + } + } + if epoch != next_epoch { + trace!( + "[handshake:{}] -> changeCipherSpec (epoch: {})", + srv_cli_str(self.state.is_client), + next_epoch + ); + self.set_local_epoch(next_epoch); + } + + Ok(HandshakeState::Sending) + } + async fn send(&mut self) -> Result { + // Send flights + if let Some(pkts) = self.flights.clone() { + self.write_packets(pkts).await?; + } + + if self.current_flight.is_last_send_flight() { + Ok(HandshakeState::Finished) + } else { + Ok(HandshakeState::Waiting) + } + } + async fn wait(&mut self) -> Result { + let retransmit_timer = tokio::time::sleep(self.cfg.retransmit_interval); + tokio::pin!(retransmit_timer); + + loop { + tokio::select! { + done_senders = self.handshake_rx.recv() =>{ + if done_senders.is_none() { + trace!("[handshake:{}] {} handshake_tx is dropped", srv_cli_str(self.state.is_client), self.current_flight); + return Err(Error::ErrAlertFatalOrClose); + } else if let Some((rendezvous_tx, done_tx)) = done_senders { + rendezvous_tx.send(()).ok(); + //trace!("[handshake:{}] {} received handshake_rx", srv_cli_str(self.state.is_client), self.current_flight); + let result = self.current_flight.parse(&mut self.handle_queue_tx, &mut self.state, &self.cache, &self.cfg).await; + drop(done_tx); + match result { + Err((alert, mut err)) => { + trace!("[handshake:{}] {} result alert:{:?}, err:{:?}", + srv_cli_str(self.state.is_client), + self.current_flight, + alert, + err); + + if let Some(alert) = alert { + let alert_err = self.notify(alert.alert_level, alert.alert_description).await; + + if let Err(alert_err) = alert_err { + if err.is_some() { + err = Some(alert_err); + } + } + } + if let Some(err) = err { + return Err(err); + } + } + Ok(next_flight) => { + trace!("[handshake:{}] {} -> {}", srv_cli_str(self.state.is_client), self.current_flight, next_flight); + if next_flight.is_last_recv_flight() && self.current_flight.to_string() == next_flight.to_string() { + return Ok(HandshakeState::Finished); + } + self.current_flight = next_flight; + return Ok(HandshakeState::Preparing); + } + }; + } + } + + _ = retransmit_timer.as_mut() =>{ + trace!("[handshake:{}] {} retransmit_timer", srv_cli_str(self.state.is_client), self.current_flight); + + if !self.retransmit { + return Ok(HandshakeState::Waiting); + } + return Ok(HandshakeState::Sending); + } + + /*_ = self.done_rx.recv() => { + return Err(Error::new("done_rx recv".to_owned())); + }*/ + } + } + } + async fn finish(&mut self) -> Result { + let retransmit_timer = tokio::time::sleep(self.cfg.retransmit_interval); + + tokio::select! { + done = self.handshake_rx.recv() =>{ + if done.is_none() { + trace!("[handshake:{}] {} handshake_tx is dropped", srv_cli_str(self.state.is_client), self.current_flight); + return Err(Error::ErrAlertFatalOrClose); + } + let result = self.current_flight.parse(&mut self.handle_queue_tx, &mut self.state, &self.cache, &self.cfg).await; + drop(done); + match result { + Err((alert, mut err)) => { + if let Some(alert) = alert { + let alert_err = self.notify(alert.alert_level, alert.alert_description).await; + if let Err(alert_err) = alert_err { + if err.is_some() { + err = Some(alert_err); + } + } + } + if let Some(err) = err { + return Err(err); + } + } + Ok(_) => { + retransmit_timer.await; + // Retransmit last flight + return Ok(HandshakeState::Sending); + } + }; + } + + /*_ = self.done_rx.recv() => { + return Err(Error::new("done_rx recv".to_owned())); + }*/ + } + + Ok(HandshakeState::Finished) + } +} diff --git a/vendor/dtls/src/lib.rs b/vendor/dtls/src/lib.rs new file mode 100644 index 00000000..46c3cb49 --- /dev/null +++ b/vendor/dtls/src/lib.rs @@ -0,0 +1,58 @@ +#![warn(rust_2018_idioms)] +#![allow(dead_code)] + +pub mod alert; +pub mod application_data; +pub mod certificate_verifier; +pub mod change_cipher_spec; +pub mod cipher_suite; +pub mod client_certificate_type; +pub mod compression_methods; +pub mod config; +pub mod conn; +pub mod content; +pub mod crypto; +pub mod curve; +mod error; +pub mod extension; +pub mod flight; +pub mod fragment_buffer; +pub mod handshake; +pub mod handshaker; +pub mod listener; +pub mod prf; +pub mod record_layer; +pub mod signature_hash_algorithm; +pub mod state; + +use cipher_suite::*; +pub use error::Error; +use extension::extension_use_srtp::SrtpProtectionProfile; + +pub(crate) fn find_matching_srtp_profile( + a: &[SrtpProtectionProfile], + b: &[SrtpProtectionProfile], +) -> Result { + for a_profile in a { + for b_profile in b { + if a_profile == b_profile { + return Ok(*a_profile); + } + } + } + Err(()) +} + +pub(crate) fn find_matching_cipher_suite( + a: &[CipherSuiteId], + b: &[CipherSuiteId], +) -> Result { + for a_suite in a { + for b_suite in b { + if a_suite == b_suite { + return Ok(*a_suite); + } + } + } + Err(()) +} diff --git a/vendor/dtls/src/listener.rs b/vendor/dtls/src/listener.rs new file mode 100644 index 00000000..0dc6257c --- /dev/null +++ b/vendor/dtls/src/listener.rs @@ -0,0 +1,95 @@ +use std::future::Future; +use std::io::BufReader; +use std::net::SocketAddr; +use std::pin::Pin; +use std::sync::Arc; + +use async_trait::async_trait; +use tokio::net::ToSocketAddrs; +use util::conn::conn_udp_listener::*; +use util::conn::*; + +use crate::config::*; +use crate::conn::DTLSConn; +use crate::content::ContentType; +use crate::error::Result; +use crate::record_layer::record_layer_header::RecordLayerHeader; +use crate::record_layer::unpack_datagram; + +/// Listen creates a DTLS listener +pub async fn listen(laddr: A, config: Config) -> Result { + validate_config(false, &config)?; + + let mut lc = ListenConfig { + accept_filter: Some(Box::new( + |packet: &[u8]| -> Pin + Send + 'static>> { + let pkts = match unpack_datagram(packet) { + Ok(pkts) => { + if pkts.is_empty() { + return Box::pin(async { false }); + } + pkts + } + Err(_) => return Box::pin(async { false }), + }; + + let mut reader = BufReader::new(pkts[0].as_slice()); + match RecordLayerHeader::unmarshal(&mut reader) { + Ok(h) => { + let content_type = h.content_type; + Box::pin(async move { content_type == ContentType::Handshake }) + } + Err(_) => Box::pin(async { false }), + } + }, + )), + ..Default::default() + }; + + let parent = Arc::new(lc.listen(laddr).await?); + Ok(DTLSListener { parent, config }) +} + +/// DTLSListener represents a DTLS listener +pub struct DTLSListener { + parent: Arc, + config: Config, +} + +impl DTLSListener { + /// creates a DTLS listener which accepts connections from an inner Listener. + pub fn new(parent: Arc, config: Config) -> Result { + validate_config(false, &config)?; + + Ok(DTLSListener { parent, config }) + } +} + +type UtilResult = std::result::Result; + +#[async_trait] +impl Listener for DTLSListener { + /// Accept waits for and returns the next connection to the listener. + /// You have to either close or read on all connection that are created. + /// Connection handshake will timeout using ConnectContextMaker in the Config. + /// If you want to specify the timeout duration, set ConnectContextMaker. + async fn accept(&self) -> UtilResult<(Arc, SocketAddr)> { + let (conn, raddr) = self.parent.accept().await?; + let dtls_conn = DTLSConn::new(conn, self.config.clone(), false, None) + .await + .map_err(util::Error::from_std)?; + Ok((Arc::new(dtls_conn), raddr)) + } + + /// Close closes the listener. + /// Any blocked Accept operations will be unblocked and return errors. + /// Already Accepted connections are not closed. + async fn close(&self) -> UtilResult<()> { + self.parent.close().await + } + + /// Addr returns the listener's network address. + async fn addr(&self) -> UtilResult { + self.parent.addr().await + } +} diff --git a/vendor/dtls/src/prf/mod.rs b/vendor/dtls/src/prf/mod.rs new file mode 100644 index 00000000..e38cd454 --- /dev/null +++ b/vendor/dtls/src/prf/mod.rs @@ -0,0 +1,315 @@ +#[cfg(test)] +mod prf_test; + +use std::convert::TryInto; +use std::fmt; + +use hmac::{Hmac, Mac}; +use sha1::Sha1; +use sha2::{Digest, Sha256}; + +type HmacSha256 = Hmac; +type HmacSha1 = Hmac; + +use crate::cipher_suite::CipherSuiteHash; +use crate::content::ContentType; +use crate::curve::named_curve::*; +use crate::error::*; +use crate::record_layer::record_layer_header::ProtocolVersion; + +pub(crate) const PRF_MASTER_SECRET_LABEL: &str = "master secret"; +pub(crate) const PRF_EXTENDED_MASTER_SECRET_LABEL: &str = "extended master secret"; +pub(crate) const PRF_KEY_EXPANSION_LABEL: &str = "key expansion"; +pub(crate) const PRF_VERIFY_DATA_CLIENT_LABEL: &str = "client finished"; +pub(crate) const PRF_VERIFY_DATA_SERVER_LABEL: &str = "server finished"; + +#[derive(PartialEq, Debug, Clone)] +pub(crate) struct EncryptionKeys { + pub(crate) master_secret: Vec, + pub(crate) client_mac_key: Vec, + pub(crate) server_mac_key: Vec, + pub(crate) client_write_key: Vec, + pub(crate) server_write_key: Vec, + pub(crate) client_write_iv: Vec, + pub(crate) server_write_iv: Vec, +} + +impl fmt::Display for EncryptionKeys { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut out = "EncryptionKeys:\n".to_string(); + + out += format!("- master_secret: {:?}\n", self.master_secret).as_str(); + out += format!("- client_mackey: {:?}\n", self.client_mac_key).as_str(); + out += format!("- server_mackey: {:?}\n", self.server_mac_key).as_str(); + out += format!("- client_write_key: {:?}\n", self.client_write_key).as_str(); + out += format!("- server_write_key: {:?}\n", self.server_write_key).as_str(); + out += format!("- client_write_iv: {:?}\n", self.client_write_iv).as_str(); + out += format!("- server_write_iv: {:?}\n", self.server_write_iv).as_str(); + + write!(f, "{out}") + } +} + +// The premaster secret is formed as follows: if the PSK is N octets +// long, concatenate a uint16 with the value N, N zero octets, a second +// uint16 with the value N, and the PSK itself. +// +// https://tools.ietf.org/html/rfc4279#section-2 +pub(crate) fn prf_psk_pre_master_secret(psk: &[u8]) -> Vec { + let psk_len = psk.len(); + + let mut out = vec![0u8; 2 + psk_len + 2]; + + out.extend_from_slice(psk); + let be = (psk_len as u16).to_be_bytes(); + out[..2].copy_from_slice(&be); + out[2 + psk_len..2 + psk_len + 2].copy_from_slice(&be); + + out +} + +pub(crate) fn prf_pre_master_secret( + public_key: &[u8], + private_key: &NamedCurvePrivateKey, + curve: NamedCurve, +) -> Result> { + match curve { + NamedCurve::P256 => elliptic_curve_pre_master_secret(public_key, private_key, curve), + NamedCurve::P384 => elliptic_curve_pre_master_secret(public_key, private_key, curve), + NamedCurve::X25519 => elliptic_curve_pre_master_secret(public_key, private_key, curve), + _ => Err(Error::ErrInvalidNamedCurve), + } +} + +fn elliptic_curve_pre_master_secret( + public_key: &[u8], + private_key: &NamedCurvePrivateKey, + curve: NamedCurve, +) -> Result> { + match curve { + NamedCurve::P256 => { + let pub_key = p256::EncodedPoint::from_bytes(public_key)?; + let public = p256::PublicKey::from_sec1_bytes(pub_key.as_ref())?; + if let NamedCurvePrivateKey::EphemeralSecretP256(secret) = private_key { + return Ok(secret.diffie_hellman(&public).raw_secret_bytes().to_vec()); + } + } + NamedCurve::P384 => { + let pub_key = p384::EncodedPoint::from_bytes(public_key)?; + let public = p384::PublicKey::from_sec1_bytes(pub_key.as_ref())?; + if let NamedCurvePrivateKey::EphemeralSecretP384(secret) = private_key { + return Ok(secret.diffie_hellman(&public).raw_secret_bytes().to_vec()); + } + } + NamedCurve::X25519 => { + if public_key.len() != 32 { + return Err(Error::Other("Public key is not 32 len".into())); + } + let pub_key: [u8; 32] = public_key.try_into().unwrap(); + let public = x25519_dalek::PublicKey::from(pub_key); + if let NamedCurvePrivateKey::StaticSecretX25519(secret) = private_key { + return Ok(secret.diffie_hellman(&public).as_bytes().to_vec()); + } + } + _ => return Err(Error::ErrInvalidNamedCurve), + } + Err(Error::ErrNamedCurveAndPrivateKeyMismatch) +} + +// This PRF with the SHA-256 hash function is used for all cipher suites +// defined in this document and in TLS documents published prior to this +// document when TLS 1.2 is negotiated. New cipher suites MUST explicitly +// specify a PRF and, in general, SHOULD use the TLS PRF with SHA-256 or a +// stronger standard hash function. +// +// P_hash(secret, seed) = HMAC_hash(secret, A(1) + seed) + +// HMAC_hash(secret, A(2) + seed) + +// HMAC_hash(secret, A(3) + seed) + ... +// +// A() is defined as: +// +// A(0) = seed +// A(i) = HMAC_hash(secret, A(i-1)) +// +// P_hash can be iterated as many times as necessary to produce the +// required quantity of data. For example, if P_SHA256 is being used to +// create 80 bytes of data, it will have to be iterated three times +// (through A(3)), creating 96 bytes of output data; the last 16 bytes +// of the final iteration will then be discarded, leaving 80 bytes of +// output data. +// +// https://tools.ietf.org/html/rfc4346w +fn hmac_sha(h: CipherSuiteHash, key: &[u8], data: &[u8]) -> Result> { + let mut mac = match h { + CipherSuiteHash::Sha256 => { + HmacSha256::new_from_slice(key).map_err(|e| Error::Other(e.to_string()))? + } + }; + mac.update(data); + let result = mac.finalize(); + let code_bytes = result.into_bytes(); + Ok(code_bytes.to_vec()) +} + +pub(crate) fn prf_p_hash( + secret: &[u8], + seed: &[u8], + requested_length: usize, + h: CipherSuiteHash, +) -> Result> { + let mut last_round = seed.to_vec(); + let mut out = vec![]; + + let iterations = ((requested_length as f64) / (h.size() as f64)).ceil() as usize; + for _ in 0..iterations { + last_round = hmac_sha(h, secret, &last_round)?; + + let mut last_round_seed = last_round.clone(); + last_round_seed.extend_from_slice(seed); + let with_secret = hmac_sha(h, secret, &last_round_seed)?; + + out.extend_from_slice(&with_secret); + } + + Ok(out[..requested_length].to_vec()) +} + +pub(crate) fn prf_extended_master_secret( + pre_master_secret: &[u8], + session_hash: &[u8], + h: CipherSuiteHash, +) -> Result> { + let mut seed = PRF_EXTENDED_MASTER_SECRET_LABEL.as_bytes().to_vec(); + seed.extend_from_slice(session_hash); + prf_p_hash(pre_master_secret, &seed, 48, h) +} + +pub(crate) fn prf_master_secret( + pre_master_secret: &[u8], + client_random: &[u8], + server_random: &[u8], + h: CipherSuiteHash, +) -> Result> { + let mut seed = PRF_MASTER_SECRET_LABEL.as_bytes().to_vec(); + seed.extend_from_slice(client_random); + seed.extend_from_slice(server_random); + prf_p_hash(pre_master_secret, &seed, 48, h) +} + +pub(crate) fn prf_encryption_keys( + master_secret: &[u8], + client_random: &[u8], + server_random: &[u8], + prf_mac_len: usize, + prf_key_len: usize, + prf_iv_len: usize, + h: CipherSuiteHash, +) -> Result { + let mut seed = PRF_KEY_EXPANSION_LABEL.as_bytes().to_vec(); + seed.extend_from_slice(server_random); + seed.extend_from_slice(client_random); + + let material = prf_p_hash( + master_secret, + &seed, + (2 * prf_mac_len) + (2 * prf_key_len) + (2 * prf_iv_len), + h, + )?; + let mut key_material = &material[..]; + + let client_mac_key = key_material[..prf_mac_len].to_vec(); + key_material = &key_material[prf_mac_len..]; + + let server_mac_key = key_material[..prf_mac_len].to_vec(); + key_material = &key_material[prf_mac_len..]; + + let client_write_key = key_material[..prf_key_len].to_vec(); + key_material = &key_material[prf_key_len..]; + + let server_write_key = key_material[..prf_key_len].to_vec(); + key_material = &key_material[prf_key_len..]; + + let client_write_iv = key_material[..prf_iv_len].to_vec(); + key_material = &key_material[prf_iv_len..]; + + let server_write_iv = key_material[..prf_iv_len].to_vec(); + + Ok(EncryptionKeys { + master_secret: master_secret.to_vec(), + client_mac_key, + server_mac_key, + client_write_key, + server_write_key, + client_write_iv, + server_write_iv, + }) +} + +pub(crate) fn prf_verify_data( + master_secret: &[u8], + handshake_bodies: &[u8], + label: &str, + h: CipherSuiteHash, +) -> Result> { + let mut hasher = match h { + CipherSuiteHash::Sha256 => Sha256::new(), + }; + hasher.update(handshake_bodies); + let result = hasher.finalize(); + let mut seed = label.as_bytes().to_vec(); + seed.extend_from_slice(&result); + + prf_p_hash(master_secret, &seed, 12, h) +} + +pub(crate) fn prf_verify_data_client( + master_secret: &[u8], + handshake_bodies: &[u8], + h: CipherSuiteHash, +) -> Result> { + prf_verify_data( + master_secret, + handshake_bodies, + PRF_VERIFY_DATA_CLIENT_LABEL, + h, + ) +} + +pub(crate) fn prf_verify_data_server( + master_secret: &[u8], + handshake_bodies: &[u8], + h: CipherSuiteHash, +) -> Result> { + prf_verify_data( + master_secret, + handshake_bodies, + PRF_VERIFY_DATA_SERVER_LABEL, + h, + ) +} + +// compute the MAC using HMAC-SHA1 +pub(crate) fn prf_mac( + epoch: u16, + sequence_number: u64, + content_type: ContentType, + protocol_version: ProtocolVersion, + payload: &[u8], + key: &[u8], +) -> Result> { + let mut hmac = HmacSha1::new_from_slice(key).map_err(|e| Error::Other(e.to_string()))?; + + let mut msg = vec![0u8; 13]; + msg[..2].copy_from_slice(&epoch.to_be_bytes()); + msg[2..8].copy_from_slice(&sequence_number.to_be_bytes()[2..]); + msg[8] = content_type as u8; + msg[9] = protocol_version.major; + msg[10] = protocol_version.minor; + msg[11..].copy_from_slice(&(payload.len() as u16).to_be_bytes()); + + hmac.update(&msg); + hmac.update(payload); + let result = hmac.finalize(); + + Ok(result.into_bytes().to_vec()) +} diff --git a/vendor/dtls/src/prf/prf_test.rs b/vendor/dtls/src/prf/prf_test.rs new file mode 100644 index 00000000..33d5241f --- /dev/null +++ b/vendor/dtls/src/prf/prf_test.rs @@ -0,0 +1,261 @@ +use super::*; +use crate::cipher_suite::CipherSuiteHash; + +#[test] +fn test_pre_master_secret() -> Result<()> { + let private_key: [u8; 32] = [ + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, + 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, + 0x3e, 0x3f, + ]; + let private_key = + NamedCurvePrivateKey::StaticSecretX25519(x25519_dalek::StaticSecret::from(private_key)); + let public_key = [ + 0x9f, 0xd7, 0xad, 0x6d, 0xcf, 0xf4, 0x29, 0x8d, 0xd3, 0xf9, 0x6d, 0x5b, 0x1b, 0x2a, 0xf9, + 0x10, 0xa0, 0x53, 0x5b, 0x14, 0x88, 0xd7, 0xf8, 0xfa, 0xbb, 0x34, 0x9a, 0x98, 0x28, 0x80, + 0xb6, 0x15, + ]; + + let expected_pre_master_secret = vec![ + 0xdf, 0x4a, 0x29, 0x1b, 0xaa, 0x1e, 0xb7, 0xcf, 0xa6, 0x93, 0x4b, 0x29, 0xb4, 0x74, 0xba, + 0xad, 0x26, 0x97, 0xe2, 0x9f, 0x1f, 0x92, 0x0d, 0xcc, 0x77, 0xc8, 0xa0, 0xa0, 0x88, 0x44, + 0x76, 0x24, + ]; + + let pre_master_secret = prf_pre_master_secret(&public_key, &private_key, NamedCurve::X25519)?; + + assert_eq!( + expected_pre_master_secret, pre_master_secret, + "PremasterSecret exp: {expected_pre_master_secret:?} actual: {pre_master_secret:?}" + ); + + Ok(()) +} + +#[test] +fn test_master_secret() -> Result<()> { + let pre_master_secret = vec![ + 0xdf, 0x4a, 0x29, 0x1b, 0xaa, 0x1e, 0xb7, 0xcf, 0xa6, 0x93, 0x4b, 0x29, 0xb4, 0x74, 0xba, + 0xad, 0x26, 0x97, 0xe2, 0x9f, 0x1f, 0x92, 0x0d, 0xcc, 0x77, 0xc8, 0xa0, 0xa0, 0x88, 0x44, + 0x76, 0x24, + ]; + let client_random = vec![ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, + 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, + 0x1e, 0x1f, + ]; + let server_random = vec![ + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, + 0x7f, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, + 0x8e, 0x8f, + ]; + let expected_master_secret = vec![ + 0x91, 0x6a, 0xbf, 0x9d, 0xa5, 0x59, 0x73, 0xe1, 0x36, 0x14, 0xae, 0x0a, 0x3f, 0x5d, 0x3f, + 0x37, 0xb0, 0x23, 0xba, 0x12, 0x9a, 0xee, 0x02, 0xcc, 0x91, 0x34, 0x33, 0x81, 0x27, 0xcd, + 0x70, 0x49, 0x78, 0x1c, 0x8e, 0x19, 0xfc, 0x1e, 0xb2, 0xa7, 0x38, 0x7a, 0xc0, 0x6a, 0xe2, + 0x37, 0x34, 0x4c, + ]; + + let master_secret = prf_master_secret( + &pre_master_secret, + &client_random, + &server_random, + CipherSuiteHash::Sha256, + )?; + + assert_eq!( + expected_master_secret, master_secret, + "master_secret exp: {expected_master_secret:?} actual: {master_secret:?}" + ); + + Ok(()) +} + +#[test] +fn test_encryption_keys() -> Result<()> { + let client_random = vec![ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, + 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, + 0x1e, 0x1f, + ]; + let server_random = vec![ + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, + 0x7f, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, + 0x8e, 0x8f, + ]; + let master_secret = vec![ + 0x91, 0x6a, 0xbf, 0x9d, 0xa5, 0x59, 0x73, 0xe1, 0x36, 0x14, 0xae, 0x0a, 0x3f, 0x5d, 0x3f, + 0x37, 0xb0, 0x23, 0xba, 0x12, 0x9a, 0xee, 0x02, 0xcc, 0x91, 0x34, 0x33, 0x81, 0x27, 0xcd, + 0x70, 0x49, 0x78, 0x1c, 0x8e, 0x19, 0xfc, 0x1e, 0xb2, 0xa7, 0x38, 0x7a, 0xc0, 0x6a, 0xe2, + 0x37, 0x34, 0x4c, + ]; + + let expected_encryption_keys = EncryptionKeys { + master_secret: master_secret.clone(), + client_mac_key: vec![], + server_mac_key: vec![], + client_write_key: vec![ + 0x1b, 0x7d, 0x11, 0x7c, 0x7d, 0x5f, 0x69, 0x0b, 0xc2, 0x63, 0xca, 0xe8, 0xef, 0x60, + 0xaf, 0x0f, + ], + server_write_key: vec![ + 0x18, 0x78, 0xac, 0xc2, 0x2a, 0xd8, 0xbd, 0xd8, 0xc6, 0x01, 0xa6, 0x17, 0x12, 0x6f, + 0x63, 0x54, + ], + client_write_iv: vec![0x0e, 0xb2, 0x09, 0x06], + server_write_iv: vec![0xf7, 0x81, 0xfa, 0xd2], + }; + + let keys = prf_encryption_keys( + &master_secret, + &client_random, + &server_random, + 0, + 16, + 4, + CipherSuiteHash::Sha256, + )?; + + assert_eq!( + expected_encryption_keys, keys, + "master_secret exp: {expected_encryption_keys:?} actual: {keys:?}", + ); + + Ok(()) +} + +#[test] +fn test_verify_data() -> Result<()> { + let client_hello = vec![ + 0x01, 0x00, 0x00, 0xa1, 0x03, 0x03, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x00, 0x00, 0x20, 0xcc, 0xa8, 0xcc, 0xa9, + 0xc0, 0x2f, 0xc0, 0x30, 0xc0, 0x2b, 0xc0, 0x2c, 0xc0, 0x13, 0xc0, 0x09, 0xc0, 0x14, 0xc0, + 0x0a, 0x00, 0x9c, 0x00, 0x9d, 0x00, 0x2f, 0x00, 0x35, 0xc0, 0x12, 0x00, 0x0a, 0x01, 0x00, + 0x00, 0x58, 0x00, 0x00, 0x00, 0x18, 0x00, 0x16, 0x00, 0x00, 0x13, 0x65, 0x78, 0x61, 0x6d, + 0x70, 0x6c, 0x65, 0x2e, 0x75, 0x6c, 0x66, 0x68, 0x65, 0x69, 0x6d, 0x2e, 0x6e, 0x65, 0x74, + 0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, + 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x00, 0x19, 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, 0x00, + 0x0d, 0x00, 0x12, 0x00, 0x10, 0x04, 0x01, 0x04, 0x03, 0x05, 0x01, 0x05, 0x03, 0x06, 0x01, + 0x06, 0x03, 0x02, 0x01, 0x02, 0x03, 0xff, 0x01, 0x00, 0x01, 0x00, 0x00, 0x12, 0x00, 0x00, + ]; + let server_hello = vec![ + 0x02, 0x00, 0x00, 0x2d, 0x03, 0x03, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, + 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, + 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, 0x00, 0xc0, 0x13, 0x00, 0x00, 0x05, 0xff, + 0x01, 0x00, 0x01, 0x00, + ]; + let server_certificate = vec![ + 0x0b, 0x00, 0x03, 0x2b, 0x00, 0x03, 0x28, 0x00, 0x03, 0x25, 0x30, 0x82, 0x03, 0x21, 0x30, + 0x82, 0x02, 0x09, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x08, 0x15, 0x5a, 0x92, 0xad, 0xc2, + 0x04, 0x8f, 0x90, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, + 0x0b, 0x05, 0x00, 0x30, 0x22, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, + 0x02, 0x55, 0x53, 0x31, 0x13, 0x30, 0x11, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x0a, 0x45, + 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x20, 0x43, 0x41, 0x30, 0x1e, 0x17, 0x0d, 0x31, 0x38, + 0x31, 0x30, 0x30, 0x35, 0x30, 0x31, 0x33, 0x38, 0x31, 0x37, 0x5a, 0x17, 0x0d, 0x31, 0x39, + 0x31, 0x30, 0x30, 0x35, 0x30, 0x31, 0x33, 0x38, 0x31, 0x37, 0x5a, 0x30, 0x2b, 0x31, 0x0b, + 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, 0x1c, 0x30, 0x1a, + 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x13, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, + 0x75, 0x6c, 0x66, 0x68, 0x65, 0x69, 0x6d, 0x2e, 0x6e, 0x65, 0x74, 0x30, 0x82, 0x01, 0x22, + 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, + 0x03, 0x82, 0x01, 0x0f, 0x00, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, 0x00, 0xc4, + 0x80, 0x36, 0x06, 0xba, 0xe7, 0x47, 0x6b, 0x08, 0x94, 0x04, 0xec, 0xa7, 0xb6, 0x91, 0x04, + 0x3f, 0xf7, 0x92, 0xbc, 0x19, 0xee, 0xfb, 0x7d, 0x74, 0xd7, 0xa8, 0x0d, 0x00, 0x1e, 0x7b, + 0x4b, 0x3a, 0x4a, 0xe6, 0x0f, 0xe8, 0xc0, 0x71, 0xfc, 0x73, 0xe7, 0x02, 0x4c, 0x0d, 0xbc, + 0xf4, 0xbd, 0xd1, 0x1d, 0x39, 0x6b, 0xba, 0x70, 0x46, 0x4a, 0x13, 0xe9, 0x4a, 0xf8, 0x3d, + 0xf3, 0xe1, 0x09, 0x59, 0x54, 0x7b, 0xc9, 0x55, 0xfb, 0x41, 0x2d, 0xa3, 0x76, 0x52, 0x11, + 0xe1, 0xf3, 0xdc, 0x77, 0x6c, 0xaa, 0x53, 0x37, 0x6e, 0xca, 0x3a, 0xec, 0xbe, 0xc3, 0xaa, + 0xb7, 0x3b, 0x31, 0xd5, 0x6c, 0xb6, 0x52, 0x9c, 0x80, 0x98, 0xbc, 0xc9, 0xe0, 0x28, 0x18, + 0xe2, 0x0b, 0xf7, 0xf8, 0xa0, 0x3a, 0xfd, 0x17, 0x04, 0x50, 0x9e, 0xce, 0x79, 0xbd, 0x9f, + 0x39, 0xf1, 0xea, 0x69, 0xec, 0x47, 0x97, 0x2e, 0x83, 0x0f, 0xb5, 0xca, 0x95, 0xde, 0x95, + 0xa1, 0xe6, 0x04, 0x22, 0xd5, 0xee, 0xbe, 0x52, 0x79, 0x54, 0xa1, 0xe7, 0xbf, 0x8a, 0x86, + 0xf6, 0x46, 0x6d, 0x0d, 0x9f, 0x16, 0x95, 0x1a, 0x4c, 0xf7, 0xa0, 0x46, 0x92, 0x59, 0x5c, + 0x13, 0x52, 0xf2, 0x54, 0x9e, 0x5a, 0xfb, 0x4e, 0xbf, 0xd7, 0x7a, 0x37, 0x95, 0x01, 0x44, + 0xe4, 0xc0, 0x26, 0x87, 0x4c, 0x65, 0x3e, 0x40, 0x7d, 0x7d, 0x23, 0x07, 0x44, 0x01, 0xf4, + 0x84, 0xff, 0xd0, 0x8f, 0x7a, 0x1f, 0xa0, 0x52, 0x10, 0xd1, 0xf4, 0xf0, 0xd5, 0xce, 0x79, + 0x70, 0x29, 0x32, 0xe2, 0xca, 0xbe, 0x70, 0x1f, 0xdf, 0xad, 0x6b, 0x4b, 0xb7, 0x11, 0x01, + 0xf4, 0x4b, 0xad, 0x66, 0x6a, 0x11, 0x13, 0x0f, 0xe2, 0xee, 0x82, 0x9e, 0x4d, 0x02, 0x9d, + 0xc9, 0x1c, 0xdd, 0x67, 0x16, 0xdb, 0xb9, 0x06, 0x18, 0x86, 0xed, 0xc1, 0xba, 0x94, 0x21, + 0x02, 0x03, 0x01, 0x00, 0x01, 0xa3, 0x52, 0x30, 0x50, 0x30, 0x0e, 0x06, 0x03, 0x55, 0x1d, + 0x0f, 0x01, 0x01, 0xff, 0x04, 0x04, 0x03, 0x02, 0x05, 0xa0, 0x30, 0x1d, 0x06, 0x03, 0x55, + 0x1d, 0x25, 0x04, 0x16, 0x30, 0x14, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, + 0x02, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x01, 0x30, 0x1f, 0x06, 0x03, + 0x55, 0x1d, 0x23, 0x04, 0x18, 0x30, 0x16, 0x80, 0x14, 0x89, 0x4f, 0xde, 0x5b, 0xcc, 0x69, + 0xe2, 0x52, 0xcf, 0x3e, 0xa3, 0x00, 0xdf, 0xb1, 0x97, 0xb8, 0x1d, 0xe1, 0xc1, 0x46, 0x30, + 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x03, + 0x82, 0x01, 0x01, 0x00, 0x59, 0x16, 0x45, 0xa6, 0x9a, 0x2e, 0x37, 0x79, 0xe4, 0xf6, 0xdd, + 0x27, 0x1a, 0xba, 0x1c, 0x0b, 0xfd, 0x6c, 0xd7, 0x55, 0x99, 0xb5, 0xe7, 0xc3, 0x6e, 0x53, + 0x3e, 0xff, 0x36, 0x59, 0x08, 0x43, 0x24, 0xc9, 0xe7, 0xa5, 0x04, 0x07, 0x9d, 0x39, 0xe0, + 0xd4, 0x29, 0x87, 0xff, 0xe3, 0xeb, 0xdd, 0x09, 0xc1, 0xcf, 0x1d, 0x91, 0x44, 0x55, 0x87, + 0x0b, 0x57, 0x1d, 0xd1, 0x9b, 0xdf, 0x1d, 0x24, 0xf8, 0xbb, 0x9a, 0x11, 0xfe, 0x80, 0xfd, + 0x59, 0x2b, 0xa0, 0x39, 0x8c, 0xde, 0x11, 0xe2, 0x65, 0x1e, 0x61, 0x8c, 0xe5, 0x98, 0xfa, + 0x96, 0xe5, 0x37, 0x2e, 0xef, 0x3d, 0x24, 0x8a, 0xfd, 0xe1, 0x74, 0x63, 0xeb, 0xbf, 0xab, + 0xb8, 0xe4, 0xd1, 0xab, 0x50, 0x2a, 0x54, 0xec, 0x00, 0x64, 0xe9, 0x2f, 0x78, 0x19, 0x66, + 0x0d, 0x3f, 0x27, 0xcf, 0x20, 0x9e, 0x66, 0x7f, 0xce, 0x5a, 0xe2, 0xe4, 0xac, 0x99, 0xc7, + 0xc9, 0x38, 0x18, 0xf8, 0xb2, 0x51, 0x07, 0x22, 0xdf, 0xed, 0x97, 0xf3, 0x2e, 0x3e, 0x93, + 0x49, 0xd4, 0xc6, 0x6c, 0x9e, 0xa6, 0x39, 0x6d, 0x74, 0x44, 0x62, 0xa0, 0x6b, 0x42, 0xc6, + 0xd5, 0xba, 0x68, 0x8e, 0xac, 0x3a, 0x01, 0x7b, 0xdd, 0xfc, 0x8e, 0x2c, 0xfc, 0xad, 0x27, + 0xcb, 0x69, 0xd3, 0xcc, 0xdc, 0xa2, 0x80, 0x41, 0x44, 0x65, 0xd3, 0xae, 0x34, 0x8c, 0xe0, + 0xf3, 0x4a, 0xb2, 0xfb, 0x9c, 0x61, 0x83, 0x71, 0x31, 0x2b, 0x19, 0x10, 0x41, 0x64, 0x1c, + 0x23, 0x7f, 0x11, 0xa5, 0xd6, 0x5c, 0x84, 0x4f, 0x04, 0x04, 0x84, 0x99, 0x38, 0x71, 0x2b, + 0x95, 0x9e, 0xd6, 0x85, 0xbc, 0x5c, 0x5d, 0xd6, 0x45, 0xed, 0x19, 0x90, 0x94, 0x73, 0x40, + 0x29, 0x26, 0xdc, 0xb4, 0x0e, 0x34, 0x69, 0xa1, 0x59, 0x41, 0xe8, 0xe2, 0xcc, 0xa8, 0x4b, + 0xb6, 0x08, 0x46, 0x36, 0xa0, + ]; + let server_key_exchange = vec![ + 0x0c, 0x00, 0x01, 0x28, 0x03, 0x00, 0x1d, 0x20, 0x9f, 0xd7, 0xad, 0x6d, 0xcf, 0xf4, 0x29, + 0x8d, 0xd3, 0xf9, 0x6d, 0x5b, 0x1b, 0x2a, 0xf9, 0x10, 0xa0, 0x53, 0x5b, 0x14, 0x88, 0xd7, + 0xf8, 0xfa, 0xbb, 0x34, 0x9a, 0x98, 0x28, 0x80, 0xb6, 0x15, 0x04, 0x01, 0x01, 0x00, 0x04, + 0x02, 0xb6, 0x61, 0xf7, 0xc1, 0x91, 0xee, 0x59, 0xbe, 0x45, 0x37, 0x66, 0x39, 0xbd, 0xc3, + 0xd4, 0xbb, 0x81, 0xe1, 0x15, 0xca, 0x73, 0xc8, 0x34, 0x8b, 0x52, 0x5b, 0x0d, 0x23, 0x38, + 0xaa, 0x14, 0x46, 0x67, 0xed, 0x94, 0x31, 0x02, 0x14, 0x12, 0xcd, 0x9b, 0x84, 0x4c, 0xba, + 0x29, 0x93, 0x4a, 0xaa, 0xcc, 0xe8, 0x73, 0x41, 0x4e, 0xc1, 0x1c, 0xb0, 0x2e, 0x27, 0x2d, + 0x0a, 0xd8, 0x1f, 0x76, 0x7d, 0x33, 0x07, 0x67, 0x21, 0xf1, 0x3b, 0xf3, 0x60, 0x20, 0xcf, + 0x0b, 0x1f, 0xd0, 0xec, 0xb0, 0x78, 0xde, 0x11, 0x28, 0xbe, 0xba, 0x09, 0x49, 0xeb, 0xec, + 0xe1, 0xa1, 0xf9, 0x6e, 0x20, 0x9d, 0xc3, 0x6e, 0x4f, 0xff, 0xd3, 0x6b, 0x67, 0x3a, 0x7d, + 0xdc, 0x15, 0x97, 0xad, 0x44, 0x08, 0xe4, 0x85, 0xc4, 0xad, 0xb2, 0xc8, 0x73, 0x84, 0x12, + 0x49, 0x37, 0x25, 0x23, 0x80, 0x9e, 0x43, 0x12, 0xd0, 0xc7, 0xb3, 0x52, 0x2e, 0xf9, 0x83, + 0xca, 0xc1, 0xe0, 0x39, 0x35, 0xff, 0x13, 0xa8, 0xe9, 0x6b, 0xa6, 0x81, 0xa6, 0x2e, 0x40, + 0xd3, 0xe7, 0x0a, 0x7f, 0xf3, 0x58, 0x66, 0xd3, 0xd9, 0x99, 0x3f, 0x9e, 0x26, 0xa6, 0x34, + 0xc8, 0x1b, 0x4e, 0x71, 0x38, 0x0f, 0xcd, 0xd6, 0xf4, 0xe8, 0x35, 0xf7, 0x5a, 0x64, 0x09, + 0xc7, 0xdc, 0x2c, 0x07, 0x41, 0x0e, 0x6f, 0x87, 0x85, 0x8c, 0x7b, 0x94, 0xc0, 0x1c, 0x2e, + 0x32, 0xf2, 0x91, 0x76, 0x9e, 0xac, 0xca, 0x71, 0x64, 0x3b, 0x8b, 0x98, 0xa9, 0x63, 0xdf, + 0x0a, 0x32, 0x9b, 0xea, 0x4e, 0xd6, 0x39, 0x7e, 0x8c, 0xd0, 0x1a, 0x11, 0x0a, 0xb3, 0x61, + 0xac, 0x5b, 0xad, 0x1c, 0xcd, 0x84, 0x0a, 0x6c, 0x8a, 0x6e, 0xaa, 0x00, 0x1a, 0x9d, 0x7d, + 0x87, 0xdc, 0x33, 0x18, 0x64, 0x35, 0x71, 0x22, 0x6c, 0x4d, 0xd2, 0xc2, 0xac, 0x41, 0xfb, + ]; + let server_hello_done = vec![0x0e, 0x00, 0x00, 0x00]; + let client_key_exchange = vec![ + 0x10, 0x00, 0x00, 0x21, 0x20, 0x35, 0x80, 0x72, 0xd6, 0x36, 0x58, 0x80, 0xd1, 0xae, 0xea, + 0x32, 0x9a, 0xdf, 0x91, 0x21, 0x38, 0x38, 0x51, 0xed, 0x21, 0xa2, 0x8e, 0x3b, 0x75, 0xe9, + 0x65, 0xd0, 0xd2, 0xcd, 0x16, 0x62, 0x54, + ]; + + let mut final_msg = vec![]; + final_msg.extend_from_slice(&client_hello); + final_msg.extend_from_slice(&server_hello); + final_msg.extend_from_slice(&server_certificate); + final_msg.extend_from_slice(&server_key_exchange); + final_msg.extend_from_slice(&server_hello_done); + final_msg.extend_from_slice(&client_key_exchange); + + let master_secret = vec![ + 0x91, 0x6a, 0xbf, 0x9d, 0xa5, 0x59, 0x73, 0xe1, 0x36, 0x14, 0xae, 0x0a, 0x3f, 0x5d, 0x3f, + 0x37, 0xb0, 0x23, 0xba, 0x12, 0x9a, 0xee, 0x02, 0xcc, 0x91, 0x34, 0x33, 0x81, 0x27, 0xcd, + 0x70, 0x49, 0x78, 0x1c, 0x8e, 0x19, 0xfc, 0x1e, 0xb2, 0xa7, 0x38, 0x7a, 0xc0, 0x6a, 0xe2, + 0x37, 0x34, 0x4c, + ]; + + let expected_verify_data = vec![ + 0xcf, 0x91, 0x96, 0x26, 0xf1, 0x36, 0x0c, 0x53, 0x6a, 0xaa, 0xd7, 0x3a, + ]; + + let verify_data = prf_verify_data_client(&master_secret, &final_msg, CipherSuiteHash::Sha256)?; + + assert_eq!( + expected_verify_data, verify_data, + "verify_data exp: {expected_verify_data:?} actual: {verify_data:?}" + ); + + Ok(()) +} diff --git a/vendor/dtls/src/record_layer/mod.rs b/vendor/dtls/src/record_layer/mod.rs new file mode 100644 index 00000000..b7cead16 --- /dev/null +++ b/vendor/dtls/src/record_layer/mod.rs @@ -0,0 +1,111 @@ +pub mod record_layer_header; + +#[cfg(test)] +mod record_layer_test; + +use std::io::{Read, Write}; + +use record_layer_header::*; + +use super::content::*; +use super::error::*; +use crate::alert::Alert; +use crate::application_data::ApplicationData; +use crate::change_cipher_spec::ChangeCipherSpec; +use crate::handshake::Handshake; + +/* + The TLS Record Layer which handles all data transport. + The record layer is assumed to sit directly on top of some + reliable transport such as TCP. The record layer can carry four types of content: + + 1. Handshake messages—used for algorithm negotiation and key establishment. + 2. ChangeCipherSpec messages—really part of the handshake but technically a separate kind of message. + 3. Alert messages—used to signal that errors have occurred + 4. Application layer data + + The DTLS record layer is extremely similar to that of TLS 1.1. The + only change is the inclusion of an explicit sequence number in the + record. This sequence number allows the recipient to correctly + verify the TLS MAC. +*/ +/// ## Specifications +/// +/// * [RFC 4347 §4.1] +/// +/// [RFC 4347 §4.1]: https://tools.ietf.org/html/rfc4347#section-4.1 +#[derive(Debug, Clone, PartialEq)] +pub struct RecordLayer { + pub record_layer_header: RecordLayerHeader, + pub content: Content, +} + +impl RecordLayer { + pub fn new(protocol_version: ProtocolVersion, epoch: u16, content: Content) -> Self { + RecordLayer { + record_layer_header: RecordLayerHeader { + content_type: content.content_type(), + protocol_version, + epoch, + sequence_number: 0, + content_len: content.size() as u16, + }, + content, + } + } + + pub fn marshal(&self, writer: &mut W) -> Result<()> { + self.record_layer_header.marshal(writer)?; + self.content.marshal(writer)?; + Ok(()) + } + + pub fn unmarshal(reader: &mut R) -> Result { + let record_layer_header = RecordLayerHeader::unmarshal(reader)?; + let content = match record_layer_header.content_type { + ContentType::Alert => Content::Alert(Alert::unmarshal(reader)?), + ContentType::ApplicationData => { + Content::ApplicationData(ApplicationData::unmarshal(reader)?) + } + ContentType::ChangeCipherSpec => { + Content::ChangeCipherSpec(ChangeCipherSpec::unmarshal(reader)?) + } + ContentType::Handshake => Content::Handshake(Handshake::unmarshal(reader)?), + _ => return Err(Error::Other("Invalid Content Type".to_owned())), + }; + + Ok(RecordLayer { + record_layer_header, + content, + }) + } +} + +// Note that as with TLS, multiple handshake messages may be placed in +// the same DTLS record, provided that there is room and that they are +// part of the same flight. Thus, there are two acceptable ways to pack +// two DTLS messages into the same datagram: in the same record or in +// separate records. +// https://tools.ietf.org/html/rfc6347#section-4.2.3 +pub(crate) fn unpack_datagram(buf: &[u8]) -> Result>> { + let mut out = vec![]; + + let mut offset = 0; + while buf.len() != offset { + if buf.len() - offset <= RECORD_LAYER_HEADER_SIZE { + return Err(Error::ErrInvalidPacketLength); + } + + let pkt_len = RECORD_LAYER_HEADER_SIZE + + (((buf[offset + RECORD_LAYER_HEADER_SIZE - 2] as usize) << 8) + | buf[offset + RECORD_LAYER_HEADER_SIZE - 1] as usize); + if offset + pkt_len > buf.len() { + return Err(Error::ErrInvalidPacketLength); + } + + out.push(buf[offset..offset + pkt_len].to_vec()); + offset += pkt_len + } + + Ok(out) +} diff --git a/vendor/dtls/src/record_layer/record_layer_header.rs b/vendor/dtls/src/record_layer/record_layer_header.rs new file mode 100644 index 00000000..e10d9ea4 --- /dev/null +++ b/vendor/dtls/src/record_layer/record_layer_header.rs @@ -0,0 +1,94 @@ +use std::io::{Read, Write}; + +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; + +use crate::content::*; +use crate::error::*; + +pub const RECORD_LAYER_HEADER_SIZE: usize = 13; +pub const MAX_SEQUENCE_NUMBER: u64 = 0x0000FFFFFFFFFFFF; + +pub const DTLS1_2MAJOR: u8 = 0xfe; +pub const DTLS1_2MINOR: u8 = 0xfd; + +pub const DTLS1_0MAJOR: u8 = 0xfe; +pub const DTLS1_0MINOR: u8 = 0xff; + +// VERSION_DTLS12 is the DTLS version in the same style as +// VersionTLSXX from crypto/tls +pub const VERSION_DTLS12: u16 = 0xfefd; + +pub const PROTOCOL_VERSION1_0: ProtocolVersion = ProtocolVersion { + major: DTLS1_0MAJOR, + minor: DTLS1_0MINOR, +}; +pub const PROTOCOL_VERSION1_2: ProtocolVersion = ProtocolVersion { + major: DTLS1_2MAJOR, + minor: DTLS1_2MINOR, +}; + +/// ## Specifications +/// +/// * [RFC 4346 §6.2.1] +/// +/// [RFC 4346 §6.2.1]: https://tools.ietf.org/html/rfc4346#section-6.2.1 +#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] +pub struct ProtocolVersion { + pub major: u8, + pub minor: u8, +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] +pub struct RecordLayerHeader { + pub content_type: ContentType, + pub protocol_version: ProtocolVersion, + pub epoch: u16, + pub sequence_number: u64, // uint48 in spec + pub content_len: u16, +} + +impl RecordLayerHeader { + pub fn marshal(&self, writer: &mut W) -> Result<()> { + if self.sequence_number > MAX_SEQUENCE_NUMBER { + return Err(Error::ErrSequenceNumberOverflow); + } + + writer.write_u8(self.content_type as u8)?; + writer.write_u8(self.protocol_version.major)?; + writer.write_u8(self.protocol_version.minor)?; + writer.write_u16::(self.epoch)?; + + let be: [u8; 8] = self.sequence_number.to_be_bytes(); + writer.write_all(&be[2..])?; // uint48 in spec + + writer.write_u16::(self.content_len)?; + + Ok(writer.flush()?) + } + + pub fn unmarshal(reader: &mut R) -> Result { + let content_type = reader.read_u8()?.into(); + let major = reader.read_u8()?; + let minor = reader.read_u8()?; + let epoch = reader.read_u16::()?; + + // SequenceNumber is stored as uint48, make into uint64 + let mut be: [u8; 8] = [0u8; 8]; + reader.read_exact(&mut be[2..])?; + let sequence_number = u64::from_be_bytes(be); + + let protocol_version = ProtocolVersion { major, minor }; + if protocol_version != PROTOCOL_VERSION1_0 && protocol_version != PROTOCOL_VERSION1_2 { + return Err(Error::ErrUnsupportedProtocolVersion); + } + let content_len = reader.read_u16::()?; + + Ok(RecordLayerHeader { + content_type, + protocol_version, + epoch, + sequence_number, + content_len, + }) + } +} diff --git a/vendor/dtls/src/record_layer/record_layer_test.rs b/vendor/dtls/src/record_layer/record_layer_test.rs new file mode 100644 index 00000000..3e644e50 --- /dev/null +++ b/vendor/dtls/src/record_layer/record_layer_test.rs @@ -0,0 +1,118 @@ +use std::io::{BufReader, BufWriter}; + +use super::record_layer_header::*; +use super::*; +use crate::change_cipher_spec::ChangeCipherSpec; + +#[test] +fn test_udp_decode() -> Result<()> { + let tests = vec![ + ( + "Change Cipher Spec, single packet", + vec![ + 0x14, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0x01, 0x01, + ], + vec![vec![ + 0x14, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0x01, 0x01, + ]], + None, + ), + ( + "Change Cipher Spec, multi packet", + vec![ + 0x14, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0x01, 0x01, + 0x14, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x00, 0x01, 0x01, + ], + vec![ + vec![ + 0x14, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0x01, + 0x01, + ], + vec![ + 0x14, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x00, 0x01, + 0x01, + ], + ], + None, + ), + ( + "Invalid packet length", + vec![0x14, 0xfe], + vec![], + Some(Error::ErrInvalidPacketLength), + ), + ( + "Packet declared invalid length", + vec![ + 0x14, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0xFF, 0x01, + ], + vec![], + Some(Error::ErrInvalidPacketLength), + ), + ]; + + for (name, data, wanted, wanted_err) in tests { + let dtls_pkts = unpack_datagram(&data); + if let Some(err) = wanted_err { + if let Err(dtls) = dtls_pkts { + assert_eq!(err.to_string(), dtls.to_string()); + } else { + panic!("something wrong for {name} when wanted_err is Some"); + } + } else if let Ok(pkts) = dtls_pkts { + assert_eq!( + wanted, pkts, + "{name} UDP decode: got {pkts:?}, want {wanted:?}", + ); + } else { + panic!("something wrong for {name} when wanted_err is None"); + } + } + + Ok(()) +} + +#[test] +fn test_record_layer_round_trip() -> Result<()> { + let tests = vec![( + "Change Cipher Spec, single packet", + vec![ + 0x14, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0x01, 0x01, + ], + RecordLayer { + record_layer_header: RecordLayerHeader { + content_type: ContentType::ChangeCipherSpec, + protocol_version: ProtocolVersion { + major: 0xfe, + minor: 0xff, + }, + epoch: 0, + sequence_number: 18, + content_len: 1, + }, + content: Content::ChangeCipherSpec(ChangeCipherSpec {}), + }, + )]; + + for (name, data, want) in tests { + let mut reader = BufReader::new(data.as_slice()); + let r = RecordLayer::unmarshal(&mut reader)?; + + assert_eq!( + want, r, + "{name} recordLayer.unmarshal: got {r:?}, want {want:?}" + ); + + let mut data2 = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(data2.as_mut()); + r.marshal(&mut writer)?; + } + assert_eq!( + data, data2, + "{name} recordLayer.marshal: got {data2:?}, want {data:?}" + ); + } + + Ok(()) +} diff --git a/vendor/dtls/src/signature_hash_algorithm/mod.rs b/vendor/dtls/src/signature_hash_algorithm/mod.rs new file mode 100644 index 00000000..41e5dab3 --- /dev/null +++ b/vendor/dtls/src/signature_hash_algorithm/mod.rs @@ -0,0 +1,215 @@ +#[cfg(test)] +mod signature_hash_algorithm_test; + +use std::fmt; + +use crate::crypto::*; +use crate::error::*; + +// HashAlgorithm is used to indicate the hash algorithm used +// https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-18 +// Supported hash hash algorithms +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum HashAlgorithm { + Md2 = 0, // Blacklisted + Md5 = 1, // Blacklisted + Sha1 = 2, // Blacklisted + Sha224 = 3, + Sha256 = 4, + Sha384 = 5, + Sha512 = 6, + Ed25519 = 8, + Unsupported, +} + +impl From for HashAlgorithm { + fn from(val: u8) -> Self { + match val { + 0 => HashAlgorithm::Md2, + 1 => HashAlgorithm::Md5, + 2 => HashAlgorithm::Sha1, + 3 => HashAlgorithm::Sha224, + 4 => HashAlgorithm::Sha256, + 5 => HashAlgorithm::Sha384, + 6 => HashAlgorithm::Sha512, + 8 => HashAlgorithm::Ed25519, + _ => HashAlgorithm::Unsupported, + } + } +} + +impl fmt::Display for HashAlgorithm { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + HashAlgorithm::Md2 => write!(f, "md2"), + HashAlgorithm::Md5 => write!(f, "md5"), // [RFC3279] + HashAlgorithm::Sha1 => write!(f, "sha-1"), // [RFC3279] + HashAlgorithm::Sha224 => write!(f, "sha-224"), // [RFC4055] + HashAlgorithm::Sha256 => write!(f, "sha-256"), // [RFC4055] + HashAlgorithm::Sha384 => write!(f, "sha-384"), // [RFC4055] + HashAlgorithm::Sha512 => write!(f, "sha-512"), // [RFC4055] + HashAlgorithm::Ed25519 => write!(f, "null"), // [RFC4055] + _ => write!(f, "unknown or unsupported hash algorithm"), + } + } +} + +impl HashAlgorithm { + pub(crate) fn insecure(&self) -> bool { + matches!( + *self, + HashAlgorithm::Md2 | HashAlgorithm::Md5 | HashAlgorithm::Sha1 + ) + } + + pub(crate) fn invalid(&self) -> bool { + matches!(*self, HashAlgorithm::Md2) + } +} + +// https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-16 +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum SignatureAlgorithm { + Rsa = 1, + Ecdsa = 3, + Ed25519 = 7, + Unsupported, +} + +impl From for SignatureAlgorithm { + fn from(val: u8) -> Self { + match val { + 1 => SignatureAlgorithm::Rsa, + 3 => SignatureAlgorithm::Ecdsa, + 7 => SignatureAlgorithm::Ed25519, + _ => SignatureAlgorithm::Unsupported, + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct SignatureHashAlgorithm { + pub hash: HashAlgorithm, + pub signature: SignatureAlgorithm, +} + +impl SignatureHashAlgorithm { + // is_compatible checks that given private key is compatible with the signature scheme. + pub(crate) fn is_compatible(&self, private_key: &CryptoPrivateKey) -> bool { + match &private_key.kind { + CryptoPrivateKeyKind::Ed25519(_) => self.signature == SignatureAlgorithm::Ed25519, + CryptoPrivateKeyKind::Ecdsa256(_) => self.signature == SignatureAlgorithm::Ecdsa, + CryptoPrivateKeyKind::Rsa256(_) => self.signature == SignatureAlgorithm::Rsa, + } + } +} + +pub(crate) fn default_signature_schemes() -> Vec { + vec![ + SignatureHashAlgorithm { + hash: HashAlgorithm::Sha256, + signature: SignatureAlgorithm::Ecdsa, + }, + SignatureHashAlgorithm { + hash: HashAlgorithm::Sha384, + signature: SignatureAlgorithm::Ecdsa, + }, + SignatureHashAlgorithm { + hash: HashAlgorithm::Sha512, + signature: SignatureAlgorithm::Ecdsa, + }, + SignatureHashAlgorithm { + hash: HashAlgorithm::Sha256, + signature: SignatureAlgorithm::Rsa, + }, + SignatureHashAlgorithm { + hash: HashAlgorithm::Sha384, + signature: SignatureAlgorithm::Rsa, + }, + SignatureHashAlgorithm { + hash: HashAlgorithm::Sha512, + signature: SignatureAlgorithm::Rsa, + }, + SignatureHashAlgorithm { + hash: HashAlgorithm::Ed25519, + signature: SignatureAlgorithm::Ed25519, + }, + ] +} + +// select Signature Scheme returns most preferred and compatible scheme. +pub(crate) fn select_signature_scheme( + sigs: &[SignatureHashAlgorithm], + private_key: &CryptoPrivateKey, +) -> Result { + for ss in sigs { + if ss.is_compatible(private_key) { + return Ok(*ss); + } + } + + Err(Error::ErrNoAvailableSignatureSchemes) +} + +// SignatureScheme identifies a signature algorithm supported by TLS. See +// RFC 8446, Section 4.2.3. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum SignatureScheme { + // RSASSA-PKCS1-v1_5 algorithms. + Pkcs1WithSha256 = 0x0401, + Pkcs1WithSha384 = 0x0501, + Pkcs1WithSha512 = 0x0601, + + // RSASSA-PSS algorithms with public key OID rsaEncryption. + PssWithSha256 = 0x0804, + PssWithSha384 = 0x0805, + PssWithSha512 = 0x0806, + + // ECDSA algorithms. Only constrained to a specific curve in TLS 1.3. + EcdsaWithP256AndSha256 = 0x0403, + EcdsaWithP384AndSha384 = 0x0503, + EcdsaWithP521AndSha512 = 0x0603, + + // EdDSA algorithms. + Ed25519 = 0x0807, + + // Legacy signature and hash algorithms for TLS 1.2. + Pkcs1WithSha1 = 0x0201, + EcdsaWithSha1 = 0x0203, +} + +// parse_signature_schemes translates []tls.SignatureScheme to []signatureHashAlgorithm. +// It returns default signature scheme list if no SignatureScheme is passed. +pub(crate) fn parse_signature_schemes( + sigs: &[u16], + insecure_hashes: bool, +) -> Result> { + if sigs.is_empty() { + return Ok(default_signature_schemes()); + } + + let mut out = vec![]; + for ss in sigs { + let sig: SignatureAlgorithm = ((*ss & 0xFF) as u8).into(); + if sig == SignatureAlgorithm::Unsupported { + return Err(Error::ErrInvalidSignatureAlgorithm); + } + let h: HashAlgorithm = (((*ss >> 8) & 0xFF) as u8).into(); + if h == HashAlgorithm::Unsupported || h.invalid() { + return Err(Error::ErrInvalidHashAlgorithm); + } + if h.insecure() && !insecure_hashes { + continue; + } + out.push(SignatureHashAlgorithm { + hash: h, + signature: sig, + }) + } + + if out.is_empty() { + Err(Error::ErrNoAvailableSignatureSchemes) + } else { + Ok(out) + } +} diff --git a/vendor/dtls/src/signature_hash_algorithm/signature_hash_algorithm_test.rs b/vendor/dtls/src/signature_hash_algorithm/signature_hash_algorithm_test.rs new file mode 100644 index 00000000..775ef327 --- /dev/null +++ b/vendor/dtls/src/signature_hash_algorithm/signature_hash_algorithm_test.rs @@ -0,0 +1,141 @@ +use super::*; + +#[test] +fn test_parse_signature_schemes() -> Result<()> { + let tests = vec![ + ( + "Translate", + vec![ + SignatureScheme::EcdsaWithP256AndSha256 as u16, + SignatureScheme::EcdsaWithP384AndSha384 as u16, + SignatureScheme::EcdsaWithP521AndSha512 as u16, + SignatureScheme::Pkcs1WithSha256 as u16, + SignatureScheme::Pkcs1WithSha384 as u16, + SignatureScheme::Pkcs1WithSha512 as u16, + ], + vec![ + SignatureHashAlgorithm { + hash: HashAlgorithm::Sha256, + signature: SignatureAlgorithm::Ecdsa, + }, + SignatureHashAlgorithm { + hash: HashAlgorithm::Sha384, + signature: SignatureAlgorithm::Ecdsa, + }, + SignatureHashAlgorithm { + hash: HashAlgorithm::Sha512, + signature: SignatureAlgorithm::Ecdsa, + }, + SignatureHashAlgorithm { + hash: HashAlgorithm::Sha256, + signature: SignatureAlgorithm::Rsa, + }, + SignatureHashAlgorithm { + hash: HashAlgorithm::Sha384, + signature: SignatureAlgorithm::Rsa, + }, + SignatureHashAlgorithm { + hash: HashAlgorithm::Sha512, + signature: SignatureAlgorithm::Rsa, + }, + ], + false, + None, + ), + ( + "InvalidSignatureAlgorithm", + vec![ + SignatureScheme::EcdsaWithP256AndSha256 as u16, // Valid + 0x04FF, // Invalid: unknown signature with SHA-256 + ], + vec![], + false, + Some(Error::ErrInvalidSignatureAlgorithm), + ), + ( + "InvalidHashAlgorithm", + vec![ + SignatureScheme::EcdsaWithP256AndSha256 as u16, // Valid + 0x0003, // Invalid: ECDSA with MD2 + ], + vec![], + false, + Some(Error::ErrInvalidHashAlgorithm), + ), + ( + "InsecureHashAlgorithmDenied", + vec![ + SignatureScheme::EcdsaWithP256AndSha256 as u16, // Valid + SignatureScheme::EcdsaWithSha1 as u16, // Insecure + ], + vec![SignatureHashAlgorithm { + hash: HashAlgorithm::Sha256, + signature: SignatureAlgorithm::Ecdsa, + }], + false, + None, + ), + ( + "InsecureHashAlgorithmAllowed", + vec![ + SignatureScheme::EcdsaWithP256AndSha256 as u16, // Valid + SignatureScheme::EcdsaWithSha1 as u16, // Insecure + ], + vec![ + SignatureHashAlgorithm { + hash: HashAlgorithm::Sha256, + signature: SignatureAlgorithm::Ecdsa, + }, + SignatureHashAlgorithm { + hash: HashAlgorithm::Sha1, + signature: SignatureAlgorithm::Ecdsa, + }, + ], + true, + None, + ), + ( + "OnlyInsecureHashAlgorithm", + vec![ + SignatureScheme::EcdsaWithSha1 as u16, // Insecure + ], + vec![], + false, + Some(Error::ErrNoAvailableSignatureSchemes), + ), + ( + "Translate", + vec![SignatureScheme::Ed25519 as u16], + vec![SignatureHashAlgorithm { + hash: HashAlgorithm::Ed25519, + signature: SignatureAlgorithm::Ed25519, + }], + false, + None, + ), + ]; + + for (name, inputs, expected, insecure_hashes, want_err) in tests { + let output = parse_signature_schemes(&inputs, insecure_hashes); + if let Some(err) = want_err { + if let Err(output_err) = output { + assert_eq!( + err.to_string(), + output_err.to_string(), + "Expected error: {err:?}, got: {output_err:?}" + ); + } else { + panic!("expect err, but got non-err for {name}"); + } + } else if let Ok(output_val) = output { + assert_eq!( + expected, output_val, + "Expected signatureHashAlgorithm:\n{expected:?}\ngot:\n{output_val:?}", + ); + } else { + panic!("expect non-err, but got err for {name}"); + } + } + + Ok(()) +} diff --git a/vendor/dtls/src/state.rs b/vendor/dtls/src/state.rs new file mode 100644 index 00000000..e911e7fa --- /dev/null +++ b/vendor/dtls/src/state.rs @@ -0,0 +1,336 @@ +use std::io::{BufWriter, Cursor}; +use std::marker::{Send, Sync}; +use std::sync::atomic::Ordering; +use std::sync::Arc; + +use async_trait::async_trait; +use portable_atomic::AtomicU16; +use rkyv::{Archive, Deserialize, Serialize}; +use tokio::sync::Mutex; +use util::{KeyingMaterialExporter, KeyingMaterialExporterError}; + +use super::cipher_suite::*; +use super::conn::*; +use super::curve::named_curve::*; +use super::extension::extension_use_srtp::SrtpProtectionProfile; +use super::handshake::handshake_random::*; +use super::prf::*; +use crate::error::*; + +// State holds the dtls connection state and implements both encoding.BinaryMarshaler and encoding.BinaryUnmarshaler +pub struct State { + pub(crate) local_epoch: Arc, + pub(crate) remote_epoch: Arc, + pub(crate) local_sequence_number: Arc>>, // uint48 + pub(crate) local_random: HandshakeRandom, + pub(crate) remote_random: HandshakeRandom, + pub(crate) master_secret: Vec, + pub(crate) cipher_suite: Arc>>>, // nil if a cipher_suite hasn't been chosen + + pub(crate) srtp_protection_profile: SrtpProtectionProfile, // Negotiated srtp_protection_profile + pub peer_certificates: Vec>, + pub identity_hint: Vec, + + pub(crate) is_client: bool, + + pub(crate) pre_master_secret: Vec, + pub(crate) extended_master_secret: bool, + + pub(crate) named_curve: NamedCurve, + pub(crate) local_keypair: Option, + pub(crate) cookie: Vec, + pub(crate) handshake_send_sequence: isize, + pub(crate) handshake_recv_sequence: isize, + pub(crate) server_name: String, + pub(crate) remote_requested_certificate: bool, // Did we get a CertificateRequest + pub(crate) local_certificates_verify: Vec, // cache CertificateVerify + pub(crate) local_verify_data: Vec, // cached VerifyData + pub(crate) local_key_signature: Vec, // cached keySignature + pub(crate) peer_certificates_verified: bool, + //pub(crate) replay_detector: Vec>, +} + +#[derive(Archive, Deserialize, Serialize, PartialEq, Debug)] +struct SerializedState { + local_epoch: u16, + remote_epoch: u16, + local_random: [u8; HANDSHAKE_RANDOM_LENGTH], + remote_random: [u8; HANDSHAKE_RANDOM_LENGTH], + cipher_suite_id: u16, + master_secret: Vec, + sequence_number: u64, + srtp_protection_profile: u16, + peer_certificates: Vec>, + identity_hint: Vec, + is_client: bool, +} + +#[derive(Archive, Deserialize, Serialize, Debug, Clone)] +pub struct KeyLogData { + pub local_random: Vec, + pub remote_random: Vec, + pub master_secret: Vec, +} + +impl Default for State { + fn default() -> Self { + State { + local_epoch: Arc::new(AtomicU16::new(0)), + remote_epoch: Arc::new(AtomicU16::new(0)), + local_sequence_number: Arc::new(Mutex::new(vec![])), + local_random: HandshakeRandom::default(), + remote_random: HandshakeRandom::default(), + master_secret: vec![], + cipher_suite: Arc::new(Mutex::new(None)), // nil if a cipher_suite hasn't been chosen + + srtp_protection_profile: SrtpProtectionProfile::Unsupported, // Negotiated srtp_protection_profile + peer_certificates: vec![], + identity_hint: vec![], + + is_client: false, + + pre_master_secret: vec![], + extended_master_secret: false, + + named_curve: NamedCurve::Unsupported, + local_keypair: None, + cookie: vec![], + handshake_send_sequence: 0, + handshake_recv_sequence: 0, + server_name: "".to_string(), + remote_requested_certificate: false, // Did we get a CertificateRequest + local_certificates_verify: vec![], // cache CertificateVerify + local_verify_data: vec![], // cached VerifyData + local_key_signature: vec![], // cached keySignature + peer_certificates_verified: false, + //replay_detector: vec![], + } + } +} + +impl State { + pub(crate) async fn clone(&self) -> Self { + let mut state = State::default(); + + if let Ok(serialized) = self.serialize().await { + let _ = state.deserialize(&serialized).await; + } + + state + } + + async fn serialize(&self) -> Result { + let mut local_rand = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(local_rand.as_mut()); + self.local_random.marshal(&mut writer)?; + } + let mut remote_rand = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(remote_rand.as_mut()); + self.remote_random.marshal(&mut writer)?; + } + + let mut local_random = [0u8; HANDSHAKE_RANDOM_LENGTH]; + let mut remote_random = [0u8; HANDSHAKE_RANDOM_LENGTH]; + + local_random.copy_from_slice(&local_rand); + remote_random.copy_from_slice(&remote_rand); + + let local_epoch = self.local_epoch.load(Ordering::SeqCst); + let remote_epoch = self.remote_epoch.load(Ordering::SeqCst); + let sequence_number = { + let lsn = self.local_sequence_number.lock().await; + lsn[local_epoch as usize] + }; + let cipher_suite_id = { + let cipher_suite = self.cipher_suite.lock().await; + match &*cipher_suite { + Some(cipher_suite) => cipher_suite.id() as u16, + None => return Err(Error::ErrCipherSuiteUnset), + } + }; + + Ok(SerializedState { + local_epoch, + remote_epoch, + local_random, + remote_random, + cipher_suite_id, + master_secret: self.master_secret.clone(), + sequence_number, + srtp_protection_profile: self.srtp_protection_profile as u16, + peer_certificates: self.peer_certificates.clone(), + identity_hint: self.identity_hint.clone(), + is_client: self.is_client, + }) + } + + async fn deserialize(&mut self, serialized: &SerializedState) -> Result<()> { + // Set epoch values + self.local_epoch + .store(serialized.local_epoch, Ordering::SeqCst); + self.remote_epoch + .store(serialized.remote_epoch, Ordering::SeqCst); + { + let mut lsn = self.local_sequence_number.lock().await; + while lsn.len() <= serialized.local_epoch as usize { + lsn.push(0); + } + lsn[serialized.local_epoch as usize] = serialized.sequence_number; + } + + // Set random values + let mut reader = Cursor::new(&serialized.local_random); + self.local_random = HandshakeRandom::unmarshal(&mut reader)?; + + let mut reader = Cursor::new(&serialized.remote_random); + self.remote_random = HandshakeRandom::unmarshal(&mut reader)?; + + self.is_client = serialized.is_client; + + // Set master secret + self.master_secret.clone_from(&serialized.master_secret); + + // Set cipher suite + self.cipher_suite = Arc::new(Mutex::new(Some(cipher_suite_for_id( + serialized.cipher_suite_id.into(), + )?))); + + self.srtp_protection_profile = serialized.srtp_protection_profile.into(); + + // Set remote certificate + self.peer_certificates + .clone_from(&serialized.peer_certificates); + self.identity_hint.clone_from(&serialized.identity_hint); + + Ok(()) + } + + pub async fn init_cipher_suite(&mut self) -> Result<()> { + let mut cipher_suite = self.cipher_suite.lock().await; + if let Some(cipher_suite) = &mut *cipher_suite { + if cipher_suite.is_initialized() { + return Ok(()); + } + + let mut local_random = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(local_random.as_mut()); + self.local_random.marshal(&mut writer)?; + } + let mut remote_random = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(remote_random.as_mut()); + self.remote_random.marshal(&mut writer)?; + } + + if self.is_client { + cipher_suite.init(&self.master_secret, &local_random, &remote_random, true) + } else { + cipher_suite.init(&self.master_secret, &remote_random, &local_random, false) + } + } else { + Err(Error::ErrCipherSuiteUnset) + } + } + + // marshal_binary is a binary.BinaryMarshaler.marshal_binary implementation + pub async fn marshal_binary(&self) -> Result> { + let serialized = self.serialize().await?; + + match rkyv::to_bytes::(&serialized).map(Vec::from) { + Ok(enc) => Ok(enc), + Err(err) => Err(Error::Other(err.to_string())), + } + } + + // unmarshal_binary is a binary.BinaryUnmarshaler.unmarshal_binary implementation + pub async fn unmarshal_binary(&mut self, data: &[u8]) -> Result<()> { + let serialized: SerializedState = + match rkyv::access::(data) + .and_then(rkyv::deserialize) + { + Ok(dec) => dec, + Err(err) => return Err(Error::Other(err.to_string())), + }; + self.deserialize(&serialized).await?; + self.init_cipher_suite().await?; + + Ok(()) + } + + /// key_log_data returns the key log data for the current state. + pub fn key_log_data(&self) -> Result { + let mut local_random = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(local_random.as_mut()); + self.local_random.marshal(&mut writer)?; + } + let mut remote_random = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(remote_random.as_mut()); + self.remote_random.marshal(&mut writer)?; + } + + Ok(KeyLogData { + local_random, + remote_random, + master_secret: self.master_secret.clone(), + }) + } +} + +#[async_trait] +impl KeyingMaterialExporter for State { + /// export_keying_material returns length bytes of exported key material in a new + /// slice as defined in RFC 5705. + /// This allows protocols to use DTLS for key establishment, but + /// then use some of the keying material for their own purposes + async fn export_keying_material( + &self, + label: &str, + context: &[u8], + length: usize, + ) -> std::result::Result, KeyingMaterialExporterError> { + use KeyingMaterialExporterError::*; + + if self.local_epoch.load(Ordering::SeqCst) == 0 { + return Err(HandshakeInProgress); + } else if !context.is_empty() { + return Err(ContextUnsupported); + } else if INVALID_KEYING_LABELS.contains(&label) { + return Err(ReservedExportKeyingMaterial); + } + + let mut local_random = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(local_random.as_mut()); + self.local_random.marshal(&mut writer)?; + } + let mut remote_random = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(remote_random.as_mut()); + self.remote_random.marshal(&mut writer)?; + } + + let mut seed = label.as_bytes().to_vec(); + if self.is_client { + seed.extend_from_slice(&local_random); + seed.extend_from_slice(&remote_random); + } else { + seed.extend_from_slice(&remote_random); + seed.extend_from_slice(&local_random); + } + + let cipher_suite = self.cipher_suite.lock().await; + if let Some(cipher_suite) = &*cipher_suite { + match prf_p_hash(&self.master_secret, &seed, length, cipher_suite.hash_func()) { + Ok(v) => Ok(v), + Err(err) => Err(Hash(err.to_string())), + } + } else { + Err(CipherSuiteUnset) + } + } +} From 2f60e61dd5a9841485c0ae5e81047b96476834d8 Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Thu, 1 Oct 2026 20:29:06 +0800 Subject: [PATCH 06/11] =?UTF-8?q?perf(video):=20=E6=B6=88=E9=99=A4?= =?UTF-8?q?=E4=B8=AD=E9=97=B4=E5=B8=A7=E5=A4=8D=E5=88=B6=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=20MJPEG=20=E6=9C=89=E7=95=8C=E9=A2=84=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 通过 Bytes 所有权和只读 AVBufferRef 引用池化 NV12 输入,消除中间 AVFrame 的整帧复制,并保留 V4L2 编码包脱离硬件缓冲的处理。 允许输出健康时有界预取 MJPEG,在输出背压时优先解码前丢帧;增加等帧、输出等待、丢帧原因及编码分段耗时统计,提供预取和所有权输入的对照开关。 补充缓冲寿命、只读保护、软件编码回退和队列策略测试及接口说明,保留现有 PTS、解码 worker、音频和加密行为。 --- libs/hwcodec/README.md | 16 + .../cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp | 88 ++++- .../cpp/ffmpeg_ram/ffmpeg_ram_encode_stats.h | 17 + libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_ffi.h | 7 + .../hwcodec/cpp/ffmpeg_ram/owned_nv12_frame.h | 51 +++ libs/hwcodec/src/ffmpeg_ram/encode.rs | 132 +++++++- src/video/codec/h264.rs | 65 +++- src/video/frame.rs | 50 +++ src/video/pipeline/encoder_state.rs | 36 +- src/video/pipeline/frame_mailbox.rs | 140 +++++++- src/video/pipeline/mjpeg_admission.rs | 125 +++++++ src/video/pipeline/mod.rs | 1 + src/video/pipeline/shared.rs | 307 ++++++++++++++++-- 13 files changed, 994 insertions(+), 41 deletions(-) create mode 100644 libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_encode_stats.h create mode 100644 libs/hwcodec/cpp/ffmpeg_ram/owned_nv12_frame.h create mode 100644 src/video/pipeline/mjpeg_admission.rs diff --git a/libs/hwcodec/README.md b/libs/hwcodec/README.md index d81e614b..f9f899f6 100644 --- a/libs/hwcodec/README.md +++ b/libs/hwcodec/README.md @@ -41,6 +41,22 @@ Based on the information above, there are several optimizations and changes made * remove hevc_vaapi because of possible poor quality * amf: not tested, https://github.com/GPUOpen-LibrariesAndSDKs/AMF/issues/378 +#### Owned NV12 input + +The `bytes` feature provides `Encoder::encode_owned_bytes` for packed NV12 +input to FFmpeg V4L2 M2M encoders. It holds a `Bytes` owner in a read-only +`AVBufferRef` until FFmpeg releases its last reference, instead of copying the +input into an intermediate AVFrame. Other encoders retain the existing copy +path. Converted or stride-compacted One-KVM input also retains that path. + +`ONE_KVM_V4L2_OWNED_INPUT=0` disables this optimization when the encoder is +created, allowing comparisons with the same binary. `ONE_KVM_VIDEO_STATS=1` +enables preparation, send-frame, receive-packet, callback and output-copy +timings. One-KVM reports them in `[VideoInputPerf]` at five-second intervals; +`copied_bytes` counts only the intermediate raw-input copy, not FFmpeg's +remaining copy into V4L2 MMAP buffers. Encoded packet detachment remains in +place to avoid retaining scarce V4L2 capture buffers in downstream caches. + ## System requirements * intel diff --git a/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp b/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp index 19ffd84b..856b751a 100644 --- a/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp +++ b/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp @@ -5,6 +5,7 @@ extern "C" { #include #include #include +#include } #include @@ -14,6 +15,8 @@ extern "C" { #include #include "common.h" +#include "ffmpeg_ram_encode_stats.h" +#include "owned_nv12_frame.h" #define LOG_MODULE "FFMPEG_RAM_ENC" #include @@ -146,6 +149,8 @@ public: RamEncodePacketCallback packet_callback_ = NULL; int offset_[AV_NUM_DATA_POINTERS] = {0}; bool force_keyframe_ = false; // Force next frame to be a keyframe + bool measure_ = false; + RamEncodeStats stats_ = {}; AVHWDeviceType hw_device_type_ = AV_HWDEVICE_TYPE_NONE; AVPixelFormat hw_pixfmt_ = AV_PIX_FMT_NONE; @@ -170,6 +175,8 @@ public: thread_count_ = thread_count; gpu_ = gpu; callback_ = callback; + const char *stats_setting = getenv("ONE_KVM_VIDEO_STATS"); + measure_ = stats_setting && strcmp(stats_setting, "1") == 0; if (name_.find("vaapi") != std::string::npos) { hw_device_type_ = AV_HWDEVICE_TYPE_VAAPI; hw_pixfmt_ = AV_PIX_FMT_VAAPI; @@ -298,6 +305,7 @@ public: int encode(const uint8_t *data, int length, const void *obj, uint64_t ms) { int ret; + const int64_t prepare_start = timer_start(); if ((ret = av_frame_make_writable(frame_)) != 0) { LOG_ERROR(std::string("av_frame_make_writable failed, ret = ") + av_err2str(ret)); @@ -316,9 +324,42 @@ public: tmp_frame = frame_; } + timer_add(stats_.prepare_us, prepare_start); + if (measure_) { + stats_.input_frames++; + stats_.copied_frames++; + const int bytes = av_image_get_buffer_size(pixfmt_, width_, height_, 1); + if (bytes > 0) + stats_.copied_bytes += bytes; + } + return do_encode(tmp_frame, obj, ms); } + int encode_owned_packet(const uint8_t *data, int length, void *owner, + RamInputRelease release, const void *obj, + uint64_t ms, RamEncodePacketCallback callback) { + if (pixfmt_ != AV_PIX_FMT_NV12 || hw_device_type_ != AV_HWDEVICE_TYPE_NONE || + name_.find("v4l2m2m") == std::string::npos) { + release(owner, const_cast(data)); + return AVERROR(ENOSYS); + } + const int64_t prepare_start = timer_start(); + auto frame = make_owned_nv12_frame(data, length, width_, height_, owner, + release); + timer_add(stats_.prepare_us, prepare_start); + if (!frame) + return AVERROR(EINVAL); + if (measure_) { + stats_.input_frames++; + stats_.borrowed_frames++; + } + packet_callback_ = callback; + int ret = do_encode(frame.get(), obj, ms); + packet_callback_ = NULL; + return ret; + } + int encode_packet(const uint8_t *data, int length, const void *obj, uint64_t ms, RamEncodePacketCallback callback) { packet_callback_ = callback; @@ -345,6 +386,20 @@ public: } private: + int64_t timer_start() const { return measure_ ? av_gettime_relative() : 0; } + + void timer_add(uint64_t &elapsed, int64_t start) const { + if (measure_) + elapsed += av_gettime_relative() - start; + } + + int send_frame(AVFrame *frame) { + const int64_t start = timer_start(); + const int ret = avcodec_send_frame(c_, frame); + timer_add(stats_.send_us, start); + return ret; + } + int set_hwframe_ctx() { AVBufferRef *hw_frames_ref; AVHWFramesContext *frames_ctx = NULL; @@ -386,13 +441,13 @@ private: } force_keyframe_ = false; - ret = avcodec_send_frame(c_, frame); + ret = send_frame(frame); if (ret == AVERROR(EAGAIN)) { int drain_ret = receive_available_packets(obj, encoded); if (drain_ret < 0) { return drain_ret; } - ret = avcodec_send_frame(c_, frame); + ret = send_frame(frame); } if (ret == AVERROR(EAGAIN)) { return encoded ? 0 : AVERROR(EAGAIN); @@ -417,7 +472,9 @@ private: auto start = util::now(); while (util::elapsed_ms(start) < DECODE_TIMEOUT_MS) { + const int64_t receive_start = timer_start(); ret = avcodec_receive_packet(c_, pkt_); + timer_add(stats_.receive_us, receive_start); if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { return 0; } @@ -432,6 +489,7 @@ private: continue; } encoded = true; + const int64_t packet_start = timer_start(); if (packet_callback_) { AVPacket *owned_pkt = av_packet_clone(pkt_); if (!owned_pkt) { @@ -447,6 +505,7 @@ private: pkt_->flags & AV_PKT_FLAG_KEY, obj); } av_packet_unref(pkt_); + timer_add(stats_.packet_us, packet_start); } return 0; @@ -704,6 +763,31 @@ extern "C" void ffmpeg_ram_free_packet(void *packet) { } } +extern "C" int ffmpeg_ram_encode_owned_packet( + FFmpegRamEncoder *encoder, const uint8_t *data, int length, void *owner, + RamInputRelease release, const void *obj, uint64_t ms, + RamEncodePacketCallback callback) { + if (!encoder || !release) { + if (release) + release(owner, const_cast(data)); + return AVERROR(EINVAL); + } + try { + return encoder->encode_owned_packet(data, length, owner, release, obj, ms, + callback); + } catch (const std::exception &e) { + encoder->packet_callback_ = NULL; + LOG_ERROR(std::string("encode_owned_packet failed, ") + e.what()); + return -1; + } +} + +extern "C" void ffmpeg_ram_encoder_stats(FFmpegRamEncoder *encoder, + RamEncodeStats *stats) { + if (encoder && stats) + *stats = encoder->stats_; +} + extern "C" int ffmpeg_ram_set_bitrate(FFmpegRamEncoder *encoder, int kbs) { try { return encoder->set_bitrate(kbs); diff --git a/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_encode_stats.h b/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_encode_stats.h new file mode 100644 index 00000000..231a195b --- /dev/null +++ b/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_encode_stats.h @@ -0,0 +1,17 @@ +#ifndef FFMPEG_RAM_ENCODE_STATS_H +#define FFMPEG_RAM_ENCODE_STATS_H + +#include + +typedef struct RamEncodeStats { + uint64_t input_frames; + uint64_t borrowed_frames; + uint64_t copied_frames; + uint64_t copied_bytes; + uint64_t prepare_us; + uint64_t send_us; + uint64_t receive_us; + uint64_t packet_us; +} RamEncodeStats; + +#endif diff --git a/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_ffi.h b/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_ffi.h index b30de54c..516ca69c 100644 --- a/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_ffi.h +++ b/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_ffi.h @@ -2,6 +2,7 @@ #define FFMPEG_RAM_FFI_H #include +#include "ffmpeg_ram_encode_stats.h" #define AV_NUM_DATA_POINTERS 8 @@ -24,6 +25,12 @@ int ffmpeg_ram_encode(void *encoder, const uint8_t *data, int length, int ffmpeg_ram_encode_packet(void *encoder, const uint8_t *data, int length, const void *obj, int64_t ms, RamEncodePacketCallback callback); +int ffmpeg_ram_encode_owned_packet(void *encoder, const uint8_t *data, + int length, void *owner, + void (*release)(void *, uint8_t *), + const void *obj, int64_t ms, + RamEncodePacketCallback callback); +void ffmpeg_ram_encoder_stats(void *encoder, RamEncodeStats *stats); void ffmpeg_ram_free_encoder(void *encoder); void ffmpeg_ram_free_packet(void *packet); int ffmpeg_ram_get_linesize_offset_length(int pix_fmt, int width, int height, diff --git a/libs/hwcodec/cpp/ffmpeg_ram/owned_nv12_frame.h b/libs/hwcodec/cpp/ffmpeg_ram/owned_nv12_frame.h new file mode 100644 index 00000000..2b88b121 --- /dev/null +++ b/libs/hwcodec/cpp/ffmpeg_ram/owned_nv12_frame.h @@ -0,0 +1,51 @@ +#ifndef OWNED_NV12_FRAME_H +#define OWNED_NV12_FRAME_H + +extern "C" { +#include +} + +#include +#include + +using RamInputRelease = void (*)(void *, uint8_t *); + +struct RamFrameDeleter { + void operator()(AVFrame *frame) const { av_frame_free(&frame); } +}; + +using RamOwnedFrame = std::unique_ptr; + +static RamOwnedFrame make_owned_nv12_frame(const uint8_t *data, int length, + int width, int height, void *owner, + RamInputRelease release) { + const int64_t pixels = static_cast(width) * height; + if (!data || !release || width <= 0 || height <= 0 || width % 2 || + height % 2 || pixels > std::numeric_limits::max() / 3 * 2 || + length < pixels + pixels / 2) { + if (release) + release(owner, const_cast(data)); + return RamOwnedFrame(); + } + RamOwnedFrame frame(av_frame_alloc()); + if (!frame) { + release(owner, const_cast(data)); + return frame; + } + frame->buf[0] = av_buffer_create(const_cast(data), length, release, + owner, AV_BUFFER_FLAG_READONLY); + if (!frame->buf[0]) { + release(owner, const_cast(data)); + return RamOwnedFrame(); + } + frame->format = AV_PIX_FMT_NV12; + frame->width = width; + frame->height = height; + frame->data[0] = frame->buf[0]->data; + frame->data[1] = frame->data[0] + pixels; + frame->linesize[0] = width; + frame->linesize[1] = width; + return frame; +} + +#endif diff --git a/libs/hwcodec/src/ffmpeg_ram/encode.rs b/libs/hwcodec/src/ffmpeg_ram/encode.rs index 7f01dff0..6daf6458 100644 --- a/libs/hwcodec/src/ffmpeg_ram/encode.rs +++ b/libs/hwcodec/src/ffmpeg_ram/encode.rs @@ -1,5 +1,7 @@ #[cfg(feature = "bytes")] -use crate::ffmpeg_ram::{ffmpeg_ram_encode_packet, ffmpeg_ram_free_packet}; +use crate::ffmpeg_ram::{ + ffmpeg_ram_encode_owned_packet, ffmpeg_ram_encode_packet, ffmpeg_ram_free_packet, +}; use crate::{ common::DataFormat::{self, *}, ffmpeg::{init_av_log, AVPixelFormat}, @@ -498,6 +500,23 @@ pub struct Encoder { pub linesize: Vec, pub offset: Vec, pub length: i32, + owned_nv12_input: bool, + measure: bool, + #[cfg(feature = "bytes")] + output_copy_us: u64, +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct EncodeInputStats { + pub input_frames: u64, + pub borrowed_frames: u64, + pub copied_frames: u64, + pub copied_bytes: u64, + pub prepare_us: u64, + pub send_us: u64, + pub receive_us: u64, + pub packet_us: u64, + pub output_copy_us: u64, } impl Encoder { @@ -545,6 +564,9 @@ impl Encoder { return Err(()); } + let owned_nv12_input = ctx.name.ends_with("_v4l2m2m") + && ctx.pixfmt == AVPixelFormat::AV_PIX_FMT_NV12 as i32 + && std::env::var("ONE_KVM_V4L2_OWNED_INPUT").as_deref() != Ok("0"); Ok(Encoder { codec, frames: Box::into_raw(Box::new(Vec::::new())), @@ -552,6 +574,10 @@ impl Encoder { linesize, offset, length: length[0], + owned_nv12_input, + measure: std::env::var("ONE_KVM_VIDEO_STATS").as_deref() == Ok("1"), + #[cfg(feature = "bytes")] + output_copy_us: 0, }) } } @@ -581,33 +607,107 @@ impl Encoder { #[cfg(feature = "bytes")] pub fn encode_bytes(&mut self, data: &[u8], ms: i64) -> Result, i32> { + let length = c_int::try_from(data.len()).map_err(|_| -22)?; unsafe { let mut frames = Vec::::new(); let result = ffmpeg_ram_encode_packet( self.codec, data.as_ptr(), - data.len() as _, + length, &mut frames as *mut _ as *const c_void, ms, Some(Encoder::packet_callback), ); - if result == -11 || result == 0 { - if self.ctx.name.contains("v4l2m2m") { - return Ok(frames - .into_iter() - .map(|frame| EncodeBytesFrame { - data: Bytes::copy_from_slice(frame.data.as_ref()), - pts: frame.pts, - key: frame.key, - }) - .collect()); - } - return Ok(frames); - } - Err(result) + self.finish_bytes_encode(frames, result) } } + pub fn supports_owned_nv12_input(&self) -> bool { + self.owned_nv12_input + } + + pub fn input_stats(&self) -> Option { + if !self.measure { + return None; + } + let mut stats = unsafe { std::mem::zeroed() }; + unsafe { crate::ffmpeg_ram::ffmpeg_ram_encoder_stats(self.codec, &mut stats) }; + Some(EncodeInputStats { + input_frames: stats.input_frames, + borrowed_frames: stats.borrowed_frames, + copied_frames: stats.copied_frames, + copied_bytes: stats.copied_bytes, + prepare_us: stats.prepare_us, + send_us: stats.send_us, + receive_us: stats.receive_us, + packet_us: stats.packet_us, + #[cfg(feature = "bytes")] + output_copy_us: self.output_copy_us, + #[cfg(not(feature = "bytes"))] + output_copy_us: 0, + }) + } + + #[cfg(feature = "bytes")] + pub fn encode_owned_bytes( + &mut self, + data: Bytes, + ms: i64, + ) -> Result, i32> { + if !self.owned_nv12_input { + return self.encode_bytes(data.as_ref(), ms); + } + let length = c_int::try_from(data.len()).map_err(|_| -22)?; + let owner = Box::new(data); + let pointer = owner.as_ptr(); + let mut frames = Vec::::new(); + let result = unsafe { + ffmpeg_ram_encode_owned_packet( + self.codec, + pointer, + length, + Box::into_raw(owner) as *mut c_void, + Some(Self::release_input), + &mut frames as *mut _ as *const c_void, + ms, + Some(Self::packet_callback), + ) + }; + self.finish_bytes_encode(frames, result) + } + + #[cfg(feature = "bytes")] + fn finish_bytes_encode( + &mut self, + frames: Vec, + result: i32, + ) -> Result, i32> { + if result != -11 && result != 0 { + return Err(result); + } + if !self.ctx.name.contains("v4l2m2m") { + return Ok(frames); + } + let start = self.measure.then(std::time::Instant::now); + let detached = frames + .into_iter() + .map(|frame| EncodeBytesFrame { + data: Bytes::copy_from_slice(frame.data.as_ref()), + pts: frame.pts, + key: frame.key, + }) + .collect(); + if let Some(start) = start { + self.output_copy_us += start.elapsed().as_micros() as u64; + } + Ok(detached) + } + + #[cfg(feature = "bytes")] + extern "C" fn release_input(owner: *mut c_void, _data: *mut u8) { + unsafe { drop(Box::from_raw(owner as *mut Bytes)) }; + } + extern "C" fn callback(data: *const u8, size: c_int, pts: i64, key: i32, obj: *const c_void) { unsafe { let frames = &mut *(obj as *mut Vec); diff --git a/src/video/codec/h264.rs b/src/video/codec/h264.rs index 8b0ac59a..7465c4a7 100644 --- a/src/video/codec/h264.rs +++ b/src/video/codec/h264.rs @@ -370,7 +370,37 @@ impl H264Encoder { self.frame_count += 1; - match self.inner.encode_bytes(data, pts_ms) { + let result = self.inner.encode_bytes(data, pts_ms); + Self::finish_encode(result) + } + + pub fn supports_owned_nv12_input(&self) -> bool { + self.inner.supports_owned_nv12_input() + } + + pub fn input_stats(&self) -> Option { + self.inner.input_stats() + } + + pub fn encode_owned_nv12(&mut self, data: Bytes, pts_ms: i64) -> Result> { + if !self.supports_owned_nv12_input() { + return self.encode_raw(data.as_ref(), pts_ms); + } + if data.len() < self.yuv_length as usize { + return Err(AppError::VideoError(format!( + "Frame data too small: {} < {}", + data.len(), + self.yuv_length + ))); + } + self.frame_count += 1; + Self::finish_encode(self.inner.encode_owned_bytes(data, pts_ms)) + } + + fn finish_encode( + result: std::result::Result, i32>, + ) -> Result> { + match result { Ok(frames) => { let owned_frames: Vec = frames .into_iter() @@ -490,6 +520,39 @@ impl Encoder for H264Encoder { mod tests { use super::*; + #[test] + fn owned_input_fallback_preserves_software_packets_and_pts() { + let config = H264Config::low_latency(Resolution::new(64, 64), 500); + let mut copied = H264Encoder::with_codec(config.clone(), "libx264").unwrap(); + let mut owned = H264Encoder::with_codec(config, "libx264").unwrap(); + assert!(!owned.supports_owned_nv12_input()); + let mut packets = 0; + for sequence in 0..12 { + let data = Bytes::from(vec![64 + sequence as u8; owned.yuv_length as usize]); + let pts = sequence * 33; + let expected = copied.encode_raw(data.as_ref(), pts).unwrap(); + let actual = owned.encode_owned_nv12(data, pts).unwrap(); + assert_eq!(actual.len(), expected.len()); + packets += actual.len(); + for (actual, expected) in actual.iter().zip(expected.iter()) { + assert_eq!(actual.data, expected.data); + assert_eq!(actual.pts, expected.pts); + assert_eq!(actual.key, expected.key); + } + } + assert!(packets > 0); + } + + #[test] + fn owned_input_rejects_short_frame_before_encoding() { + let config = H264Config::low_latency(Resolution::new(64, 64), 500); + let mut encoder = H264Encoder::with_codec(config, "libx264").unwrap(); + assert!(encoder + .encode_owned_nv12(Bytes::from_static(&[0]), 0) + .is_err()); + assert_eq!(encoder.frame_count, 0); + } + #[test] fn test_detect_encoder() { let (encoder_type, codec_name) = detect_best_encoder(1280, 720); diff --git a/src/video/frame.rs b/src/video/frame.rs index 9d0b43b0..404ffcdb 100644 --- a/src/video/frame.rs +++ b/src/video/frame.rs @@ -14,6 +14,14 @@ enum FrameData { Pooled(Arc), } +struct PooledFrameOwner(Arc); + +impl AsRef<[u8]> for PooledFrameOwner { + fn as_ref(&self) -> &[u8] { + self.0.as_slice() + } +} + impl std::fmt::Debug for FrameData { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -198,6 +206,13 @@ impl VideoFrame { } } + pub(crate) fn owned_data_bytes(&self) -> Bytes { + match &self.data { + FrameData::Bytes(bytes) => bytes.clone(), + FrameData::Pooled(buffer) => Bytes::from_owner(PooledFrameOwner(buffer.clone())), + } + } + /// Get data length pub fn len(&self) -> usize { self.data().len() @@ -302,3 +317,38 @@ impl From<&VideoFrame> for FrameMeta { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn owned_pooled_bytes_preserve_pointer_and_return_buffer_after_last_reference() { + let pool = Arc::new(FrameBufferPool::new(1)); + let buffer = Arc::new(FrameBuffer::new(vec![17; 24], Some(pool.clone()))); + let pointer = buffer.as_slice().as_ptr(); + let frame = VideoFrame::from_pooled(buffer, Resolution::new(4, 4), PixelFormat::Nv12, 4, 1); + let bytes = frame.owned_data_bytes(); + assert_eq!(bytes.as_ptr(), pointer); + let retained = bytes.slice(4..); + drop(frame); + drop(bytes); + assert!(pool.pool.lock().is_empty()); + assert_eq!(retained.as_ref(), &[17; 20]); + drop(retained); + assert_eq!(pool.pool.lock().len(), 1); + let recycled = pool.take(24); + assert_eq!(recycled.as_ptr(), pointer); + } + + #[test] + fn owned_bytes_input_is_a_shared_reference() { + let bytes = Bytes::from(vec![29; 24]); + let pointer = bytes.as_ptr(); + let frame = VideoFrame::new(bytes, Resolution::new(4, 4), PixelFormat::Nv12, 4, 1); + let owned = frame.owned_data_bytes(); + drop(frame); + assert_eq!(owned.as_ptr(), pointer); + assert_eq!(owned.as_ref(), &[29; 24]); + } +} diff --git a/src/video/pipeline/encoder_state.rs b/src/video/pipeline/encoder_state.rs index 346365b8..579665c7 100644 --- a/src/video/pipeline/encoder_state.rs +++ b/src/video/pipeline/encoder_state.rs @@ -33,6 +33,15 @@ pub(super) struct EncoderThreadState { pub(super) trait VideoEncoderTrait: Send { fn encode_raw(&mut self, data: &[u8], pts_ms: i64) -> Result>; + fn supports_owned_nv12_input(&self) -> bool { + false + } + fn encode_owned_nv12(&mut self, data: Bytes, pts_ms: i64) -> Result> { + self.encode_raw(data.as_ref(), pts_ms) + } + fn input_stats(&self) -> Option { + None + } fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()>; fn codec_name(&self) -> &str; fn request_keyframe(&mut self); @@ -67,6 +76,27 @@ impl VideoEncoderTrait for H264EncoderWrapper { self.0.set_bitrate(bitrate_kbps) } + fn supports_owned_nv12_input(&self) -> bool { + self.0.supports_owned_nv12_input() + } + + fn encode_owned_nv12(&mut self, data: Bytes, pts_ms: i64) -> Result> { + Ok(self + .0 + .encode_owned_nv12(data, pts_ms)? + .into_iter() + .map(|frame| EncodedFrame { + data: frame.data, + key: frame.key, + pts: frame.pts, + }) + .collect()) + } + + fn input_stats(&self) -> Option { + self.0.input_stats() + } + fn codec_name(&self) -> &str { self.0.codec_name() } @@ -91,7 +121,11 @@ fn create_h264_encoder( }, codec_name, )?; - info!("Created H264 encoder: {}", encoder.codec_name()); + info!( + "Created H264 encoder: {} owned_nv12_input={}", + encoder.codec_name(), + encoder.supports_owned_nv12_input() + ); Ok(Box::new(H264EncoderWrapper(encoder))) } diff --git a/src/video/pipeline/frame_mailbox.rs b/src/video/pipeline/frame_mailbox.rs index 2e0fa2c8..02316c1f 100644 --- a/src/video/pipeline/frame_mailbox.rs +++ b/src/video/pipeline/frame_mailbox.rs @@ -1,4 +1,21 @@ use std::sync::{Condvar, Mutex}; +use std::time::{Duration, Instant}; + +#[derive(Debug, Default, PartialEq, Eq)] +pub(super) struct FrameWaitTimes { + pub(super) frame: Duration, + pub(super) output: Duration, +} + +impl FrameWaitTimes { + fn record(&mut self, pending: bool, elapsed: Duration) { + if pending { + self.output += elapsed; + } else { + self.frame += elapsed; + } + } +} #[derive(Debug, PartialEq, Eq)] pub(super) enum PublishResult { @@ -50,10 +67,21 @@ impl FrameMailbox { self.receive_when(|| Some(())).map(|(frame, ())| frame) } + #[cfg(test)] pub(super) fn receive_when( &self, - mut ready: impl FnMut() -> Option, + ready: impl FnMut() -> Option, ) -> Option<(Frame, Ready)> { + self.receive_when_timed(ready, false) + .map(|(frame, ready, _)| (frame, ready)) + } + + pub(super) fn receive_when_timed( + &self, + mut ready: impl FnMut() -> Option, + measure: bool, + ) -> Option<(Frame, Ready, FrameWaitTimes)> { + let mut waits = FrameWaitTimes::default(); let mut state = self.state.lock().expect("frame mailbox lock poisoned"); loop { if state.closed { @@ -61,13 +89,18 @@ impl FrameMailbox { } if state.pending.is_some() { if let Some(ready) = ready() { - return state.pending.take().map(|frame| (frame, ready)); + return state.pending.take().map(|frame| (frame, ready, waits)); } } + let pending = state.pending.is_some(); + let wait_started = measure.then(Instant::now); state = self .available .wait(state) .expect("frame mailbox lock poisoned"); + if let Some(started) = wait_started { + waits.record(pending, started.elapsed()); + } } } @@ -175,4 +208,107 @@ mod tests { worker.join().unwrap(); } } + + #[test] + fn wait_times_separate_missing_input_from_full_output() { + let mut waits = FrameWaitTimes::default(); + waits.record(false, Duration::from_millis(3)); + waits.record(true, Duration::from_millis(7)); + waits.record(false, Duration::from_millis(2)); + assert_eq!(waits.frame, Duration::from_millis(5)); + assert_eq!(waits.output, Duration::from_millis(7)); + } + + #[test] + fn available_frame_has_no_wait_time() { + let mailbox = FrameMailbox::new(); + mailbox.publish(1, "ready"); + let result = mailbox.receive_when_timed(|| Some(42), true); + assert_eq!(result, Some(("ready", 42, FrameWaitTimes::default()))); + } + + #[test] + fn timed_output_wait_preserves_latest_frame_and_can_be_disabled() { + use std::sync::atomic::{AtomicBool, Ordering}; + + for measure in [false, true] { + let mailbox = Arc::new(FrameMailbox::new()); + mailbox.publish(1, "initial"); + let ready = Arc::new(AtomicBool::new(false)); + let receiver_mailbox = mailbox.clone(); + let receiver_ready = ready.clone(); + let (waiting_sender, waiting_receiver) = mpsc::channel(); + let (result_sender, result_receiver) = mpsc::channel(); + let worker = std::thread::spawn(move || { + let mut reported = false; + let result = receiver_mailbox.receive_when_timed( + || { + if receiver_ready.load(Ordering::Acquire) { + Some(42) + } else { + if !reported { + waiting_sender.send(()).unwrap(); + reported = true; + } + None + } + }, + measure, + ); + result_sender.send(result).unwrap(); + }); + waiting_receiver + .recv_timeout(Duration::from_secs(2)) + .unwrap(); + mailbox.publish(2, "latest"); + ready.store(true, Ordering::Release); + mailbox.notify(); + let (frame, permit, waits) = result_receiver + .recv_timeout(Duration::from_secs(2)) + .unwrap() + .unwrap(); + assert_eq!((frame, permit), ("latest", 42)); + assert_eq!(waits.frame, Duration::ZERO); + if measure { + assert!(waits.output > Duration::ZERO); + } else { + assert_eq!(waits, FrameWaitTimes::default()); + } + worker.join().unwrap(); + } + } + + #[test] + fn timed_receiver_exits_when_closed_without_output_capacity() { + let mailbox = Arc::new(FrameMailbox::new()); + mailbox.publish(1, "pending"); + let receiver = mailbox.clone(); + let (waiting_sender, waiting_receiver) = mpsc::channel(); + let (result_sender, result_receiver) = mpsc::channel(); + let worker = std::thread::spawn(move || { + let mut reported = false; + let result = receiver.receive_when_timed( + || { + if !reported { + waiting_sender.send(()).unwrap(); + reported = true; + } + None::<()> + }, + true, + ); + result_sender.send(result).unwrap(); + }); + waiting_receiver + .recv_timeout(Duration::from_secs(2)) + .unwrap(); + mailbox.close(); + assert_eq!( + result_receiver + .recv_timeout(Duration::from_secs(2)) + .unwrap(), + None + ); + worker.join().unwrap(); + } } diff --git a/src/video/pipeline/mjpeg_admission.rs b/src/video/pipeline/mjpeg_admission.rs new file mode 100644 index 00000000..b274473d --- /dev/null +++ b/src/video/pipeline/mjpeg_admission.rs @@ -0,0 +1,125 @@ +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum MjpegAdmissionPolicy { + PendingSlot, + BoundedPrefetch, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum MjpegAdmissionStage { + Capture, + Decode, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum MjpegDropReason { + PendingCapture, + PendingDecode, + BackpressureCapture, + BackpressureDecode, + WorkersBusy, +} + +impl MjpegAdmissionPolicy { + pub(super) fn from_prefetch_setting(setting: Option<&str>) -> Self { + if setting == Some("0") { + Self::PendingSlot + } else { + Self::BoundedPrefetch + } + } + + pub(super) fn check_pending(self, output_backpressured: bool) -> bool { + self == Self::PendingSlot || output_backpressured + } + + pub(super) fn drop_reason( + self, + pending: bool, + output_backpressured: bool, + stage: MjpegAdmissionStage, + ) -> Option { + if !pending || !self.check_pending(output_backpressured) { + return None; + } + Some(match (output_backpressured, stage) { + (false, MjpegAdmissionStage::Capture) => MjpegDropReason::PendingCapture, + (false, MjpegAdmissionStage::Decode) => MjpegDropReason::PendingDecode, + (true, MjpegAdmissionStage::Capture) => MjpegDropReason::BackpressureCapture, + (true, MjpegAdmissionStage::Decode) => MjpegDropReason::BackpressureDecode, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bounded_prefetch_is_default_and_zero_restores_pending_slot_policy() { + for setting in [None, Some("1"), Some(""), Some("invalid")] { + assert_eq!( + MjpegAdmissionPolicy::from_prefetch_setting(setting), + MjpegAdmissionPolicy::BoundedPrefetch + ); + } + assert_eq!( + MjpegAdmissionPolicy::from_prefetch_setting(Some("0")), + MjpegAdmissionPolicy::PendingSlot + ); + } + + #[test] + fn prefetch_keeps_decoding_while_output_has_capacity() { + for stage in [MjpegAdmissionStage::Capture, MjpegAdmissionStage::Decode] { + for pending in [false, true] { + assert_eq!( + MjpegAdmissionPolicy::BoundedPrefetch.drop_reason(pending, false, stage), + None + ); + } + } + assert!(!MjpegAdmissionPolicy::BoundedPrefetch.check_pending(false)); + } + + #[test] + fn output_backpressure_preserves_one_pending_frame() { + for policy in [ + MjpegAdmissionPolicy::BoundedPrefetch, + MjpegAdmissionPolicy::PendingSlot, + ] { + for (stage, reason) in [ + ( + MjpegAdmissionStage::Capture, + MjpegDropReason::BackpressureCapture, + ), + ( + MjpegAdmissionStage::Decode, + MjpegDropReason::BackpressureDecode, + ), + ] { + assert_eq!(policy.drop_reason(false, true, stage), None); + assert_eq!(policy.drop_reason(true, true, stage), Some(reason)); + } + } + } + + #[test] + fn legacy_policy_drops_when_pending_even_without_output_backpressure() { + for (stage, reason) in [ + ( + MjpegAdmissionStage::Capture, + MjpegDropReason::PendingCapture, + ), + (MjpegAdmissionStage::Decode, MjpegDropReason::PendingDecode), + ] { + assert_eq!( + MjpegAdmissionPolicy::PendingSlot.drop_reason(false, false, stage), + None + ); + assert_eq!( + MjpegAdmissionPolicy::PendingSlot.drop_reason(true, false, stage), + Some(reason) + ); + } + } +} diff --git a/src/video/pipeline/mod.rs b/src/video/pipeline/mod.rs index cbd0f99d..27cf8f96 100644 --- a/src/video/pipeline/mod.rs +++ b/src/video/pipeline/mod.rs @@ -3,6 +3,7 @@ mod delivery; mod encoder_state; mod frame_mailbox; +mod mjpeg_admission; mod shared; pub use delivery::EncodedVideoFrameReceiver; diff --git a/src/video/pipeline/shared.rs b/src/video/pipeline/shared.rs index 75b5f96e..92cd33c4 100644 --- a/src/video/pipeline/shared.rs +++ b/src/video/pipeline/shared.rs @@ -33,6 +33,7 @@ use super::delivery::{ }; use super::encoder_state::{build_encoder_state, should_parallel_decode_mjpeg, EncoderThreadState}; use super::frame_mailbox::{FrameMailbox, PublishResult}; +use super::mjpeg_admission::{MjpegAdmissionPolicy, MjpegAdmissionStage, MjpegDropReason}; #[cfg(all(target_os = "linux", any(target_arch = "aarch64", target_arch = "arm")))] #[path = "dmabuf.rs"] @@ -181,6 +182,7 @@ fn spawn_mjpeg_decode_workers( frame_mailbox: &Arc, buffer_pool: &Arc, resolution: Resolution, + admission_policy: MjpegAdmissionPolicy, ) -> Vec> { let available = std::thread::available_parallelism() .map(|count| count.get()) @@ -206,10 +208,11 @@ fn spawn_mjpeg_decode_workers( worker_buffer_pool.put(job.data); break; } - if worker_frame_mailbox.has_pending() { - worker_pipeline - .mjpeg_dropped_before_decode - .fetch_add(1, Ordering::Relaxed); + if worker_pipeline.drop_pending_mjpeg( + &worker_frame_mailbox, + admission_policy, + MjpegAdmissionStage::Decode, + ) { worker_buffer_pool.put(job.data); continue; } @@ -488,10 +491,47 @@ fn log_encoding_error( } } +fn latency_percentile_ms(sorted_time_ns: &[u64], percentile: usize) -> f64 { + if sorted_time_ns.is_empty() { + return 0.0; + } + let index = (sorted_time_ns.len() * percentile.clamp(1, 100)).div_ceil(100) - 1; + sorted_time_ns[index] as f64 / 1_000_000.0 +} + +fn log_encoder_input_performance( + stats: hwcodec::ffmpeg_ram::encode::EncodeInputStats, + previous: &mut hwcodec::ffmpeg_ram::encode::EncodeInputStats, +) { + let delta = |current: u64, before: u64| current.saturating_sub(before); + let inputs = delta(stats.input_frames, previous.input_frames); + let average_ms = |current: u64, before: u64| { + if inputs == 0 { + 0.0 + } else { + delta(current, before) as f64 / inputs as f64 / 1000.0 + } + }; + info!( + "[VideoInputPerf] inputs={} borrowed={} copied={} copied_bytes={} prepare_ms={:.3} send_frame_ms={:.3} receive_packet_ms={:.3} packet_callback_ms={:.3} output_copy_ms={:.3}", + inputs, + delta(stats.borrowed_frames, previous.borrowed_frames), + delta(stats.copied_frames, previous.copied_frames), + delta(stats.copied_bytes, previous.copied_bytes), + average_ms(stats.prepare_us, previous.prepare_us), + average_ms(stats.send_us, previous.send_us), + average_ms(stats.receive_us, previous.receive_us), + average_ms(stats.packet_us, previous.packet_us), + average_ms(stats.output_copy_us, previous.output_copy_us), + ); + *previous = stats; +} + fn log_pipeline_performance( stats: &SharedVideoPipelineStats, previous: &SharedVideoPipelineStats, elapsed: f64, + sorted_encode_time_ns: &[u64], ) { let delta = |current: u64, previous: u64| current.saturating_sub(previous); let decoded = delta(stats.mjpeg_decoded_frames, previous.mjpeg_decoded_frames); @@ -522,6 +562,21 @@ fn log_pipeline_performance( delta(stats.encoded_frames_skipped_for_slow_subscribers, previous.encoded_frames_skipped_for_slow_subscribers), send_errors, ); + info!( + "[VideoQueuePerf] window_s={:.2} input_fps={:.2} frame_wait_ms={:.2} output_wait_ms={:.2} timed_inputs={} encode_p95_ms={:.2} encode_p99_ms={:.2} drop_pending_capture={} drop_pending_decode={} drop_backpressure_capture={} drop_backpressure_decode={} drop_workers_busy={}", + elapsed, + encoded_inputs as f64 / elapsed, + average_ms(delta(stats.encoder_frame_wait_ns, previous.encoder_frame_wait_ns), encoded_inputs), + average_ms(delta(stats.encoder_output_wait_ns, previous.encoder_output_wait_ns), encoded_inputs), + sorted_encode_time_ns.len(), + latency_percentile_ms(sorted_encode_time_ns, 95), + latency_percentile_ms(sorted_encode_time_ns, 99), + delta(stats.mjpeg_dropped_pending_capture, previous.mjpeg_dropped_pending_capture), + delta(stats.mjpeg_dropped_pending_decode, previous.mjpeg_dropped_pending_decode), + delta(stats.mjpeg_dropped_backpressure_capture, previous.mjpeg_dropped_backpressure_capture), + delta(stats.mjpeg_dropped_backpressure_decode, previous.mjpeg_dropped_backpressure_decode), + delta(stats.mjpeg_dropped_workers_busy, previous.mjpeg_dropped_workers_busy), + ); } /// Pipeline statistics @@ -530,12 +585,19 @@ pub struct SharedVideoPipelineStats { pub current_fps: f32, pub mjpeg_decoded_frames: u64, pub mjpeg_dropped_before_decode: u64, + pub mjpeg_dropped_pending_capture: u64, + pub mjpeg_dropped_pending_decode: u64, + pub mjpeg_dropped_backpressure_capture: u64, + pub mjpeg_dropped_backpressure_decode: u64, + pub mjpeg_dropped_workers_busy: u64, pub decoded_frames_not_encoded: u64, pub mjpeg_decode_time_ns: u64, pub encoded_input_frames: u64, pub encoded_frames: u64, pub encoded_bytes: u64, pub encode_time_ns: u64, + pub encoder_frame_wait_ns: u64, + pub encoder_output_wait_ns: u64, pub output_backpressure_events: u64, pub encoded_frames_skipped_for_slow_subscribers: u64, pub sent_frames: u64, @@ -560,12 +622,19 @@ pub struct SharedVideoPipeline { frame_mailbox: FrameMailboxHandle, mjpeg_decoded_frames: AtomicU64, mjpeg_dropped_before_decode: AtomicU64, + mjpeg_dropped_pending_capture: AtomicU64, + mjpeg_dropped_pending_decode: AtomicU64, + mjpeg_dropped_backpressure_capture: AtomicU64, + mjpeg_dropped_backpressure_decode: AtomicU64, + mjpeg_dropped_workers_busy: AtomicU64, decoded_frames_not_encoded: AtomicU64, mjpeg_decode_time_ns: AtomicU64, encoded_input_frames: AtomicU64, encoded_frames: AtomicU64, encoded_bytes: AtomicU64, encode_time_ns: AtomicU64, + encoder_frame_wait_ns: AtomicU64, + encoder_output_wait_ns: AtomicU64, output_backpressure_events: AtomicU64, output_backpressured: AtomicBool, encoded_frames_skipped_for_slow_subscribers: AtomicU64, @@ -620,12 +689,19 @@ impl SharedVideoPipeline { frame_mailbox: Arc::new(ParkingMutex::new(None)), mjpeg_decoded_frames: AtomicU64::new(0), mjpeg_dropped_before_decode: AtomicU64::new(0), + mjpeg_dropped_pending_capture: AtomicU64::new(0), + mjpeg_dropped_pending_decode: AtomicU64::new(0), + mjpeg_dropped_backpressure_capture: AtomicU64::new(0), + mjpeg_dropped_backpressure_decode: AtomicU64::new(0), + mjpeg_dropped_workers_busy: AtomicU64::new(0), decoded_frames_not_encoded: AtomicU64::new(0), mjpeg_decode_time_ns: AtomicU64::new(0), encoded_input_frames: AtomicU64::new(0), encoded_frames: AtomicU64::new(0), encoded_bytes: AtomicU64::new(0), encode_time_ns: AtomicU64::new(0), + encoder_frame_wait_ns: AtomicU64::new(0), + encoder_output_wait_ns: AtomicU64::new(0), output_backpressure_events: AtomicU64::new(0), output_backpressured: AtomicBool::new(false), encoded_frames_skipped_for_slow_subscribers: AtomicU64::new(0), @@ -786,12 +862,25 @@ impl SharedVideoPipeline { stats.mjpeg_decoded_frames = self.mjpeg_decoded_frames.load(Ordering::Relaxed); stats.mjpeg_dropped_before_decode = self.mjpeg_dropped_before_decode.load(Ordering::Relaxed); + stats.mjpeg_dropped_pending_capture = + self.mjpeg_dropped_pending_capture.load(Ordering::Relaxed); + stats.mjpeg_dropped_pending_decode = + self.mjpeg_dropped_pending_decode.load(Ordering::Relaxed); + stats.mjpeg_dropped_backpressure_capture = self + .mjpeg_dropped_backpressure_capture + .load(Ordering::Relaxed); + stats.mjpeg_dropped_backpressure_decode = self + .mjpeg_dropped_backpressure_decode + .load(Ordering::Relaxed); + stats.mjpeg_dropped_workers_busy = self.mjpeg_dropped_workers_busy.load(Ordering::Relaxed); stats.decoded_frames_not_encoded = self.decoded_frames_not_encoded.load(Ordering::Relaxed); stats.mjpeg_decode_time_ns = self.mjpeg_decode_time_ns.load(Ordering::Relaxed); stats.encoded_input_frames = self.encoded_input_frames.load(Ordering::Relaxed); stats.encoded_frames = self.encoded_frames.load(Ordering::Relaxed); stats.encoded_bytes = self.encoded_bytes.load(Ordering::Relaxed); stats.encode_time_ns = self.encode_time_ns.load(Ordering::Relaxed); + stats.encoder_frame_wait_ns = self.encoder_frame_wait_ns.load(Ordering::Relaxed); + stats.encoder_output_wait_ns = self.encoder_output_wait_ns.load(Ordering::Relaxed); stats.output_backpressure_events = self.output_backpressure_events.load(Ordering::Relaxed); stats.encoded_frames_skipped_for_slow_subscribers = self .encoded_frames_skipped_for_slow_subscribers @@ -802,6 +891,37 @@ impl SharedVideoPipeline { stats } + fn record_mjpeg_drop(&self, reason: MjpegDropReason) { + let counter = match reason { + MjpegDropReason::PendingCapture => &self.mjpeg_dropped_pending_capture, + MjpegDropReason::PendingDecode => &self.mjpeg_dropped_pending_decode, + MjpegDropReason::BackpressureCapture => &self.mjpeg_dropped_backpressure_capture, + MjpegDropReason::BackpressureDecode => &self.mjpeg_dropped_backpressure_decode, + MjpegDropReason::WorkersBusy => &self.mjpeg_dropped_workers_busy, + }; + counter.fetch_add(1, Ordering::Relaxed); + self.mjpeg_dropped_before_decode + .fetch_add(1, Ordering::Relaxed); + } + + fn drop_pending_mjpeg( + &self, + mailbox: &VideoFrameMailbox, + policy: MjpegAdmissionPolicy, + stage: MjpegAdmissionStage, + ) -> bool { + let backpressured = self.output_backpressured.load(Ordering::Acquire); + if !policy.check_pending(backpressured) { + return false; + } + if let Some(reason) = policy.drop_reason(mailbox.has_pending(), backpressured, stage) { + self.record_mjpeg_drop(reason); + true + } else { + false + } + } + /// Check if running pub fn is_running(&self) -> bool { *self.running_rx.borrow() @@ -1150,21 +1270,36 @@ impl SharedVideoPipeline { } let mut encoder_config = config.clone(); + let admission_policy = MjpegAdmissionPolicy::from_prefetch_setting( + std::env::var("ONE_KVM_MJPEG_PREFETCH").ok().as_deref(), + ); if parallel_mjpeg_decode { encoder_config.input_format = PixelFormat::Nv12; info!("Using parallel libyuv MJPEG decode with hardware encoding"); + info!("MJPEG admission policy: {:?}", admission_policy); } let mut encoder_state = build_encoder_state(&encoder_config)?; let frame_mailbox = Arc::new(VideoFrameMailbox::new()); *self.frame_mailbox.lock() = Some(frame_mailbox.clone()); self.mjpeg_decoded_frames.store(0, Ordering::Relaxed); self.mjpeg_dropped_before_decode.store(0, Ordering::Relaxed); + self.mjpeg_dropped_pending_capture + .store(0, Ordering::Relaxed); + self.mjpeg_dropped_pending_decode + .store(0, Ordering::Relaxed); + self.mjpeg_dropped_backpressure_capture + .store(0, Ordering::Relaxed); + self.mjpeg_dropped_backpressure_decode + .store(0, Ordering::Relaxed); + self.mjpeg_dropped_workers_busy.store(0, Ordering::Relaxed); self.decoded_frames_not_encoded.store(0, Ordering::Relaxed); self.mjpeg_decode_time_ns.store(0, Ordering::Relaxed); self.encoded_input_frames.store(0, Ordering::Relaxed); self.encoded_frames.store(0, Ordering::Relaxed); self.encoded_bytes.store(0, Ordering::Relaxed); self.encode_time_ns.store(0, Ordering::Relaxed); + self.encoder_frame_wait_ns.store(0, Ordering::Relaxed); + self.encoder_output_wait_ns.store(0, Ordering::Relaxed); self.output_backpressure_events.store(0, Ordering::Relaxed); self.output_backpressured.store(false, Ordering::Release); self.encoded_frames_skipped_for_slow_subscribers @@ -1202,12 +1337,14 @@ impl SharedVideoPipeline { let perf_logging = std::env::var("ONE_KVM_VIDEO_STATS").as_deref() == Ok("1"); let mut last_perf_time = Instant::now(); let mut last_perf_stats = pipeline.stats_snapshot(); + let mut last_input_stats = Default::default(); + let mut encode_latencies = Vec::with_capacity(if perf_logging { 256 } else { 0 }); let encode_error_throttler = LogThrottler::with_secs(ENCODE_ERROR_THROTTLE_SECS); let mut suppressed_encode_errors: HashMap = HashMap::new(); while pipeline.running_flag.load(Ordering::Acquire) { - let Some((frame, reservations)) = - frame_mailbox.receive_when(|| pipeline.reserve_outputs()) + let Some((frame, reservations, waits)) = frame_mailbox + .receive_when_timed(|| pipeline.reserve_outputs(), perf_logging) else { break; }; @@ -1223,6 +1360,14 @@ impl SharedVideoPipeline { } continue; } + if perf_logging { + pipeline + .encoder_frame_wait_ns + .fetch_add(waits.frame.as_nanos() as u64, Ordering::Relaxed); + pipeline + .encoder_output_wait_ns + .fetch_add(waits.output.as_nanos() as u64, Ordering::Relaxed); + } while let Ok(cmd) = cmd_rx.try_recv() { if let Err(e) = pipeline.apply_cmd(&mut encoder_state, cmd) { @@ -1236,10 +1381,13 @@ impl SharedVideoPipeline { .fetch_add(1, Ordering::Relaxed); let encode_started = Instant::now(); let encode_result = pipeline.encode_frame_sync(&mut encoder_state, &frame); - pipeline.encode_time_ns.fetch_add( - encode_started.elapsed().as_nanos() as u64, - Ordering::Relaxed, - ); + let encode_elapsed_ns = encode_started.elapsed().as_nanos() as u64; + pipeline + .encode_time_ns + .fetch_add(encode_elapsed_ns, Ordering::Relaxed); + if perf_logging { + encode_latencies.push(encode_elapsed_ns); + } match encode_result { Ok(encoded_frames) => { @@ -1282,7 +1430,21 @@ impl SharedVideoPipeline { if perf_logging && last_perf_time.elapsed() >= Duration::from_secs(5) { let elapsed = last_perf_time.elapsed().as_secs_f64(); let stats = pipeline.stats_snapshot(); - log_pipeline_performance(&stats, &last_perf_stats, elapsed); + encode_latencies.sort_unstable(); + log_pipeline_performance( + &stats, + &last_perf_stats, + elapsed, + &encode_latencies, + ); + if let Some(stats) = encoder_state + .encoder + .as_ref() + .and_then(|encoder| encoder.input_stats()) + { + log_encoder_input_performance(stats, &mut last_input_stats); + } + encode_latencies.clear(); last_perf_stats = stats; last_perf_time = Instant::now(); } @@ -1324,6 +1486,7 @@ impl SharedVideoPipeline { &frame_mailbox, &buffer_pool, config.resolution, + admission_policy, ) }) .filter(|senders| !senders.is_empty()); @@ -1676,11 +1839,12 @@ impl SharedVideoPipeline { if parallel_mjpeg_decode && pixel_format == PixelFormat::Mjpeg - && frame_mailbox.has_pending() + && pipeline.drop_pending_mjpeg( + &frame_mailbox, + admission_policy, + MjpegAdmissionStage::Capture, + ) { - pipeline - .mjpeg_dropped_before_decode - .fetch_add(1, Ordering::Relaxed); buffer_pool.put(owned); continue; } @@ -1712,9 +1876,7 @@ impl SharedVideoPipeline { } } if let Some(job) = pending { - pipeline - .mjpeg_dropped_before_decode - .fetch_add(1, Ordering::Relaxed); + pipeline.record_mjpeg_drop(MjpegDropReason::WorkersBusy); buffer_pool.put(job.data); } continue; @@ -1895,7 +2057,15 @@ impl SharedVideoPipeline { encoder.encode_raw(nv12_data, pts_ms) } else { // Direct input (already in correct format) - encoder.encode_raw(raw_frame, pts_ms) + if decoded_buf.is_none() + && compacted_buf.is_none() + && frame.format == PixelFormat::Nv12 + && encoder.supports_owned_nv12_input() + { + encoder.encode_owned_nv12(frame.owned_data_bytes(), pts_ms) + } else { + encoder.encode_raw(raw_frame, pts_ms) + } }; match encode_result { @@ -2368,6 +2538,105 @@ mod tests { assert_eq!(stats.decoded_frames_not_encoded, 5); } + #[test] + fn mjpeg_drop_reasons_sum_to_total_without_double_counting() { + let pipeline = delivery_test_pipeline(); + for reason in [ + MjpegDropReason::PendingCapture, + MjpegDropReason::PendingDecode, + MjpegDropReason::BackpressureCapture, + MjpegDropReason::BackpressureDecode, + MjpegDropReason::WorkersBusy, + ] { + pipeline.record_mjpeg_drop(reason); + } + pipeline.encoder_frame_wait_ns.store(123, Ordering::Relaxed); + pipeline + .encoder_output_wait_ns + .store(456, Ordering::Relaxed); + let stats = pipeline.stats_snapshot(); + assert_eq!(stats.mjpeg_dropped_before_decode, 5); + assert_eq!(stats.mjpeg_dropped_pending_capture, 1); + assert_eq!(stats.mjpeg_dropped_pending_decode, 1); + assert_eq!(stats.mjpeg_dropped_backpressure_capture, 1); + assert_eq!(stats.mjpeg_dropped_backpressure_decode, 1); + assert_eq!(stats.mjpeg_dropped_workers_busy, 1); + assert_eq!(stats.encoder_frame_wait_ns, 123); + assert_eq!(stats.encoder_output_wait_ns, 456); + } + + #[test] + fn bounded_prefetch_accepts_pending_frame_when_output_is_healthy() { + let pipeline = delivery_test_pipeline(); + let mailbox = VideoFrameMailbox::new(); + mailbox.publish(1, delivery_test_raw_frame(1)); + for stage in [MjpegAdmissionStage::Capture, MjpegAdmissionStage::Decode] { + assert!(!pipeline.drop_pending_mjpeg( + &mailbox, + MjpegAdmissionPolicy::BoundedPrefetch, + stage, + )); + assert!(pipeline.drop_pending_mjpeg( + &mailbox, + MjpegAdmissionPolicy::PendingSlot, + stage, + )); + } + assert!(mailbox.has_pending()); + let stats = pipeline.stats_snapshot(); + assert_eq!(stats.mjpeg_dropped_before_decode, 2); + assert_eq!(stats.mjpeg_dropped_pending_capture, 1); + assert_eq!(stats.mjpeg_dropped_pending_decode, 1); + } + + #[test] + fn bounded_prefetch_drops_only_with_pending_frame_and_output_backpressure() { + let pipeline = delivery_test_pipeline(); + let mailbox = VideoFrameMailbox::new(); + let mut receiver = pipeline.subscribe(); + let reservations = pipeline.reserve_outputs().unwrap(); + pipeline.broadcast_encoded_batch(vec![delivery_test_frame(1, true)].into(), reservations); + assert!(pipeline.reserve_outputs().is_none()); + for stage in [MjpegAdmissionStage::Capture, MjpegAdmissionStage::Decode] { + assert!(!pipeline.drop_pending_mjpeg( + &mailbox, + MjpegAdmissionPolicy::BoundedPrefetch, + stage, + )); + } + mailbox.publish(1, delivery_test_raw_frame(1)); + for stage in [MjpegAdmissionStage::Capture, MjpegAdmissionStage::Decode] { + assert!(pipeline.drop_pending_mjpeg( + &mailbox, + MjpegAdmissionPolicy::BoundedPrefetch, + stage, + )); + } + assert_eq!(receiver.try_recv().unwrap().sequence, 1); + let _reservations = pipeline.reserve_outputs().unwrap(); + assert!(!pipeline.drop_pending_mjpeg( + &mailbox, + MjpegAdmissionPolicy::BoundedPrefetch, + MjpegAdmissionStage::Capture, + )); + assert!(mailbox.has_pending()); + let stats = pipeline.stats_snapshot(); + assert_eq!(stats.mjpeg_dropped_before_decode, 2); + assert_eq!(stats.mjpeg_dropped_backpressure_capture, 1); + assert_eq!(stats.mjpeg_dropped_backpressure_decode, 1); + assert_eq!(stats.mjpeg_dropped_pending_capture, 0); + assert_eq!(stats.mjpeg_dropped_pending_decode, 0); + } + + #[test] + fn encoding_percentiles_use_nearest_rank_and_handle_empty_windows() { + assert_eq!(latency_percentile_ms(&[], 95), 0.0); + assert_eq!(latency_percentile_ms(&[3_000_000], 99), 3.0); + let sorted: Vec = (1..=100).map(|millis| millis * 1_000_000).collect(); + assert_eq!(latency_percentile_ms(&sorted, 95), 95.0); + assert_eq!(latency_percentile_ms(&sorted, 99), 99.0); + } + #[test] fn encoder_packet_pts_is_preserved() { use super::super::encoder_state::{EncodedFrame, VideoEncoderTrait}; From 4f59e781792126a477160a432b19f26caed6020e Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Thu, 1 Oct 2026 22:46:36 +0800 Subject: [PATCH 07/11] =?UTF-8?q?refactor:=20=E5=B0=86=20DTLS=20=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0=E4=BE=9D=E8=B5=96=E8=BF=81=E7=A7=BB=E5=88=B0=20libs?= =?UTF-8?q?=20=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 2 +- {vendor => libs}/dtls/Cargo.toml | 0 {vendor => libs}/dtls/LICENSE-APACHE | 0 {vendor => libs}/dtls/LICENSE-MIT | 0 {vendor => libs}/dtls/README.md | 2 +- {vendor => libs}/dtls/src/alert/alert_test.rs | 0 {vendor => libs}/dtls/src/alert/mod.rs | 0 {vendor => libs}/dtls/src/application_data.rs | 0 {vendor => libs}/dtls/src/certificate_verifier.rs | 0 .../dtls/src/change_cipher_spec/change_cipher_spec_test.rs | 0 {vendor => libs}/dtls/src/change_cipher_spec/mod.rs | 0 .../dtls/src/cipher_suite/cipher_suite_aes_128_ccm.rs | 0 .../dtls/src/cipher_suite/cipher_suite_aes_128_gcm_sha256.rs | 0 .../dtls/src/cipher_suite/cipher_suite_aes_256_cbc_sha.rs | 0 .../src/cipher_suite/cipher_suite_chacha20_poly1305_sha256.rs | 0 .../cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm.rs | 0 .../cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8.rs | 0 .../src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm.rs | 0 .../src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm8.rs | 0 .../cipher_suite_tls_psk_with_aes_128_gcm_sha256.rs | 0 {vendor => libs}/dtls/src/cipher_suite/mod.rs | 0 {vendor => libs}/dtls/src/client_certificate_type.rs | 0 {vendor => libs}/dtls/src/compression_methods.rs | 0 {vendor => libs}/dtls/src/config.rs | 0 {vendor => libs}/dtls/src/conn/conn_test.rs | 0 {vendor => libs}/dtls/src/conn/mod.rs | 0 {vendor => libs}/dtls/src/content.rs | 0 {vendor => libs}/dtls/src/crypto/crypto_cbc.rs | 0 {vendor => libs}/dtls/src/crypto/crypto_ccm.rs | 0 {vendor => libs}/dtls/src/crypto/crypto_chacha20.rs | 0 {vendor => libs}/dtls/src/crypto/crypto_gcm.rs | 0 {vendor => libs}/dtls/src/crypto/crypto_test.rs | 0 {vendor => libs}/dtls/src/crypto/mod.rs | 0 {vendor => libs}/dtls/src/crypto/padding.rs | 0 {vendor => libs}/dtls/src/curve/mod.rs | 0 {vendor => libs}/dtls/src/curve/named_curve.rs | 0 {vendor => libs}/dtls/src/error.rs | 0 {vendor => libs}/dtls/src/extension/extension_server_name.rs | 0 .../extension_server_name/extension_server_name_test.rs | 0 .../dtls/src/extension/extension_supported_elliptic_curves.rs | 0 .../extension_supported_elliptic_curves_test.rs | 0 .../dtls/src/extension/extension_supported_point_formats.rs | 0 .../extension_supported_point_formats_test.rs | 0 .../src/extension/extension_supported_signature_algorithms.rs | 0 .../extension_supported_signature_algorithms_test.rs | 0 .../dtls/src/extension/extension_use_extended_master_secret.rs | 0 .../extension_use_extended_master_secret_test.rs | 0 {vendor => libs}/dtls/src/extension/extension_use_srtp.rs | 0 .../src/extension/extension_use_srtp/extension_use_srtp_test.rs | 0 {vendor => libs}/dtls/src/extension/mod.rs | 0 {vendor => libs}/dtls/src/extension/renegotiation_info.rs | 0 .../src/extension/renegotiation_info/renegotiation_info_test.rs | 0 {vendor => libs}/dtls/src/flight/flight0.rs | 0 {vendor => libs}/dtls/src/flight/flight1.rs | 0 {vendor => libs}/dtls/src/flight/flight2.rs | 0 {vendor => libs}/dtls/src/flight/flight3.rs | 0 {vendor => libs}/dtls/src/flight/flight4.rs | 0 {vendor => libs}/dtls/src/flight/flight5.rs | 0 {vendor => libs}/dtls/src/flight/flight6.rs | 0 {vendor => libs}/dtls/src/flight/mod.rs | 0 .../dtls/src/fragment_buffer/fragment_buffer_test.rs | 0 {vendor => libs}/dtls/src/fragment_buffer/mod.rs | 0 {vendor => libs}/dtls/src/handshake/handshake_cache.rs | 0 .../dtls/src/handshake/handshake_cache/handshake_cache_test.rs | 0 {vendor => libs}/dtls/src/handshake/handshake_header.rs | 0 .../dtls/src/handshake/handshake_message_certificate.rs | 0 .../handshake_message_certificate_test.rs | 0 .../dtls/src/handshake/handshake_message_certificate_request.rs | 0 .../handshake_message_certificate_request_test.rs | 0 .../dtls/src/handshake/handshake_message_certificate_verify.rs | 0 .../handshake_message_certificate_verify_test.rs | 0 .../dtls/src/handshake/handshake_message_client_hello.rs | 0 .../handshake_message_client_hello_test.rs | 0 .../dtls/src/handshake/handshake_message_client_key_exchange.rs | 0 .../handshake_message_client_key_exchange_test.rs | 0 .../dtls/src/handshake/handshake_message_finished.rs | 0 .../handshake_message_finished_test.rs | 0 .../src/handshake/handshake_message_hello_verify_request.rs | 0 .../handshake_message_hello_verify_request_test.rs | 0 .../dtls/src/handshake/handshake_message_server_hello.rs | 0 .../handshake_message_server_hello_test.rs | 0 .../dtls/src/handshake/handshake_message_server_hello_done.rs | 0 .../handshake_message_server_hello_done_test.rs | 0 .../dtls/src/handshake/handshake_message_server_key_exchange.rs | 0 .../handshake_message_server_key_exchange_test.rs | 0 {vendor => libs}/dtls/src/handshake/handshake_random.rs | 0 {vendor => libs}/dtls/src/handshake/handshake_test.rs | 0 {vendor => libs}/dtls/src/handshake/mod.rs | 0 {vendor => libs}/dtls/src/handshaker.rs | 0 {vendor => libs}/dtls/src/lib.rs | 0 {vendor => libs}/dtls/src/listener.rs | 0 {vendor => libs}/dtls/src/prf/mod.rs | 0 {vendor => libs}/dtls/src/prf/prf_test.rs | 0 {vendor => libs}/dtls/src/record_layer/mod.rs | 0 {vendor => libs}/dtls/src/record_layer/record_layer_header.rs | 0 {vendor => libs}/dtls/src/record_layer/record_layer_test.rs | 0 {vendor => libs}/dtls/src/signature_hash_algorithm/mod.rs | 0 .../signature_hash_algorithm/signature_hash_algorithm_test.rs | 0 {vendor => libs}/dtls/src/state.rs | 0 99 files changed, 2 insertions(+), 2 deletions(-) rename {vendor => libs}/dtls/Cargo.toml (100%) rename {vendor => libs}/dtls/LICENSE-APACHE (100%) rename {vendor => libs}/dtls/LICENSE-MIT (100%) rename {vendor => libs}/dtls/README.md (92%) rename {vendor => libs}/dtls/src/alert/alert_test.rs (100%) rename {vendor => libs}/dtls/src/alert/mod.rs (100%) rename {vendor => libs}/dtls/src/application_data.rs (100%) rename {vendor => libs}/dtls/src/certificate_verifier.rs (100%) rename {vendor => libs}/dtls/src/change_cipher_spec/change_cipher_spec_test.rs (100%) rename {vendor => libs}/dtls/src/change_cipher_spec/mod.rs (100%) rename {vendor => libs}/dtls/src/cipher_suite/cipher_suite_aes_128_ccm.rs (100%) rename {vendor => libs}/dtls/src/cipher_suite/cipher_suite_aes_128_gcm_sha256.rs (100%) rename {vendor => libs}/dtls/src/cipher_suite/cipher_suite_aes_256_cbc_sha.rs (100%) rename {vendor => libs}/dtls/src/cipher_suite/cipher_suite_chacha20_poly1305_sha256.rs (100%) rename {vendor => libs}/dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm.rs (100%) rename {vendor => libs}/dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8.rs (100%) rename {vendor => libs}/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm.rs (100%) rename {vendor => libs}/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm8.rs (100%) rename {vendor => libs}/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_gcm_sha256.rs (100%) rename {vendor => libs}/dtls/src/cipher_suite/mod.rs (100%) rename {vendor => libs}/dtls/src/client_certificate_type.rs (100%) rename {vendor => libs}/dtls/src/compression_methods.rs (100%) rename {vendor => libs}/dtls/src/config.rs (100%) rename {vendor => libs}/dtls/src/conn/conn_test.rs (100%) rename {vendor => libs}/dtls/src/conn/mod.rs (100%) rename {vendor => libs}/dtls/src/content.rs (100%) rename {vendor => libs}/dtls/src/crypto/crypto_cbc.rs (100%) rename {vendor => libs}/dtls/src/crypto/crypto_ccm.rs (100%) rename {vendor => libs}/dtls/src/crypto/crypto_chacha20.rs (100%) rename {vendor => libs}/dtls/src/crypto/crypto_gcm.rs (100%) rename {vendor => libs}/dtls/src/crypto/crypto_test.rs (100%) rename {vendor => libs}/dtls/src/crypto/mod.rs (100%) rename {vendor => libs}/dtls/src/crypto/padding.rs (100%) rename {vendor => libs}/dtls/src/curve/mod.rs (100%) rename {vendor => libs}/dtls/src/curve/named_curve.rs (100%) rename {vendor => libs}/dtls/src/error.rs (100%) rename {vendor => libs}/dtls/src/extension/extension_server_name.rs (100%) rename {vendor => libs}/dtls/src/extension/extension_server_name/extension_server_name_test.rs (100%) rename {vendor => libs}/dtls/src/extension/extension_supported_elliptic_curves.rs (100%) rename {vendor => libs}/dtls/src/extension/extension_supported_elliptic_curves/extension_supported_elliptic_curves_test.rs (100%) rename {vendor => libs}/dtls/src/extension/extension_supported_point_formats.rs (100%) rename {vendor => libs}/dtls/src/extension/extension_supported_point_formats/extension_supported_point_formats_test.rs (100%) rename {vendor => libs}/dtls/src/extension/extension_supported_signature_algorithms.rs (100%) rename {vendor => libs}/dtls/src/extension/extension_supported_signature_algorithms/extension_supported_signature_algorithms_test.rs (100%) rename {vendor => libs}/dtls/src/extension/extension_use_extended_master_secret.rs (100%) rename {vendor => libs}/dtls/src/extension/extension_use_extended_master_secret/extension_use_extended_master_secret_test.rs (100%) rename {vendor => libs}/dtls/src/extension/extension_use_srtp.rs (100%) rename {vendor => libs}/dtls/src/extension/extension_use_srtp/extension_use_srtp_test.rs (100%) rename {vendor => libs}/dtls/src/extension/mod.rs (100%) rename {vendor => libs}/dtls/src/extension/renegotiation_info.rs (100%) rename {vendor => libs}/dtls/src/extension/renegotiation_info/renegotiation_info_test.rs (100%) rename {vendor => libs}/dtls/src/flight/flight0.rs (100%) rename {vendor => libs}/dtls/src/flight/flight1.rs (100%) rename {vendor => libs}/dtls/src/flight/flight2.rs (100%) rename {vendor => libs}/dtls/src/flight/flight3.rs (100%) rename {vendor => libs}/dtls/src/flight/flight4.rs (100%) rename {vendor => libs}/dtls/src/flight/flight5.rs (100%) rename {vendor => libs}/dtls/src/flight/flight6.rs (100%) rename {vendor => libs}/dtls/src/flight/mod.rs (100%) rename {vendor => libs}/dtls/src/fragment_buffer/fragment_buffer_test.rs (100%) rename {vendor => libs}/dtls/src/fragment_buffer/mod.rs (100%) rename {vendor => libs}/dtls/src/handshake/handshake_cache.rs (100%) rename {vendor => libs}/dtls/src/handshake/handshake_cache/handshake_cache_test.rs (100%) rename {vendor => libs}/dtls/src/handshake/handshake_header.rs (100%) rename {vendor => libs}/dtls/src/handshake/handshake_message_certificate.rs (100%) rename {vendor => libs}/dtls/src/handshake/handshake_message_certificate/handshake_message_certificate_test.rs (100%) rename {vendor => libs}/dtls/src/handshake/handshake_message_certificate_request.rs (100%) rename {vendor => libs}/dtls/src/handshake/handshake_message_certificate_request/handshake_message_certificate_request_test.rs (100%) rename {vendor => libs}/dtls/src/handshake/handshake_message_certificate_verify.rs (100%) rename {vendor => libs}/dtls/src/handshake/handshake_message_certificate_verify/handshake_message_certificate_verify_test.rs (100%) rename {vendor => libs}/dtls/src/handshake/handshake_message_client_hello.rs (100%) rename {vendor => libs}/dtls/src/handshake/handshake_message_client_hello/handshake_message_client_hello_test.rs (100%) rename {vendor => libs}/dtls/src/handshake/handshake_message_client_key_exchange.rs (100%) rename {vendor => libs}/dtls/src/handshake/handshake_message_client_key_exchange/handshake_message_client_key_exchange_test.rs (100%) rename {vendor => libs}/dtls/src/handshake/handshake_message_finished.rs (100%) rename {vendor => libs}/dtls/src/handshake/handshake_message_finished/handshake_message_finished_test.rs (100%) rename {vendor => libs}/dtls/src/handshake/handshake_message_hello_verify_request.rs (100%) rename {vendor => libs}/dtls/src/handshake/handshake_message_hello_verify_request/handshake_message_hello_verify_request_test.rs (100%) rename {vendor => libs}/dtls/src/handshake/handshake_message_server_hello.rs (100%) rename {vendor => libs}/dtls/src/handshake/handshake_message_server_hello/handshake_message_server_hello_test.rs (100%) rename {vendor => libs}/dtls/src/handshake/handshake_message_server_hello_done.rs (100%) rename {vendor => libs}/dtls/src/handshake/handshake_message_server_hello_done/handshake_message_server_hello_done_test.rs (100%) rename {vendor => libs}/dtls/src/handshake/handshake_message_server_key_exchange.rs (100%) rename {vendor => libs}/dtls/src/handshake/handshake_message_server_key_exchange/handshake_message_server_key_exchange_test.rs (100%) rename {vendor => libs}/dtls/src/handshake/handshake_random.rs (100%) rename {vendor => libs}/dtls/src/handshake/handshake_test.rs (100%) rename {vendor => libs}/dtls/src/handshake/mod.rs (100%) rename {vendor => libs}/dtls/src/handshaker.rs (100%) rename {vendor => libs}/dtls/src/lib.rs (100%) rename {vendor => libs}/dtls/src/listener.rs (100%) rename {vendor => libs}/dtls/src/prf/mod.rs (100%) rename {vendor => libs}/dtls/src/prf/prf_test.rs (100%) rename {vendor => libs}/dtls/src/record_layer/mod.rs (100%) rename {vendor => libs}/dtls/src/record_layer/record_layer_header.rs (100%) rename {vendor => libs}/dtls/src/record_layer/record_layer_test.rs (100%) rename {vendor => libs}/dtls/src/signature_hash_algorithm/mod.rs (100%) rename {vendor => libs}/dtls/src/signature_hash_algorithm/signature_hash_algorithm_test.rs (100%) rename {vendor => libs}/dtls/src/state.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index f8b5e0bb..86e6f130 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -206,7 +206,7 @@ windows-sys = { version = "0.61", features = [ protobuf-codegen = "3.7" [patch.crates-io] -dtls = { path = "vendor/dtls" } +dtls = { path = "libs/dtls" } [profile.release] opt-level = 3 diff --git a/vendor/dtls/Cargo.toml b/libs/dtls/Cargo.toml similarity index 100% rename from vendor/dtls/Cargo.toml rename to libs/dtls/Cargo.toml diff --git a/vendor/dtls/LICENSE-APACHE b/libs/dtls/LICENSE-APACHE similarity index 100% rename from vendor/dtls/LICENSE-APACHE rename to libs/dtls/LICENSE-APACHE diff --git a/vendor/dtls/LICENSE-MIT b/libs/dtls/LICENSE-MIT similarity index 100% rename from vendor/dtls/LICENSE-MIT rename to libs/dtls/LICENSE-MIT diff --git a/vendor/dtls/README.md b/libs/dtls/README.md similarity index 92% rename from vendor/dtls/README.md rename to libs/dtls/README.md index daa8002c..628636e5 100644 --- a/vendor/dtls/README.md +++ b/libs/dtls/README.md @@ -16,5 +16,5 @@ by this backend. The upstream application-controlled `insecure_skip_verify` policy is preserved, including WebRTC's separate certificate fingerprint check. Authentication security level 2 rejects weak RSA keys and SHA-1 leaf signatures. -Run `cargo test --manifest-path vendor/dtls/Cargo.toml --lib` after updating this +Run `cargo test --manifest-path libs/dtls/Cargo.toml --lib` after updating this fork, and verify the application dependency tree contains no `rustls` engine. diff --git a/vendor/dtls/src/alert/alert_test.rs b/libs/dtls/src/alert/alert_test.rs similarity index 100% rename from vendor/dtls/src/alert/alert_test.rs rename to libs/dtls/src/alert/alert_test.rs diff --git a/vendor/dtls/src/alert/mod.rs b/libs/dtls/src/alert/mod.rs similarity index 100% rename from vendor/dtls/src/alert/mod.rs rename to libs/dtls/src/alert/mod.rs diff --git a/vendor/dtls/src/application_data.rs b/libs/dtls/src/application_data.rs similarity index 100% rename from vendor/dtls/src/application_data.rs rename to libs/dtls/src/application_data.rs diff --git a/vendor/dtls/src/certificate_verifier.rs b/libs/dtls/src/certificate_verifier.rs similarity index 100% rename from vendor/dtls/src/certificate_verifier.rs rename to libs/dtls/src/certificate_verifier.rs diff --git a/vendor/dtls/src/change_cipher_spec/change_cipher_spec_test.rs b/libs/dtls/src/change_cipher_spec/change_cipher_spec_test.rs similarity index 100% rename from vendor/dtls/src/change_cipher_spec/change_cipher_spec_test.rs rename to libs/dtls/src/change_cipher_spec/change_cipher_spec_test.rs diff --git a/vendor/dtls/src/change_cipher_spec/mod.rs b/libs/dtls/src/change_cipher_spec/mod.rs similarity index 100% rename from vendor/dtls/src/change_cipher_spec/mod.rs rename to libs/dtls/src/change_cipher_spec/mod.rs diff --git a/vendor/dtls/src/cipher_suite/cipher_suite_aes_128_ccm.rs b/libs/dtls/src/cipher_suite/cipher_suite_aes_128_ccm.rs similarity index 100% rename from vendor/dtls/src/cipher_suite/cipher_suite_aes_128_ccm.rs rename to libs/dtls/src/cipher_suite/cipher_suite_aes_128_ccm.rs diff --git a/vendor/dtls/src/cipher_suite/cipher_suite_aes_128_gcm_sha256.rs b/libs/dtls/src/cipher_suite/cipher_suite_aes_128_gcm_sha256.rs similarity index 100% rename from vendor/dtls/src/cipher_suite/cipher_suite_aes_128_gcm_sha256.rs rename to libs/dtls/src/cipher_suite/cipher_suite_aes_128_gcm_sha256.rs diff --git a/vendor/dtls/src/cipher_suite/cipher_suite_aes_256_cbc_sha.rs b/libs/dtls/src/cipher_suite/cipher_suite_aes_256_cbc_sha.rs similarity index 100% rename from vendor/dtls/src/cipher_suite/cipher_suite_aes_256_cbc_sha.rs rename to libs/dtls/src/cipher_suite/cipher_suite_aes_256_cbc_sha.rs diff --git a/vendor/dtls/src/cipher_suite/cipher_suite_chacha20_poly1305_sha256.rs b/libs/dtls/src/cipher_suite/cipher_suite_chacha20_poly1305_sha256.rs similarity index 100% rename from vendor/dtls/src/cipher_suite/cipher_suite_chacha20_poly1305_sha256.rs rename to libs/dtls/src/cipher_suite/cipher_suite_chacha20_poly1305_sha256.rs diff --git a/vendor/dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm.rs b/libs/dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm.rs similarity index 100% rename from vendor/dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm.rs rename to libs/dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm.rs diff --git a/vendor/dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8.rs b/libs/dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8.rs similarity index 100% rename from vendor/dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8.rs rename to libs/dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8.rs diff --git a/vendor/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm.rs b/libs/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm.rs similarity index 100% rename from vendor/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm.rs rename to libs/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm.rs diff --git a/vendor/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm8.rs b/libs/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm8.rs similarity index 100% rename from vendor/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm8.rs rename to libs/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm8.rs diff --git a/vendor/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_gcm_sha256.rs b/libs/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_gcm_sha256.rs similarity index 100% rename from vendor/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_gcm_sha256.rs rename to libs/dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_gcm_sha256.rs diff --git a/vendor/dtls/src/cipher_suite/mod.rs b/libs/dtls/src/cipher_suite/mod.rs similarity index 100% rename from vendor/dtls/src/cipher_suite/mod.rs rename to libs/dtls/src/cipher_suite/mod.rs diff --git a/vendor/dtls/src/client_certificate_type.rs b/libs/dtls/src/client_certificate_type.rs similarity index 100% rename from vendor/dtls/src/client_certificate_type.rs rename to libs/dtls/src/client_certificate_type.rs diff --git a/vendor/dtls/src/compression_methods.rs b/libs/dtls/src/compression_methods.rs similarity index 100% rename from vendor/dtls/src/compression_methods.rs rename to libs/dtls/src/compression_methods.rs diff --git a/vendor/dtls/src/config.rs b/libs/dtls/src/config.rs similarity index 100% rename from vendor/dtls/src/config.rs rename to libs/dtls/src/config.rs diff --git a/vendor/dtls/src/conn/conn_test.rs b/libs/dtls/src/conn/conn_test.rs similarity index 100% rename from vendor/dtls/src/conn/conn_test.rs rename to libs/dtls/src/conn/conn_test.rs diff --git a/vendor/dtls/src/conn/mod.rs b/libs/dtls/src/conn/mod.rs similarity index 100% rename from vendor/dtls/src/conn/mod.rs rename to libs/dtls/src/conn/mod.rs diff --git a/vendor/dtls/src/content.rs b/libs/dtls/src/content.rs similarity index 100% rename from vendor/dtls/src/content.rs rename to libs/dtls/src/content.rs diff --git a/vendor/dtls/src/crypto/crypto_cbc.rs b/libs/dtls/src/crypto/crypto_cbc.rs similarity index 100% rename from vendor/dtls/src/crypto/crypto_cbc.rs rename to libs/dtls/src/crypto/crypto_cbc.rs diff --git a/vendor/dtls/src/crypto/crypto_ccm.rs b/libs/dtls/src/crypto/crypto_ccm.rs similarity index 100% rename from vendor/dtls/src/crypto/crypto_ccm.rs rename to libs/dtls/src/crypto/crypto_ccm.rs diff --git a/vendor/dtls/src/crypto/crypto_chacha20.rs b/libs/dtls/src/crypto/crypto_chacha20.rs similarity index 100% rename from vendor/dtls/src/crypto/crypto_chacha20.rs rename to libs/dtls/src/crypto/crypto_chacha20.rs diff --git a/vendor/dtls/src/crypto/crypto_gcm.rs b/libs/dtls/src/crypto/crypto_gcm.rs similarity index 100% rename from vendor/dtls/src/crypto/crypto_gcm.rs rename to libs/dtls/src/crypto/crypto_gcm.rs diff --git a/vendor/dtls/src/crypto/crypto_test.rs b/libs/dtls/src/crypto/crypto_test.rs similarity index 100% rename from vendor/dtls/src/crypto/crypto_test.rs rename to libs/dtls/src/crypto/crypto_test.rs diff --git a/vendor/dtls/src/crypto/mod.rs b/libs/dtls/src/crypto/mod.rs similarity index 100% rename from vendor/dtls/src/crypto/mod.rs rename to libs/dtls/src/crypto/mod.rs diff --git a/vendor/dtls/src/crypto/padding.rs b/libs/dtls/src/crypto/padding.rs similarity index 100% rename from vendor/dtls/src/crypto/padding.rs rename to libs/dtls/src/crypto/padding.rs diff --git a/vendor/dtls/src/curve/mod.rs b/libs/dtls/src/curve/mod.rs similarity index 100% rename from vendor/dtls/src/curve/mod.rs rename to libs/dtls/src/curve/mod.rs diff --git a/vendor/dtls/src/curve/named_curve.rs b/libs/dtls/src/curve/named_curve.rs similarity index 100% rename from vendor/dtls/src/curve/named_curve.rs rename to libs/dtls/src/curve/named_curve.rs diff --git a/vendor/dtls/src/error.rs b/libs/dtls/src/error.rs similarity index 100% rename from vendor/dtls/src/error.rs rename to libs/dtls/src/error.rs diff --git a/vendor/dtls/src/extension/extension_server_name.rs b/libs/dtls/src/extension/extension_server_name.rs similarity index 100% rename from vendor/dtls/src/extension/extension_server_name.rs rename to libs/dtls/src/extension/extension_server_name.rs diff --git a/vendor/dtls/src/extension/extension_server_name/extension_server_name_test.rs b/libs/dtls/src/extension/extension_server_name/extension_server_name_test.rs similarity index 100% rename from vendor/dtls/src/extension/extension_server_name/extension_server_name_test.rs rename to libs/dtls/src/extension/extension_server_name/extension_server_name_test.rs diff --git a/vendor/dtls/src/extension/extension_supported_elliptic_curves.rs b/libs/dtls/src/extension/extension_supported_elliptic_curves.rs similarity index 100% rename from vendor/dtls/src/extension/extension_supported_elliptic_curves.rs rename to libs/dtls/src/extension/extension_supported_elliptic_curves.rs diff --git a/vendor/dtls/src/extension/extension_supported_elliptic_curves/extension_supported_elliptic_curves_test.rs b/libs/dtls/src/extension/extension_supported_elliptic_curves/extension_supported_elliptic_curves_test.rs similarity index 100% rename from vendor/dtls/src/extension/extension_supported_elliptic_curves/extension_supported_elliptic_curves_test.rs rename to libs/dtls/src/extension/extension_supported_elliptic_curves/extension_supported_elliptic_curves_test.rs diff --git a/vendor/dtls/src/extension/extension_supported_point_formats.rs b/libs/dtls/src/extension/extension_supported_point_formats.rs similarity index 100% rename from vendor/dtls/src/extension/extension_supported_point_formats.rs rename to libs/dtls/src/extension/extension_supported_point_formats.rs diff --git a/vendor/dtls/src/extension/extension_supported_point_formats/extension_supported_point_formats_test.rs b/libs/dtls/src/extension/extension_supported_point_formats/extension_supported_point_formats_test.rs similarity index 100% rename from vendor/dtls/src/extension/extension_supported_point_formats/extension_supported_point_formats_test.rs rename to libs/dtls/src/extension/extension_supported_point_formats/extension_supported_point_formats_test.rs diff --git a/vendor/dtls/src/extension/extension_supported_signature_algorithms.rs b/libs/dtls/src/extension/extension_supported_signature_algorithms.rs similarity index 100% rename from vendor/dtls/src/extension/extension_supported_signature_algorithms.rs rename to libs/dtls/src/extension/extension_supported_signature_algorithms.rs diff --git a/vendor/dtls/src/extension/extension_supported_signature_algorithms/extension_supported_signature_algorithms_test.rs b/libs/dtls/src/extension/extension_supported_signature_algorithms/extension_supported_signature_algorithms_test.rs similarity index 100% rename from vendor/dtls/src/extension/extension_supported_signature_algorithms/extension_supported_signature_algorithms_test.rs rename to libs/dtls/src/extension/extension_supported_signature_algorithms/extension_supported_signature_algorithms_test.rs diff --git a/vendor/dtls/src/extension/extension_use_extended_master_secret.rs b/libs/dtls/src/extension/extension_use_extended_master_secret.rs similarity index 100% rename from vendor/dtls/src/extension/extension_use_extended_master_secret.rs rename to libs/dtls/src/extension/extension_use_extended_master_secret.rs diff --git a/vendor/dtls/src/extension/extension_use_extended_master_secret/extension_use_extended_master_secret_test.rs b/libs/dtls/src/extension/extension_use_extended_master_secret/extension_use_extended_master_secret_test.rs similarity index 100% rename from vendor/dtls/src/extension/extension_use_extended_master_secret/extension_use_extended_master_secret_test.rs rename to libs/dtls/src/extension/extension_use_extended_master_secret/extension_use_extended_master_secret_test.rs diff --git a/vendor/dtls/src/extension/extension_use_srtp.rs b/libs/dtls/src/extension/extension_use_srtp.rs similarity index 100% rename from vendor/dtls/src/extension/extension_use_srtp.rs rename to libs/dtls/src/extension/extension_use_srtp.rs diff --git a/vendor/dtls/src/extension/extension_use_srtp/extension_use_srtp_test.rs b/libs/dtls/src/extension/extension_use_srtp/extension_use_srtp_test.rs similarity index 100% rename from vendor/dtls/src/extension/extension_use_srtp/extension_use_srtp_test.rs rename to libs/dtls/src/extension/extension_use_srtp/extension_use_srtp_test.rs diff --git a/vendor/dtls/src/extension/mod.rs b/libs/dtls/src/extension/mod.rs similarity index 100% rename from vendor/dtls/src/extension/mod.rs rename to libs/dtls/src/extension/mod.rs diff --git a/vendor/dtls/src/extension/renegotiation_info.rs b/libs/dtls/src/extension/renegotiation_info.rs similarity index 100% rename from vendor/dtls/src/extension/renegotiation_info.rs rename to libs/dtls/src/extension/renegotiation_info.rs diff --git a/vendor/dtls/src/extension/renegotiation_info/renegotiation_info_test.rs b/libs/dtls/src/extension/renegotiation_info/renegotiation_info_test.rs similarity index 100% rename from vendor/dtls/src/extension/renegotiation_info/renegotiation_info_test.rs rename to libs/dtls/src/extension/renegotiation_info/renegotiation_info_test.rs diff --git a/vendor/dtls/src/flight/flight0.rs b/libs/dtls/src/flight/flight0.rs similarity index 100% rename from vendor/dtls/src/flight/flight0.rs rename to libs/dtls/src/flight/flight0.rs diff --git a/vendor/dtls/src/flight/flight1.rs b/libs/dtls/src/flight/flight1.rs similarity index 100% rename from vendor/dtls/src/flight/flight1.rs rename to libs/dtls/src/flight/flight1.rs diff --git a/vendor/dtls/src/flight/flight2.rs b/libs/dtls/src/flight/flight2.rs similarity index 100% rename from vendor/dtls/src/flight/flight2.rs rename to libs/dtls/src/flight/flight2.rs diff --git a/vendor/dtls/src/flight/flight3.rs b/libs/dtls/src/flight/flight3.rs similarity index 100% rename from vendor/dtls/src/flight/flight3.rs rename to libs/dtls/src/flight/flight3.rs diff --git a/vendor/dtls/src/flight/flight4.rs b/libs/dtls/src/flight/flight4.rs similarity index 100% rename from vendor/dtls/src/flight/flight4.rs rename to libs/dtls/src/flight/flight4.rs diff --git a/vendor/dtls/src/flight/flight5.rs b/libs/dtls/src/flight/flight5.rs similarity index 100% rename from vendor/dtls/src/flight/flight5.rs rename to libs/dtls/src/flight/flight5.rs diff --git a/vendor/dtls/src/flight/flight6.rs b/libs/dtls/src/flight/flight6.rs similarity index 100% rename from vendor/dtls/src/flight/flight6.rs rename to libs/dtls/src/flight/flight6.rs diff --git a/vendor/dtls/src/flight/mod.rs b/libs/dtls/src/flight/mod.rs similarity index 100% rename from vendor/dtls/src/flight/mod.rs rename to libs/dtls/src/flight/mod.rs diff --git a/vendor/dtls/src/fragment_buffer/fragment_buffer_test.rs b/libs/dtls/src/fragment_buffer/fragment_buffer_test.rs similarity index 100% rename from vendor/dtls/src/fragment_buffer/fragment_buffer_test.rs rename to libs/dtls/src/fragment_buffer/fragment_buffer_test.rs diff --git a/vendor/dtls/src/fragment_buffer/mod.rs b/libs/dtls/src/fragment_buffer/mod.rs similarity index 100% rename from vendor/dtls/src/fragment_buffer/mod.rs rename to libs/dtls/src/fragment_buffer/mod.rs diff --git a/vendor/dtls/src/handshake/handshake_cache.rs b/libs/dtls/src/handshake/handshake_cache.rs similarity index 100% rename from vendor/dtls/src/handshake/handshake_cache.rs rename to libs/dtls/src/handshake/handshake_cache.rs diff --git a/vendor/dtls/src/handshake/handshake_cache/handshake_cache_test.rs b/libs/dtls/src/handshake/handshake_cache/handshake_cache_test.rs similarity index 100% rename from vendor/dtls/src/handshake/handshake_cache/handshake_cache_test.rs rename to libs/dtls/src/handshake/handshake_cache/handshake_cache_test.rs diff --git a/vendor/dtls/src/handshake/handshake_header.rs b/libs/dtls/src/handshake/handshake_header.rs similarity index 100% rename from vendor/dtls/src/handshake/handshake_header.rs rename to libs/dtls/src/handshake/handshake_header.rs diff --git a/vendor/dtls/src/handshake/handshake_message_certificate.rs b/libs/dtls/src/handshake/handshake_message_certificate.rs similarity index 100% rename from vendor/dtls/src/handshake/handshake_message_certificate.rs rename to libs/dtls/src/handshake/handshake_message_certificate.rs diff --git a/vendor/dtls/src/handshake/handshake_message_certificate/handshake_message_certificate_test.rs b/libs/dtls/src/handshake/handshake_message_certificate/handshake_message_certificate_test.rs similarity index 100% rename from vendor/dtls/src/handshake/handshake_message_certificate/handshake_message_certificate_test.rs rename to libs/dtls/src/handshake/handshake_message_certificate/handshake_message_certificate_test.rs diff --git a/vendor/dtls/src/handshake/handshake_message_certificate_request.rs b/libs/dtls/src/handshake/handshake_message_certificate_request.rs similarity index 100% rename from vendor/dtls/src/handshake/handshake_message_certificate_request.rs rename to libs/dtls/src/handshake/handshake_message_certificate_request.rs diff --git a/vendor/dtls/src/handshake/handshake_message_certificate_request/handshake_message_certificate_request_test.rs b/libs/dtls/src/handshake/handshake_message_certificate_request/handshake_message_certificate_request_test.rs similarity index 100% rename from vendor/dtls/src/handshake/handshake_message_certificate_request/handshake_message_certificate_request_test.rs rename to libs/dtls/src/handshake/handshake_message_certificate_request/handshake_message_certificate_request_test.rs diff --git a/vendor/dtls/src/handshake/handshake_message_certificate_verify.rs b/libs/dtls/src/handshake/handshake_message_certificate_verify.rs similarity index 100% rename from vendor/dtls/src/handshake/handshake_message_certificate_verify.rs rename to libs/dtls/src/handshake/handshake_message_certificate_verify.rs diff --git a/vendor/dtls/src/handshake/handshake_message_certificate_verify/handshake_message_certificate_verify_test.rs b/libs/dtls/src/handshake/handshake_message_certificate_verify/handshake_message_certificate_verify_test.rs similarity index 100% rename from vendor/dtls/src/handshake/handshake_message_certificate_verify/handshake_message_certificate_verify_test.rs rename to libs/dtls/src/handshake/handshake_message_certificate_verify/handshake_message_certificate_verify_test.rs diff --git a/vendor/dtls/src/handshake/handshake_message_client_hello.rs b/libs/dtls/src/handshake/handshake_message_client_hello.rs similarity index 100% rename from vendor/dtls/src/handshake/handshake_message_client_hello.rs rename to libs/dtls/src/handshake/handshake_message_client_hello.rs diff --git a/vendor/dtls/src/handshake/handshake_message_client_hello/handshake_message_client_hello_test.rs b/libs/dtls/src/handshake/handshake_message_client_hello/handshake_message_client_hello_test.rs similarity index 100% rename from vendor/dtls/src/handshake/handshake_message_client_hello/handshake_message_client_hello_test.rs rename to libs/dtls/src/handshake/handshake_message_client_hello/handshake_message_client_hello_test.rs diff --git a/vendor/dtls/src/handshake/handshake_message_client_key_exchange.rs b/libs/dtls/src/handshake/handshake_message_client_key_exchange.rs similarity index 100% rename from vendor/dtls/src/handshake/handshake_message_client_key_exchange.rs rename to libs/dtls/src/handshake/handshake_message_client_key_exchange.rs diff --git a/vendor/dtls/src/handshake/handshake_message_client_key_exchange/handshake_message_client_key_exchange_test.rs b/libs/dtls/src/handshake/handshake_message_client_key_exchange/handshake_message_client_key_exchange_test.rs similarity index 100% rename from vendor/dtls/src/handshake/handshake_message_client_key_exchange/handshake_message_client_key_exchange_test.rs rename to libs/dtls/src/handshake/handshake_message_client_key_exchange/handshake_message_client_key_exchange_test.rs diff --git a/vendor/dtls/src/handshake/handshake_message_finished.rs b/libs/dtls/src/handshake/handshake_message_finished.rs similarity index 100% rename from vendor/dtls/src/handshake/handshake_message_finished.rs rename to libs/dtls/src/handshake/handshake_message_finished.rs diff --git a/vendor/dtls/src/handshake/handshake_message_finished/handshake_message_finished_test.rs b/libs/dtls/src/handshake/handshake_message_finished/handshake_message_finished_test.rs similarity index 100% rename from vendor/dtls/src/handshake/handshake_message_finished/handshake_message_finished_test.rs rename to libs/dtls/src/handshake/handshake_message_finished/handshake_message_finished_test.rs diff --git a/vendor/dtls/src/handshake/handshake_message_hello_verify_request.rs b/libs/dtls/src/handshake/handshake_message_hello_verify_request.rs similarity index 100% rename from vendor/dtls/src/handshake/handshake_message_hello_verify_request.rs rename to libs/dtls/src/handshake/handshake_message_hello_verify_request.rs diff --git a/vendor/dtls/src/handshake/handshake_message_hello_verify_request/handshake_message_hello_verify_request_test.rs b/libs/dtls/src/handshake/handshake_message_hello_verify_request/handshake_message_hello_verify_request_test.rs similarity index 100% rename from vendor/dtls/src/handshake/handshake_message_hello_verify_request/handshake_message_hello_verify_request_test.rs rename to libs/dtls/src/handshake/handshake_message_hello_verify_request/handshake_message_hello_verify_request_test.rs diff --git a/vendor/dtls/src/handshake/handshake_message_server_hello.rs b/libs/dtls/src/handshake/handshake_message_server_hello.rs similarity index 100% rename from vendor/dtls/src/handshake/handshake_message_server_hello.rs rename to libs/dtls/src/handshake/handshake_message_server_hello.rs diff --git a/vendor/dtls/src/handshake/handshake_message_server_hello/handshake_message_server_hello_test.rs b/libs/dtls/src/handshake/handshake_message_server_hello/handshake_message_server_hello_test.rs similarity index 100% rename from vendor/dtls/src/handshake/handshake_message_server_hello/handshake_message_server_hello_test.rs rename to libs/dtls/src/handshake/handshake_message_server_hello/handshake_message_server_hello_test.rs diff --git a/vendor/dtls/src/handshake/handshake_message_server_hello_done.rs b/libs/dtls/src/handshake/handshake_message_server_hello_done.rs similarity index 100% rename from vendor/dtls/src/handshake/handshake_message_server_hello_done.rs rename to libs/dtls/src/handshake/handshake_message_server_hello_done.rs diff --git a/vendor/dtls/src/handshake/handshake_message_server_hello_done/handshake_message_server_hello_done_test.rs b/libs/dtls/src/handshake/handshake_message_server_hello_done/handshake_message_server_hello_done_test.rs similarity index 100% rename from vendor/dtls/src/handshake/handshake_message_server_hello_done/handshake_message_server_hello_done_test.rs rename to libs/dtls/src/handshake/handshake_message_server_hello_done/handshake_message_server_hello_done_test.rs diff --git a/vendor/dtls/src/handshake/handshake_message_server_key_exchange.rs b/libs/dtls/src/handshake/handshake_message_server_key_exchange.rs similarity index 100% rename from vendor/dtls/src/handshake/handshake_message_server_key_exchange.rs rename to libs/dtls/src/handshake/handshake_message_server_key_exchange.rs diff --git a/vendor/dtls/src/handshake/handshake_message_server_key_exchange/handshake_message_server_key_exchange_test.rs b/libs/dtls/src/handshake/handshake_message_server_key_exchange/handshake_message_server_key_exchange_test.rs similarity index 100% rename from vendor/dtls/src/handshake/handshake_message_server_key_exchange/handshake_message_server_key_exchange_test.rs rename to libs/dtls/src/handshake/handshake_message_server_key_exchange/handshake_message_server_key_exchange_test.rs diff --git a/vendor/dtls/src/handshake/handshake_random.rs b/libs/dtls/src/handshake/handshake_random.rs similarity index 100% rename from vendor/dtls/src/handshake/handshake_random.rs rename to libs/dtls/src/handshake/handshake_random.rs diff --git a/vendor/dtls/src/handshake/handshake_test.rs b/libs/dtls/src/handshake/handshake_test.rs similarity index 100% rename from vendor/dtls/src/handshake/handshake_test.rs rename to libs/dtls/src/handshake/handshake_test.rs diff --git a/vendor/dtls/src/handshake/mod.rs b/libs/dtls/src/handshake/mod.rs similarity index 100% rename from vendor/dtls/src/handshake/mod.rs rename to libs/dtls/src/handshake/mod.rs diff --git a/vendor/dtls/src/handshaker.rs b/libs/dtls/src/handshaker.rs similarity index 100% rename from vendor/dtls/src/handshaker.rs rename to libs/dtls/src/handshaker.rs diff --git a/vendor/dtls/src/lib.rs b/libs/dtls/src/lib.rs similarity index 100% rename from vendor/dtls/src/lib.rs rename to libs/dtls/src/lib.rs diff --git a/vendor/dtls/src/listener.rs b/libs/dtls/src/listener.rs similarity index 100% rename from vendor/dtls/src/listener.rs rename to libs/dtls/src/listener.rs diff --git a/vendor/dtls/src/prf/mod.rs b/libs/dtls/src/prf/mod.rs similarity index 100% rename from vendor/dtls/src/prf/mod.rs rename to libs/dtls/src/prf/mod.rs diff --git a/vendor/dtls/src/prf/prf_test.rs b/libs/dtls/src/prf/prf_test.rs similarity index 100% rename from vendor/dtls/src/prf/prf_test.rs rename to libs/dtls/src/prf/prf_test.rs diff --git a/vendor/dtls/src/record_layer/mod.rs b/libs/dtls/src/record_layer/mod.rs similarity index 100% rename from vendor/dtls/src/record_layer/mod.rs rename to libs/dtls/src/record_layer/mod.rs diff --git a/vendor/dtls/src/record_layer/record_layer_header.rs b/libs/dtls/src/record_layer/record_layer_header.rs similarity index 100% rename from vendor/dtls/src/record_layer/record_layer_header.rs rename to libs/dtls/src/record_layer/record_layer_header.rs diff --git a/vendor/dtls/src/record_layer/record_layer_test.rs b/libs/dtls/src/record_layer/record_layer_test.rs similarity index 100% rename from vendor/dtls/src/record_layer/record_layer_test.rs rename to libs/dtls/src/record_layer/record_layer_test.rs diff --git a/vendor/dtls/src/signature_hash_algorithm/mod.rs b/libs/dtls/src/signature_hash_algorithm/mod.rs similarity index 100% rename from vendor/dtls/src/signature_hash_algorithm/mod.rs rename to libs/dtls/src/signature_hash_algorithm/mod.rs diff --git a/vendor/dtls/src/signature_hash_algorithm/signature_hash_algorithm_test.rs b/libs/dtls/src/signature_hash_algorithm/signature_hash_algorithm_test.rs similarity index 100% rename from vendor/dtls/src/signature_hash_algorithm/signature_hash_algorithm_test.rs rename to libs/dtls/src/signature_hash_algorithm/signature_hash_algorithm_test.rs diff --git a/vendor/dtls/src/state.rs b/libs/dtls/src/state.rs similarity index 100% rename from vendor/dtls/src/state.rs rename to libs/dtls/src/state.rs From 3b8a4a6f34baa80e7a5cad87df3036530a4a6dd4 Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Thu, 1 Oct 2026 22:50:37 +0800 Subject: [PATCH 08/11] =?UTF-8?q?perf(video):=20=E5=87=8F=E5=B0=91?= =?UTF-8?q?=E9=97=B2=E7=BD=AE=E7=BC=93=E5=86=B2=E5=B9=B6=E5=AE=B9=E5=BF=8D?= =?UTF-8?q?=E7=9F=AD=E6=97=B6=E8=BE=93=E5=87=BA=E8=83=8C=E5=8E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/video/capture/linux.rs | 37 ++++++++++++++++++++++++------------ src/video/codec/convert.rs | 17 +++++++++++++---- src/video/pipeline/shared.rs | 33 ++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 16 deletions(-) diff --git a/src/video/capture/linux.rs b/src/video/capture/linux.rs index ad6e7d5b..4135af44 100644 --- a/src/video/capture/linux.rs +++ b/src/video/capture/linux.rs @@ -42,6 +42,12 @@ pub struct CaptureMeta { pub sequence: u64, } +fn append_capture_plane(dst: &mut Vec, mapping: &[u8], offset: usize, bytes: usize) { + let start = offset.min(mapping.len()); + let end = offset.saturating_add(bytes).min(mapping.len()); + dst.extend_from_slice(&mapping[start..end]); +} + /// When set, DV ioctls use the subdev (rkcif: video node has no DV ioctls). #[derive(Debug, Clone, Default)] pub struct BridgeContext { @@ -629,12 +635,12 @@ impl CaptureStream { } let mapping = &self.mappings[index][plane_idx]; let start = data_offset.min(mapping.len()); - let end = (data_offset + bytes_used).min(mapping.len()); + let end = data_offset.saturating_add(bytes_used).min(mapping.len()); total += end.saturating_sub(start); } - dst.resize(total, 0); - let mut cursor = 0usize; + dst.clear(); + dst.reserve(total); for (plane_idx, plane) in dqbuf.planes_iter().enumerate() { let bytes_used = *plane.bytesused as usize; let data_offset = plane.data_offset.copied().unwrap_or(0) as usize; @@ -642,14 +648,7 @@ impl CaptureStream { continue; } let mapping = &self.mappings[index][plane_idx]; - let start = data_offset.min(mapping.len()); - let end = (data_offset + bytes_used).min(mapping.len()); - let len = end.saturating_sub(start); - if len == 0 { - continue; - } - dst[cursor..cursor + len].copy_from_slice(&mapping[start..end]); - cursor += len; + append_capture_plane(dst, mapping, data_offset, bytes_used); } self.queue_buffer(index as u32) @@ -1153,9 +1152,23 @@ fn set_fps(fd: &File, queue: QueueType, fps: u32) -> std::result::Result<(), ioc #[cfg(test)] mod tests { - use super::{open_capture_device, DvTimingsSignature, NativeHdmirxState}; + use super::{append_capture_plane, open_capture_device, DvTimingsSignature, NativeHdmirxState}; use crate::video::format::PixelFormat; + #[test] + fn capture_planes_append_without_stale_bytes_or_reallocation() { + let mut data = vec![99; 64]; + let pointer = data.as_ptr(); + data.clear(); + append_capture_plane(&mut data, &[1, 2, 3, 4], 1, 2); + append_capture_plane(&mut data, &[5, 6, 7], 0, 10); + assert_eq!(data, [2, 3, 5, 6, 7]); + assert_eq!(data.as_ptr(), pointer); + append_capture_plane(&mut data, &[8], usize::MAX, usize::MAX); + append_capture_plane(&mut data, &[8], 0, 0); + assert_eq!(data, [2, 3, 5, 6, 7]); + } + fn timing() -> DvTimingsSignature { DvTimingsSignature { width: 1920, diff --git a/src/video/codec/convert.rs b/src/video/codec/convert.rs index 305edd7e..2107c4fd 100644 --- a/src/video/codec/convert.rs +++ b/src/video/codec/convert.rs @@ -544,7 +544,7 @@ pub struct Nv12Converter { /// MJPEG decoder that writes NV12 directly using libyuv. pub struct MjpegToNv12Decoder { resolution: Resolution, - output_buffer: Nv12Buffer, + output_buffer: Option, size_checked: bool, } @@ -552,7 +552,7 @@ impl MjpegToNv12Decoder { pub fn new(resolution: Resolution) -> Self { Self { resolution, - output_buffer: Nv12Buffer::new(resolution), + output_buffer: None, size_checked: false, } } @@ -561,11 +561,14 @@ impl MjpegToNv12Decoder { self.check_size(input)?; let width = self.resolution.width as i32; let height = self.resolution.height as i32; + let output = self + .output_buffer + .get_or_insert_with(|| Nv12Buffer::new(self.resolution)); - libyuv::mjpg_to_nv12(input, self.output_buffer.as_bytes_mut(), width, height) + libyuv::mjpg_to_nv12(input, output.as_bytes_mut(), width, height) .map_err(|e| AppError::VideoError(format!("libyuv MJPEG->NV12 failed: {}", e)))?; - Ok(self.output_buffer.as_bytes()) + Ok(output.as_bytes()) } /// Decode into caller-owned storage so capture and encoding can run on @@ -899,6 +902,7 @@ mod tests { let mut output = Vec::with_capacity(output_size); let allocation = output.as_ptr(); let mut decoder = MjpegToNv12Decoder::new(resolution); + assert!(decoder.output_buffer.is_none()); decoder.decode_into(&jpeg, &mut output).unwrap(); assert_eq!(output.len(), output_size); @@ -907,5 +911,10 @@ mod tests { decoder.decode_into(&jpeg, &mut output).unwrap(); assert_eq!(output.len(), output_size); assert_eq!(output.as_ptr(), allocation); + + assert!(decoder.output_buffer.is_none()); + assert_eq!(decoder.decode(&jpeg).unwrap(), output.as_slice()); + let internal = decoder.decode(&jpeg).unwrap().as_ptr(); + assert_eq!(decoder.decode(&jpeg).unwrap().as_ptr(), internal); } } diff --git a/src/video/pipeline/shared.rs b/src/video/pipeline/shared.rs index 92cd33c4..25a10fc9 100644 --- a/src/video/pipeline/shared.rs +++ b/src/video/pipeline/shared.rs @@ -157,6 +157,11 @@ use crate::video::recovery::{wait_for_source_change, CaptureRecoveryPolicy}; use crate::video::signal::SignalStatus; const MIN_CAPTURE_FRAME_SIZE: usize = 128; +const OUTPUT_BACKPRESSURE_GRACE_NS: u64 = 10_000_000; + +fn sustained_output_backpressure(started_ns: u64, now_ns: u64) -> bool { + started_ns != 0 && now_ns.saturating_sub(started_ns - 1) >= OUTPUT_BACKPRESSURE_GRACE_NS +} type VideoFrameMailbox = FrameMailbox>; fn spawn_video_worker( @@ -637,6 +642,7 @@ pub struct SharedVideoPipeline { encoder_output_wait_ns: AtomicU64, output_backpressure_events: AtomicU64, output_backpressured: AtomicBool, + output_backpressure_since_ns: AtomicU64, encoded_frames_skipped_for_slow_subscribers: AtomicU64, delivery_stats: Arc, running: watch::Sender, @@ -704,6 +710,7 @@ impl SharedVideoPipeline { encoder_output_wait_ns: AtomicU64::new(0), output_backpressure_events: AtomicU64::new(0), output_backpressured: AtomicBool::new(false), + output_backpressure_since_ns: AtomicU64::new(0), encoded_frames_skipped_for_slow_subscribers: AtomicU64::new(0), delivery_stats: Arc::new(DeliveryStats::default()), running: running_tx, @@ -911,6 +918,13 @@ impl SharedVideoPipeline { stage: MjpegAdmissionStage, ) -> bool { let backpressured = self.output_backpressured.load(Ordering::Acquire); + if backpressured && policy == MjpegAdmissionPolicy::BoundedPrefetch { + let started = self.output_backpressure_since_ns.load(Ordering::Acquire); + let now = PROCESS_START.get_or_init(Instant::now).elapsed().as_nanos() as u64; + if !sustained_output_backpressure(started, now) { + return false; + } + } if !policy.check_pending(backpressured) { return false; } @@ -1081,6 +1095,11 @@ impl SharedVideoPipeline { } } if active_subscribers > 0 && reservations.is_empty() { + if !self.output_backpressured.load(Ordering::Acquire) { + let now = PROCESS_START.get_or_init(Instant::now).elapsed().as_nanos() as u64; + self.output_backpressure_since_ns + .store(now.saturating_add(1), Ordering::Release); + } if !self.output_backpressured.swap(true, Ordering::AcqRel) { self.output_backpressure_events .fetch_add(1, Ordering::Relaxed); @@ -1088,6 +1107,8 @@ impl SharedVideoPipeline { None } else { self.output_backpressured.store(false, Ordering::Release); + self.output_backpressure_since_ns + .store(0, Ordering::Release); Some(reservations) } } @@ -1302,6 +1323,8 @@ impl SharedVideoPipeline { self.encoder_output_wait_ns.store(0, Ordering::Relaxed); self.output_backpressure_events.store(0, Ordering::Relaxed); self.output_backpressured.store(false, Ordering::Release); + self.output_backpressure_since_ns + .store(0, Ordering::Release); self.encoded_frames_skipped_for_slow_subscribers .store(0, Ordering::Relaxed); self.delivery_stats.sent_frames.store(0, Ordering::Relaxed); @@ -2538,6 +2561,15 @@ mod tests { assert_eq!(stats.decoded_frames_not_encoded, 5); } + #[test] + fn brief_output_backpressure_keeps_bounded_prefetch_running() { + assert!(!sustained_output_backpressure(0, 100_000_000)); + assert!(!sustained_output_backpressure(1, 9_999_999)); + assert!(sustained_output_backpressure(1, 10_000_000)); + assert!(sustained_output_backpressure(1, 100_000_000)); + assert!(!sustained_output_backpressure(20_000_001, 10_000_000)); + } + #[test] fn mjpeg_drop_reasons_sum_to_total_without_double_counting() { let pipeline = delivery_test_pipeline(); @@ -2606,6 +2638,7 @@ mod tests { } mailbox.publish(1, delivery_test_raw_frame(1)); for stage in [MjpegAdmissionStage::Capture, MjpegAdmissionStage::Decode] { + std::thread::sleep(Duration::from_millis(11)); assert!(pipeline.drop_pending_mjpeg( &mailbox, MjpegAdmissionPolicy::BoundedPrefetch, From 9a1f4ff4b541cde156d6a70e81dbdd4b7aca9c28 Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Thu, 1 Oct 2026 22:55:57 +0800 Subject: [PATCH 09/11] =?UTF-8?q?perf(audio,hid):=20=E5=87=8F=E5=B0=91=20P?= =?UTF-8?q?CM=20=E5=A4=8D=E5=88=B6=E5=92=8C=E9=BC=A0=E6=A0=87=E7=83=AD?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E5=88=86=E9=85=8D=E4=B8=8E=E8=BD=AE=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/audio/capture_linux.rs | 10 +-- src/audio/capture_windows.rs | 10 +-- src/audio/streamer.rs | 92 +++++++++++++++++++-- src/hid/ch9329.rs | 2 +- src/hid/macos_drag.rs | 46 +++++++++-- src/hid/otg.rs | 2 +- src/hid/otg_device.rs | 155 ++++++++++++++++++++++++++++++----- 7 files changed, 270 insertions(+), 47 deletions(-) diff --git a/src/audio/capture_linux.rs b/src/audio/capture_linux.rs index af11b15a..7db1b518 100644 --- a/src/audio/capture_linux.rs +++ b/src/audio/capture_linux.rs @@ -79,12 +79,12 @@ pub(super) fn run_capture( Ok(frames_read) => { consecutive_errors = 0; let byte_count = frames_read * config.channels as usize * 2; - let frame = AudioFrame::new_interleaved( - Bytes::copy_from_slice(&buffer[..byte_count]), - config.channels, - config.sample_rate, - ); if frame_tx.receiver_count() > 0 { + let frame = AudioFrame::new_interleaved( + Bytes::copy_from_slice(&buffer[..byte_count]), + config.channels, + config.sample_rate, + ); let _ = frame_tx.send(frame); } } diff --git a/src/audio/capture_windows.rs b/src/audio/capture_windows.rs index 260d6314..db613ee2 100644 --- a/src/audio/capture_windows.rs +++ b/src/audio/capture_windows.rs @@ -97,12 +97,12 @@ pub(super) fn run_capture( if samples.is_empty() { continue; } - let frame = AudioFrame::new_interleaved( - Bytes::copy_from_slice(bytemuck::cast_slice(&samples)), - 2, - 48_000, - ); if frame_tx.receiver_count() > 0 { + let frame = AudioFrame::new_interleaved( + Bytes::copy_from_slice(bytemuck::cast_slice(&samples)), + 2, + 48_000, + ); if let Err(e) = frame_tx.send(frame) { debug!("No audio receivers: {}", e); } diff --git a/src/audio/streamer.rs b/src/audio/streamer.rs index c44f69c8..ca402a3c 100644 --- a/src/audio/streamer.rs +++ b/src/audio/streamer.rs @@ -15,6 +15,39 @@ use crate::error::{AppError, Result}; /// 48 kHz stereo: 20 ms = 960 × 2 samples (S16LE). const OPUS_STEREO_SAMPLES: usize = 960 * 2; +struct PcmFrames<'audio> { + pending: &'audio mut Vec, + samples: &'audio [i16], +} + +impl<'audio> PcmFrames<'audio> { + fn new(pending: &'audio mut Vec, samples: &'audio [i16]) -> Self { + Self { pending, samples } + } + + fn next_frame(&mut self) -> Option<&[i16]> { + if self.pending.len() == OPUS_STEREO_SAMPLES { + self.pending.clear(); + } + if self.pending.is_empty() && self.samples.len() >= OPUS_STEREO_SAMPLES { + let (frame, remaining) = self.samples.split_at(OPUS_STEREO_SAMPLES); + self.samples = remaining; + return Some(frame); + } + if self.samples.is_empty() { + return None; + } + let count = (OPUS_STEREO_SAMPLES - self.pending.len()).min(self.samples.len()); + if self.pending.capacity() < OPUS_STEREO_SAMPLES { + self.pending + .reserve_exact(OPUS_STEREO_SAMPLES - self.pending.len()); + } + self.pending.extend_from_slice(&self.samples[..count]); + self.samples = &self.samples[count..]; + (self.pending.len() == OPUS_STEREO_SAMPLES).then(|| self.pending.as_slice()) + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum AudioStreamState { #[default] @@ -331,18 +364,12 @@ impl AudioStreamer { continue; } }; - if !samples.is_empty() { - pending.extend_from_slice(samples); - } - - while pending.len() >= OPUS_STEREO_SAMPLES { + let mut frames = PcmFrames::new(&mut pending, samples); + while let Some(samples) = frames.next_frame() { let opus_result = { let mut enc_guard = encoder.lock().await; - (*enc_guard) - .as_mut() - .map(|enc| enc.encode(&pending[..OPUS_STEREO_SAMPLES])) + (*enc_guard).as_mut().map(|enc| enc.encode(samples)) }; - pending.drain(..OPUS_STEREO_SAMPLES); match opus_result { Some(Ok(opus_frame)) => { @@ -396,6 +423,53 @@ mod tests { use super::*; use bytes::Bytes; + #[test] + fn aligned_pcm_frames_borrow_input_without_allocating() { + let samples = vec![123; OPUS_STEREO_SAMPLES * 2]; + let mut pending = Vec::new(); + let mut frames = PcmFrames::new(&mut pending, &samples); + assert_eq!(frames.next_frame().unwrap().as_ptr(), samples.as_ptr()); + assert_eq!( + frames.next_frame().unwrap().as_ptr(), + samples[OPUS_STEREO_SAMPLES..].as_ptr() + ); + assert!(frames.next_frame().is_none()); + assert!(pending.is_empty()); + assert_eq!(pending.capacity(), 0); + } + + #[test] + fn fragmented_pcm_preserves_samples_and_opus_packets() { + let samples: Vec = (0..OPUS_STEREO_SAMPLES * 8 + 31) + .map(|index| ((index * 73 % 65536) as i32 - 32768) as i16) + .collect(); + let mut pending = Vec::new(); + let mut actual = Vec::new(); + let mut encoder = OpusEncoder::new(OpusConfig::default()).unwrap(); + let mut reference_encoder = OpusEncoder::new(OpusConfig::default()).unwrap(); + let mut offset = 0; + for size in [1, 317, 4601, 2000, 11, 8470, samples.len()] { + let end = (offset + size).min(samples.len()); + let mut frames = PcmFrames::new(&mut pending, &samples[offset..end]); + while let Some(frame) = frames.next_frame() { + actual.extend_from_slice(frame); + let encoded = encoder.encode(frame).unwrap(); + let start = actual.len() - OPUS_STEREO_SAMPLES; + let reference = reference_encoder + .encode(&samples[start..actual.len()]) + .unwrap(); + assert_eq!(encoded.data, reference.data); + assert_eq!(encoded.sequence, reference.sequence); + assert_eq!(encoded.duration_ms, reference.duration_ms); + } + assert!(pending.len() < OPUS_STEREO_SAMPLES); + assert!(pending.capacity() <= OPUS_STEREO_SAMPLES); + offset = end; + } + assert_eq!(actual, samples[..OPUS_STEREO_SAMPLES * 8]); + assert_eq!(pending, samples[OPUS_STEREO_SAMPLES * 8..]); + } + #[test] fn test_streamer_config_default() { let config = AudioStreamerConfig::default(); diff --git a/src/hid/ch9329.rs b/src/hid/ch9329.rs index b213bcc4..54314930 100644 --- a/src/hid/ch9329.rs +++ b/src/hid/ch9329.rs @@ -1291,7 +1291,7 @@ impl HidBackend for Ch9329Backend { let mut state = self.macos_drag_state.lock(); let (buttons, reports) = state.plan(event, buttons, *self.screen_resolution.read()); self.mouse_buttons.store(buttons, Ordering::Relaxed); - for report in reports { + for report in reports.iter().copied() { match report { MouseReport::Absolute { buttons, x, y } => { let x = (u32::from(x) * CH9329_MOUSE_RESOLUTION / 32768) as u16; diff --git a/src/hid/macos_drag.rs b/src/hid/macos_drag.rs index ce97a1cc..d9efc37c 100644 --- a/src/hid/macos_drag.rs +++ b/src/hid/macos_drag.rs @@ -3,7 +3,7 @@ use super::{MouseEvent, MouseEventType}; -#[derive(Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub(super) enum MouseReport { Absolute { buttons: u8, @@ -25,6 +25,7 @@ pub(super) struct MacosDrag { extent: (u32, u32), relative_input: bool, absolute_drag: bool, + reports: Vec, } impl MacosDrag { @@ -37,8 +38,13 @@ impl MacosDrag { event: MouseEvent, buttons: u8, extent: (u32, u32), - ) -> (u8, Vec) { - let mut reports = Vec::new(); + ) -> (u8, &[MouseReport]) { + if self.reports.capacity() > 128 { + self.reports = Vec::new(); + } else { + self.reports.clear(); + } + let reports = &mut self.reports; let mut next_buttons = buttons; if extent != self.extent || buttons == 0 { self.remainder = (0, 0); @@ -56,7 +62,7 @@ impl MacosDrag { Self::delta(position.0, self.position.0, extent.0, &mut self.remainder.0); let dy = Self::delta(position.1, self.position.1, extent.1, &mut self.remainder.1); - Self::motion(&mut reports, buttons, dx, dy); + Self::motion(reports, buttons, dx, dy); } else { reports.push(MouseReport::Absolute { buttons: 0, @@ -69,7 +75,7 @@ impl MacosDrag { MouseEventType::Move => { self.relative_input = true; Self::motion( - &mut reports, + reports, buttons, i64::from(event.x.clamp(-127, 127)), i64::from(event.y.clamp(-127, 127)), @@ -127,7 +133,7 @@ impl MacosDrag { }); } } - (next_buttons, reports) + (next_buttons, reports.as_slice()) } fn delta(current: u16, previous: u16, extent: u32, remainder: &mut i64) -> i64 { @@ -160,13 +166,39 @@ mod tests { use super::*; use crate::hid::MouseButton; + #[test] + fn repeated_motion_reuses_report_storage() { + let mut state = MacosDrag::default(); + let (_, reports) = state.plan(MouseEvent::move_rel(1, 1), 0, (1920, 1080)); + let allocation = reports.as_ptr(); + for _ in 0..1000 { + let (_, reports) = state.plan(MouseEvent::move_rel(1, 1), 0, (1920, 1080)); + assert_eq!(reports.as_ptr(), allocation); + assert_eq!(reports.len(), 1); + } + } + + #[test] + fn oversized_report_storage_is_not_retained_for_small_events() { + let mut state = MacosDrag::default(); + state.reports = Vec::with_capacity(256); + assert!(state.reports.capacity() > 128); + state.plan(MouseEvent::move_rel(1, 1), 0, (1920, 1080)); + assert!(state.reports.capacity() <= 128); + } + fn drag(steps: &[i32], extent: (u32, u32)) -> (i64, i64) { let mut state = MacosDrag::default(); state.plan(MouseEvent::move_abs(8000, 8000), 0, extent); state.plan(MouseEvent::button_down(MouseButton::Left), 0, extent); let mut total = (0, 0); for &x in steps { - for report in state.plan(MouseEvent::move_abs(x, x), 1, extent).1 { + for report in state + .plan(MouseEvent::move_abs(x, x), 1, extent) + .1 + .iter() + .copied() + { if let MouseReport::Relative { dx, dy, .. } = report { total.0 += i64::from(dx); total.1 += i64::from(dy); diff --git a/src/hid/otg.rs b/src/hid/otg.rs index c122e2eb..aa4b84b7 100644 --- a/src/hid/otg.rs +++ b/src/hid/otg.rs @@ -871,7 +871,7 @@ impl HidBackend for OtgBackend { let extent = self.screen_resolution.read().unwrap_or((1920, 1080)); let (buttons, reports) = state.plan(event, buttons, extent); self.mouse_buttons.store(buttons, Ordering::Relaxed); - for report in reports { + for report in reports.iter().copied() { match report { MouseReport::Absolute { buttons, x, y } => { self.send_mouse_report_absolute(buttons, x, y, 0)? diff --git a/src/hid/otg_device.rs b/src/hid/otg_device.rs index 9d47a689..3ac40abc 100644 --- a/src/hid/otg_device.rs +++ b/src/hid/otg_device.rs @@ -4,6 +4,8 @@ use std::fs::File; use std::io::Write; #[cfg(unix)] use std::os::unix::io::AsFd; +#[cfg(unix)] +use std::time::{Duration, Instant}; #[cfg(unix)] use nix::poll::{poll, PollFd, PollFlags, PollTimeout}; @@ -20,27 +22,142 @@ impl OtgDeviceIo { data: &[u8], timeout_ms: i32, ) -> std::io::Result { - let mut pollfd = [PollFd::new(file.as_fd(), PollFlags::POLLOUT)]; - match poll(&mut pollfd, PollTimeout::from(timeout_ms as u16)) { - Ok(1) => { - if let Some(revents) = pollfd[0].revents() { - if revents.contains(PollFlags::POLLERR) || revents.contains(PollFlags::POLLHUP) - { - return Err(std::io::Error::new( - std::io::ErrorKind::BrokenPipe, - "Device error or hangup", - )); + write_with_wait(file, data, timeout_ms as u16, |file, timeout| { + let mut pollfd = [PollFd::new(file.as_fd(), PollFlags::POLLOUT)]; + match poll(&mut pollfd, PollTimeout::from(timeout)) { + Ok(1) => { + if let Some(revents) = pollfd[0].revents() { + if revents.contains(PollFlags::POLLERR) + || revents.contains(PollFlags::POLLHUP) + { + return Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "Device error or hangup", + )); + } } + Ok(true) } - file.write_all(data)?; - Ok(true) + Ok(0) => { + trace!("HID write timeout, dropping data"); + Ok(false) + } + Ok(_) => Ok(false), + Err(e) => Err(std::io::Error::other(e)), } - Ok(0) => { - trace!("HID write timeout, dropping data"); - Ok(false) - } - Ok(_) => Ok(false), - Err(e) => Err(std::io::Error::other(e)), - } + }) + } +} + +#[cfg(unix)] +fn write_with_wait( + writer: &mut Writer, + mut data: &[u8], + timeout_ms: u16, + mut wait: impl FnMut(&Writer, u16) -> std::io::Result, +) -> std::io::Result { + let mut deadline = None; + while !data.is_empty() { + match writer.write(data) { + Ok(0) => return Err(std::io::ErrorKind::WriteZero.into()), + Ok(written) => data = &data[written..], + Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue, + Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { + let deadline = deadline.get_or_insert_with(|| { + Instant::now() + Duration::from_millis(u64::from(timeout_ms)) + }); + let remaining = deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() + || !wait( + writer, + remaining + .as_nanos() + .div_ceil(1_000_000) + .min(u16::MAX as u128) as u16, + )? + { + return Ok(false); + } + } + Err(error) => return Err(error), + } + } + Ok(true) +} + +#[cfg(all(test, unix))] +mod tests { + use super::*; + use std::collections::VecDeque; + use std::io::{ErrorKind, Result}; + + struct ScriptedWriter { + steps: VecDeque>, + data: Vec, + } + + impl Write for ScriptedWriter { + fn write(&mut self, input: &[u8]) -> Result { + let count = self.steps.pop_front().unwrap_or(Ok(input.len()))?; + self.data.extend_from_slice(&input[..count]); + Ok(count) + } + + fn flush(&mut self) -> Result<()> { + Ok(()) + } + } + + #[test] + fn ready_device_skips_poll_and_preserves_report_bytes() { + let mut writer = ScriptedWriter { + steps: VecDeque::new(), + data: Vec::new(), + }; + assert!( + write_with_wait(&mut writer, &[1, 2, 3, 4], 100, |_, _| panic!( + "unexpected poll" + )) + .unwrap() + ); + assert_eq!(writer.data, [1, 2, 3, 4]); + } + + #[test] + fn partial_and_interrupted_writes_do_not_duplicate_bytes() { + let mut writer = ScriptedWriter { + steps: [ + Ok(2), + Err(ErrorKind::Interrupted.into()), + Err(ErrorKind::WouldBlock.into()), + ] + .into(), + data: Vec::new(), + }; + let mut waits = 0; + assert!(write_with_wait(&mut writer, &[1, 2, 3, 4], 100, |_, _| { + waits += 1; + Ok(true) + }) + .unwrap()); + assert_eq!(waits, 1); + assert_eq!(writer.data, [1, 2, 3, 4]); + } + + #[test] + fn blocked_device_retains_bounded_timeout_and_error_handling() { + let mut writer = ScriptedWriter { + steps: [Err(ErrorKind::WouldBlock.into())].into(), + data: Vec::new(), + }; + assert!(!write_with_wait(&mut writer, &[1], 100, |_, _| Ok(false)).unwrap()); + assert!(writer.data.is_empty()); + writer.steps.push_back(Err(ErrorKind::BrokenPipe.into())); + assert_eq!( + write_with_wait(&mut writer, &[1], 100, |_, _| Ok(true)) + .unwrap_err() + .kind(), + ErrorKind::BrokenPipe + ); } } From 8b4ca6d080af23d46e82fd1b440ad5df4295d768 Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Thu, 1 Oct 2026 23:05:24 +0800 Subject: [PATCH 10/11] =?UTF-8?q?refactor(video):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E6=80=A7=E8=83=BD=E5=AE=9E=E9=AA=8C=E5=BC=80=E5=85=B3=E5=92=8C?= =?UTF-8?q?=E9=80=90=E5=B8=A7=E7=BB=9F=E8=AE=A1=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libs/hwcodec/README.md | 10 +- .../cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp | 41 ---- .../cpp/ffmpeg_ram/ffmpeg_ram_encode_stats.h | 17 -- libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_ffi.h | 2 - libs/hwcodec/src/ffmpeg_ram/encode.rs | 48 +---- src/video/codec/h264.rs | 4 - src/video/pipeline/delivery.rs | 7 +- src/video/pipeline/encoder_state.rs | 7 - src/video/pipeline/frame_mailbox.rs | 125 +---------- src/video/pipeline/mjpeg_admission.rs | 22 -- src/video/pipeline/shared.rs | 195 +----------------- src/webrtc/universal_session.rs | 3 +- src/webrtc/video_track.rs | 107 +--------- 13 files changed, 25 insertions(+), 563 deletions(-) delete mode 100644 libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_encode_stats.h diff --git a/libs/hwcodec/README.md b/libs/hwcodec/README.md index f9f899f6..4b87e5ba 100644 --- a/libs/hwcodec/README.md +++ b/libs/hwcodec/README.md @@ -49,13 +49,9 @@ input to FFmpeg V4L2 M2M encoders. It holds a `Bytes` owner in a read-only input into an intermediate AVFrame. Other encoders retain the existing copy path. Converted or stride-compacted One-KVM input also retains that path. -`ONE_KVM_V4L2_OWNED_INPUT=0` disables this optimization when the encoder is -created, allowing comparisons with the same binary. `ONE_KVM_VIDEO_STATS=1` -enables preparation, send-frame, receive-packet, callback and output-copy -timings. One-KVM reports them in `[VideoInputPerf]` at five-second intervals; -`copied_bytes` counts only the intermediate raw-input copy, not FFmpeg's -remaining copy into V4L2 MMAP buffers. Encoded packet detachment remains in -place to avoid retaining scarce V4L2 capture buffers in downstream caches. +Encoded packet detachment remains in place to avoid retaining scarce V4L2 +capture buffers in downstream caches. FFmpeg still copies raw input into its +V4L2 MMAP buffers. ## System requirements diff --git a/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp b/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp index 856b751a..fb821554 100644 --- a/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp +++ b/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp @@ -5,7 +5,6 @@ extern "C" { #include #include #include -#include } #include @@ -15,7 +14,6 @@ extern "C" { #include #include "common.h" -#include "ffmpeg_ram_encode_stats.h" #include "owned_nv12_frame.h" #define LOG_MODULE "FFMPEG_RAM_ENC" @@ -149,8 +147,6 @@ public: RamEncodePacketCallback packet_callback_ = NULL; int offset_[AV_NUM_DATA_POINTERS] = {0}; bool force_keyframe_ = false; // Force next frame to be a keyframe - bool measure_ = false; - RamEncodeStats stats_ = {}; AVHWDeviceType hw_device_type_ = AV_HWDEVICE_TYPE_NONE; AVPixelFormat hw_pixfmt_ = AV_PIX_FMT_NONE; @@ -175,8 +171,6 @@ public: thread_count_ = thread_count; gpu_ = gpu; callback_ = callback; - const char *stats_setting = getenv("ONE_KVM_VIDEO_STATS"); - measure_ = stats_setting && strcmp(stats_setting, "1") == 0; if (name_.find("vaapi") != std::string::npos) { hw_device_type_ = AV_HWDEVICE_TYPE_VAAPI; hw_pixfmt_ = AV_PIX_FMT_VAAPI; @@ -305,7 +299,6 @@ public: int encode(const uint8_t *data, int length, const void *obj, uint64_t ms) { int ret; - const int64_t prepare_start = timer_start(); if ((ret = av_frame_make_writable(frame_)) != 0) { LOG_ERROR(std::string("av_frame_make_writable failed, ret = ") + av_err2str(ret)); @@ -324,15 +317,6 @@ public: tmp_frame = frame_; } - timer_add(stats_.prepare_us, prepare_start); - if (measure_) { - stats_.input_frames++; - stats_.copied_frames++; - const int bytes = av_image_get_buffer_size(pixfmt_, width_, height_, 1); - if (bytes > 0) - stats_.copied_bytes += bytes; - } - return do_encode(tmp_frame, obj, ms); } @@ -344,16 +328,10 @@ public: release(owner, const_cast(data)); return AVERROR(ENOSYS); } - const int64_t prepare_start = timer_start(); auto frame = make_owned_nv12_frame(data, length, width_, height_, owner, release); - timer_add(stats_.prepare_us, prepare_start); if (!frame) return AVERROR(EINVAL); - if (measure_) { - stats_.input_frames++; - stats_.borrowed_frames++; - } packet_callback_ = callback; int ret = do_encode(frame.get(), obj, ms); packet_callback_ = NULL; @@ -386,17 +364,8 @@ public: } private: - int64_t timer_start() const { return measure_ ? av_gettime_relative() : 0; } - - void timer_add(uint64_t &elapsed, int64_t start) const { - if (measure_) - elapsed += av_gettime_relative() - start; - } - int send_frame(AVFrame *frame) { - const int64_t start = timer_start(); const int ret = avcodec_send_frame(c_, frame); - timer_add(stats_.send_us, start); return ret; } @@ -472,9 +441,7 @@ private: auto start = util::now(); while (util::elapsed_ms(start) < DECODE_TIMEOUT_MS) { - const int64_t receive_start = timer_start(); ret = avcodec_receive_packet(c_, pkt_); - timer_add(stats_.receive_us, receive_start); if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { return 0; } @@ -489,7 +456,6 @@ private: continue; } encoded = true; - const int64_t packet_start = timer_start(); if (packet_callback_) { AVPacket *owned_pkt = av_packet_clone(pkt_); if (!owned_pkt) { @@ -505,7 +471,6 @@ private: pkt_->flags & AV_PKT_FLAG_KEY, obj); } av_packet_unref(pkt_); - timer_add(stats_.packet_us, packet_start); } return 0; @@ -782,12 +747,6 @@ extern "C" int ffmpeg_ram_encode_owned_packet( } } -extern "C" void ffmpeg_ram_encoder_stats(FFmpegRamEncoder *encoder, - RamEncodeStats *stats) { - if (encoder && stats) - *stats = encoder->stats_; -} - extern "C" int ffmpeg_ram_set_bitrate(FFmpegRamEncoder *encoder, int kbs) { try { return encoder->set_bitrate(kbs); diff --git a/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_encode_stats.h b/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_encode_stats.h deleted file mode 100644 index 231a195b..00000000 --- a/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_encode_stats.h +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef FFMPEG_RAM_ENCODE_STATS_H -#define FFMPEG_RAM_ENCODE_STATS_H - -#include - -typedef struct RamEncodeStats { - uint64_t input_frames; - uint64_t borrowed_frames; - uint64_t copied_frames; - uint64_t copied_bytes; - uint64_t prepare_us; - uint64_t send_us; - uint64_t receive_us; - uint64_t packet_us; -} RamEncodeStats; - -#endif diff --git a/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_ffi.h b/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_ffi.h index 516ca69c..831fefbf 100644 --- a/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_ffi.h +++ b/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_ffi.h @@ -2,7 +2,6 @@ #define FFMPEG_RAM_FFI_H #include -#include "ffmpeg_ram_encode_stats.h" #define AV_NUM_DATA_POINTERS 8 @@ -30,7 +29,6 @@ int ffmpeg_ram_encode_owned_packet(void *encoder, const uint8_t *data, void (*release)(void *, uint8_t *), const void *obj, int64_t ms, RamEncodePacketCallback callback); -void ffmpeg_ram_encoder_stats(void *encoder, RamEncodeStats *stats); void ffmpeg_ram_free_encoder(void *encoder); void ffmpeg_ram_free_packet(void *packet); int ffmpeg_ram_get_linesize_offset_length(int pix_fmt, int width, int height, diff --git a/libs/hwcodec/src/ffmpeg_ram/encode.rs b/libs/hwcodec/src/ffmpeg_ram/encode.rs index 6daf6458..a788cbd4 100644 --- a/libs/hwcodec/src/ffmpeg_ram/encode.rs +++ b/libs/hwcodec/src/ffmpeg_ram/encode.rs @@ -501,22 +501,6 @@ pub struct Encoder { pub offset: Vec, pub length: i32, owned_nv12_input: bool, - measure: bool, - #[cfg(feature = "bytes")] - output_copy_us: u64, -} - -#[derive(Clone, Copy, Debug, Default)] -pub struct EncodeInputStats { - pub input_frames: u64, - pub borrowed_frames: u64, - pub copied_frames: u64, - pub copied_bytes: u64, - pub prepare_us: u64, - pub send_us: u64, - pub receive_us: u64, - pub packet_us: u64, - pub output_copy_us: u64, } impl Encoder { @@ -565,8 +549,7 @@ impl Encoder { } let owned_nv12_input = ctx.name.ends_with("_v4l2m2m") - && ctx.pixfmt == AVPixelFormat::AV_PIX_FMT_NV12 as i32 - && std::env::var("ONE_KVM_V4L2_OWNED_INPUT").as_deref() != Ok("0"); + && ctx.pixfmt == AVPixelFormat::AV_PIX_FMT_NV12 as i32; Ok(Encoder { codec, frames: Box::into_raw(Box::new(Vec::::new())), @@ -575,9 +558,6 @@ impl Encoder { offset, length: length[0], owned_nv12_input, - measure: std::env::var("ONE_KVM_VIDEO_STATS").as_deref() == Ok("1"), - #[cfg(feature = "bytes")] - output_copy_us: 0, }) } } @@ -626,28 +606,6 @@ impl Encoder { self.owned_nv12_input } - pub fn input_stats(&self) -> Option { - if !self.measure { - return None; - } - let mut stats = unsafe { std::mem::zeroed() }; - unsafe { crate::ffmpeg_ram::ffmpeg_ram_encoder_stats(self.codec, &mut stats) }; - Some(EncodeInputStats { - input_frames: stats.input_frames, - borrowed_frames: stats.borrowed_frames, - copied_frames: stats.copied_frames, - copied_bytes: stats.copied_bytes, - prepare_us: stats.prepare_us, - send_us: stats.send_us, - receive_us: stats.receive_us, - packet_us: stats.packet_us, - #[cfg(feature = "bytes")] - output_copy_us: self.output_copy_us, - #[cfg(not(feature = "bytes"))] - output_copy_us: 0, - }) - } - #[cfg(feature = "bytes")] pub fn encode_owned_bytes( &mut self, @@ -688,7 +646,6 @@ impl Encoder { if !self.ctx.name.contains("v4l2m2m") { return Ok(frames); } - let start = self.measure.then(std::time::Instant::now); let detached = frames .into_iter() .map(|frame| EncodeBytesFrame { @@ -697,9 +654,6 @@ impl Encoder { key: frame.key, }) .collect(); - if let Some(start) = start { - self.output_copy_us += start.elapsed().as_micros() as u64; - } Ok(detached) } diff --git a/src/video/codec/h264.rs b/src/video/codec/h264.rs index 7465c4a7..e66b8919 100644 --- a/src/video/codec/h264.rs +++ b/src/video/codec/h264.rs @@ -378,10 +378,6 @@ impl H264Encoder { self.inner.supports_owned_nv12_input() } - pub fn input_stats(&self) -> Option { - self.inner.input_stats() - } - pub fn encode_owned_nv12(&mut self, data: Bytes, pts_ms: i64) -> Result> { if !self.supports_owned_nv12_input() { return self.encode_raw(data.as_ref(), pts_ms); diff --git a/src/video/pipeline/delivery.rs b/src/video/pipeline/delivery.rs index 7296a8a6..3859c78d 100644 --- a/src/video/pipeline/delivery.rs +++ b/src/video/pipeline/delivery.rs @@ -1,7 +1,6 @@ use std::collections::VecDeque; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::Arc; -use std::time::Duration; use parking_lot::Mutex; use tokio::sync::mpsc; @@ -16,7 +15,6 @@ pub(super) type FrameMailboxHandle = Arc Result> { self.encode_raw(data.as_ref(), pts_ms) } - fn input_stats(&self) -> Option { - None - } fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()>; fn codec_name(&self) -> &str; fn request_keyframe(&mut self); @@ -93,10 +90,6 @@ impl VideoEncoderTrait for H264EncoderWrapper { .collect()) } - fn input_stats(&self) -> Option { - self.0.input_stats() - } - fn codec_name(&self) -> &str { self.0.codec_name() } diff --git a/src/video/pipeline/frame_mailbox.rs b/src/video/pipeline/frame_mailbox.rs index 02316c1f..15bc05ec 100644 --- a/src/video/pipeline/frame_mailbox.rs +++ b/src/video/pipeline/frame_mailbox.rs @@ -1,21 +1,4 @@ use std::sync::{Condvar, Mutex}; -use std::time::{Duration, Instant}; - -#[derive(Debug, Default, PartialEq, Eq)] -pub(super) struct FrameWaitTimes { - pub(super) frame: Duration, - pub(super) output: Duration, -} - -impl FrameWaitTimes { - fn record(&mut self, pending: bool, elapsed: Duration) { - if pending { - self.output += elapsed; - } else { - self.frame += elapsed; - } - } -} #[derive(Debug, PartialEq, Eq)] pub(super) enum PublishResult { @@ -67,21 +50,10 @@ impl FrameMailbox { self.receive_when(|| Some(())).map(|(frame, ())| frame) } - #[cfg(test)] pub(super) fn receive_when( - &self, - ready: impl FnMut() -> Option, - ) -> Option<(Frame, Ready)> { - self.receive_when_timed(ready, false) - .map(|(frame, ready, _)| (frame, ready)) - } - - pub(super) fn receive_when_timed( &self, mut ready: impl FnMut() -> Option, - measure: bool, - ) -> Option<(Frame, Ready, FrameWaitTimes)> { - let mut waits = FrameWaitTimes::default(); + ) -> Option<(Frame, Ready)> { let mut state = self.state.lock().expect("frame mailbox lock poisoned"); loop { if state.closed { @@ -89,18 +61,13 @@ impl FrameMailbox { } if state.pending.is_some() { if let Some(ready) = ready() { - return state.pending.take().map(|frame| (frame, ready, waits)); + return state.pending.take().map(|frame| (frame, ready)); } } - let pending = state.pending.is_some(); - let wait_started = measure.then(Instant::now); state = self .available .wait(state) .expect("frame mailbox lock poisoned"); - if let Some(started) = wait_started { - waits.record(pending, started.elapsed()); - } } } @@ -210,76 +177,7 @@ mod tests { } #[test] - fn wait_times_separate_missing_input_from_full_output() { - let mut waits = FrameWaitTimes::default(); - waits.record(false, Duration::from_millis(3)); - waits.record(true, Duration::from_millis(7)); - waits.record(false, Duration::from_millis(2)); - assert_eq!(waits.frame, Duration::from_millis(5)); - assert_eq!(waits.output, Duration::from_millis(7)); - } - - #[test] - fn available_frame_has_no_wait_time() { - let mailbox = FrameMailbox::new(); - mailbox.publish(1, "ready"); - let result = mailbox.receive_when_timed(|| Some(42), true); - assert_eq!(result, Some(("ready", 42, FrameWaitTimes::default()))); - } - - #[test] - fn timed_output_wait_preserves_latest_frame_and_can_be_disabled() { - use std::sync::atomic::{AtomicBool, Ordering}; - - for measure in [false, true] { - let mailbox = Arc::new(FrameMailbox::new()); - mailbox.publish(1, "initial"); - let ready = Arc::new(AtomicBool::new(false)); - let receiver_mailbox = mailbox.clone(); - let receiver_ready = ready.clone(); - let (waiting_sender, waiting_receiver) = mpsc::channel(); - let (result_sender, result_receiver) = mpsc::channel(); - let worker = std::thread::spawn(move || { - let mut reported = false; - let result = receiver_mailbox.receive_when_timed( - || { - if receiver_ready.load(Ordering::Acquire) { - Some(42) - } else { - if !reported { - waiting_sender.send(()).unwrap(); - reported = true; - } - None - } - }, - measure, - ); - result_sender.send(result).unwrap(); - }); - waiting_receiver - .recv_timeout(Duration::from_secs(2)) - .unwrap(); - mailbox.publish(2, "latest"); - ready.store(true, Ordering::Release); - mailbox.notify(); - let (frame, permit, waits) = result_receiver - .recv_timeout(Duration::from_secs(2)) - .unwrap() - .unwrap(); - assert_eq!((frame, permit), ("latest", 42)); - assert_eq!(waits.frame, Duration::ZERO); - if measure { - assert!(waits.output > Duration::ZERO); - } else { - assert_eq!(waits, FrameWaitTimes::default()); - } - worker.join().unwrap(); - } - } - - #[test] - fn timed_receiver_exits_when_closed_without_output_capacity() { + fn receiver_exits_when_closed_without_output_capacity() { let mailbox = Arc::new(FrameMailbox::new()); mailbox.publish(1, "pending"); let receiver = mailbox.clone(); @@ -287,16 +185,13 @@ mod tests { let (result_sender, result_receiver) = mpsc::channel(); let worker = std::thread::spawn(move || { let mut reported = false; - let result = receiver.receive_when_timed( - || { - if !reported { - waiting_sender.send(()).unwrap(); - reported = true; - } - None::<()> - }, - true, - ); + let result = receiver.receive_when(|| { + if !reported { + waiting_sender.send(()).unwrap(); + reported = true; + } + None::<()> + }); result_sender.send(result).unwrap(); }); waiting_receiver diff --git a/src/video/pipeline/mjpeg_admission.rs b/src/video/pipeline/mjpeg_admission.rs index b274473d..9e71bd7e 100644 --- a/src/video/pipeline/mjpeg_admission.rs +++ b/src/video/pipeline/mjpeg_admission.rs @@ -20,14 +20,6 @@ pub(super) enum MjpegDropReason { } impl MjpegAdmissionPolicy { - pub(super) fn from_prefetch_setting(setting: Option<&str>) -> Self { - if setting == Some("0") { - Self::PendingSlot - } else { - Self::BoundedPrefetch - } - } - pub(super) fn check_pending(self, output_backpressured: bool) -> bool { self == Self::PendingSlot || output_backpressured } @@ -54,20 +46,6 @@ impl MjpegAdmissionPolicy { mod tests { use super::*; - #[test] - fn bounded_prefetch_is_default_and_zero_restores_pending_slot_policy() { - for setting in [None, Some("1"), Some(""), Some("invalid")] { - assert_eq!( - MjpegAdmissionPolicy::from_prefetch_setting(setting), - MjpegAdmissionPolicy::BoundedPrefetch - ); - } - assert_eq!( - MjpegAdmissionPolicy::from_prefetch_setting(Some("0")), - MjpegAdmissionPolicy::PendingSlot - ); - } - #[test] fn prefetch_keeps_decoding_while_output_has_capacity() { for stage in [MjpegAdmissionStage::Capture, MjpegAdmissionStage::Decode] { diff --git a/src/video/pipeline/shared.rs b/src/video/pipeline/shared.rs index 25a10fc9..31e6c75b 100644 --- a/src/video/pipeline/shared.rs +++ b/src/video/pipeline/shared.rs @@ -223,7 +223,6 @@ fn spawn_mjpeg_decode_workers( } let nv12_size = resolution.width as usize * resolution.height as usize * 3 / 2; let mut nv12 = worker_buffer_pool.take(nv12_size); - let decode_started = Instant::now(); let decode_result = decoder.decode_into(&job.data, &mut nv12); worker_buffer_pool.put(job.data); @@ -232,10 +231,7 @@ fn spawn_mjpeg_decode_workers( warn!("Dropping undecodable MJPEG frame: {}", error); continue; } - worker_pipeline.mjpeg_decode_time_ns.fetch_add( - decode_started.elapsed().as_nanos() as u64, - Ordering::Relaxed, - ); + worker_pipeline .mjpeg_decoded_frames .fetch_add(1, Ordering::Relaxed); @@ -496,94 +492,6 @@ fn log_encoding_error( } } -fn latency_percentile_ms(sorted_time_ns: &[u64], percentile: usize) -> f64 { - if sorted_time_ns.is_empty() { - return 0.0; - } - let index = (sorted_time_ns.len() * percentile.clamp(1, 100)).div_ceil(100) - 1; - sorted_time_ns[index] as f64 / 1_000_000.0 -} - -fn log_encoder_input_performance( - stats: hwcodec::ffmpeg_ram::encode::EncodeInputStats, - previous: &mut hwcodec::ffmpeg_ram::encode::EncodeInputStats, -) { - let delta = |current: u64, before: u64| current.saturating_sub(before); - let inputs = delta(stats.input_frames, previous.input_frames); - let average_ms = |current: u64, before: u64| { - if inputs == 0 { - 0.0 - } else { - delta(current, before) as f64 / inputs as f64 / 1000.0 - } - }; - info!( - "[VideoInputPerf] inputs={} borrowed={} copied={} copied_bytes={} prepare_ms={:.3} send_frame_ms={:.3} receive_packet_ms={:.3} packet_callback_ms={:.3} output_copy_ms={:.3}", - inputs, - delta(stats.borrowed_frames, previous.borrowed_frames), - delta(stats.copied_frames, previous.copied_frames), - delta(stats.copied_bytes, previous.copied_bytes), - average_ms(stats.prepare_us, previous.prepare_us), - average_ms(stats.send_us, previous.send_us), - average_ms(stats.receive_us, previous.receive_us), - average_ms(stats.packet_us, previous.packet_us), - average_ms(stats.output_copy_us, previous.output_copy_us), - ); - *previous = stats; -} - -fn log_pipeline_performance( - stats: &SharedVideoPipelineStats, - previous: &SharedVideoPipelineStats, - elapsed: f64, - sorted_encode_time_ns: &[u64], -) { - let delta = |current: u64, previous: u64| current.saturating_sub(previous); - let decoded = delta(stats.mjpeg_decoded_frames, previous.mjpeg_decoded_frames); - let encoded_inputs = delta(stats.encoded_input_frames, previous.encoded_input_frames); - let encoded = delta(stats.encoded_frames, previous.encoded_frames); - let sent = delta(stats.sent_frames, previous.sent_frames); - let send_errors = delta(stats.send_errors, previous.send_errors); - let average_ms = |time_ns: u64, count: u64| { - if count == 0 { - 0.0 - } else { - time_ns as f64 / count as f64 / 1_000_000.0 - } - }; - info!( - "[VideoPerf] window_s={:.2} decoded_fps={:.2} encoded_fps={:.2} sent_fps={:.2} decode_ms={:.2} encode_ms={:.2} send_ms={:.2} encoded_mbps={:.2} dropped_before_decode={} decoded_unused={} backpressure_events={} slow_subscriber_skips={} send_errors={}", - elapsed, - decoded as f64 / elapsed, - encoded as f64 / elapsed, - sent as f64 / elapsed, - average_ms(delta(stats.mjpeg_decode_time_ns, previous.mjpeg_decode_time_ns), decoded), - average_ms(delta(stats.encode_time_ns, previous.encode_time_ns), encoded_inputs), - average_ms(delta(stats.send_time_ns, previous.send_time_ns), sent + send_errors), - delta(stats.encoded_bytes, previous.encoded_bytes) as f64 * 8.0 / elapsed / 1_000_000.0, - delta(stats.mjpeg_dropped_before_decode, previous.mjpeg_dropped_before_decode), - delta(stats.decoded_frames_not_encoded, previous.decoded_frames_not_encoded), - delta(stats.output_backpressure_events, previous.output_backpressure_events), - delta(stats.encoded_frames_skipped_for_slow_subscribers, previous.encoded_frames_skipped_for_slow_subscribers), - send_errors, - ); - info!( - "[VideoQueuePerf] window_s={:.2} input_fps={:.2} frame_wait_ms={:.2} output_wait_ms={:.2} timed_inputs={} encode_p95_ms={:.2} encode_p99_ms={:.2} drop_pending_capture={} drop_pending_decode={} drop_backpressure_capture={} drop_backpressure_decode={} drop_workers_busy={}", - elapsed, - encoded_inputs as f64 / elapsed, - average_ms(delta(stats.encoder_frame_wait_ns, previous.encoder_frame_wait_ns), encoded_inputs), - average_ms(delta(stats.encoder_output_wait_ns, previous.encoder_output_wait_ns), encoded_inputs), - sorted_encode_time_ns.len(), - latency_percentile_ms(sorted_encode_time_ns, 95), - latency_percentile_ms(sorted_encode_time_ns, 99), - delta(stats.mjpeg_dropped_pending_capture, previous.mjpeg_dropped_pending_capture), - delta(stats.mjpeg_dropped_pending_decode, previous.mjpeg_dropped_pending_decode), - delta(stats.mjpeg_dropped_backpressure_capture, previous.mjpeg_dropped_backpressure_capture), - delta(stats.mjpeg_dropped_backpressure_decode, previous.mjpeg_dropped_backpressure_decode), - delta(stats.mjpeg_dropped_workers_busy, previous.mjpeg_dropped_workers_busy), - ); -} - /// Pipeline statistics #[derive(Debug, Clone, Default)] pub struct SharedVideoPipelineStats { @@ -596,17 +504,12 @@ pub struct SharedVideoPipelineStats { pub mjpeg_dropped_backpressure_decode: u64, pub mjpeg_dropped_workers_busy: u64, pub decoded_frames_not_encoded: u64, - pub mjpeg_decode_time_ns: u64, pub encoded_input_frames: u64, pub encoded_frames: u64, pub encoded_bytes: u64, - pub encode_time_ns: u64, - pub encoder_frame_wait_ns: u64, - pub encoder_output_wait_ns: u64, pub output_backpressure_events: u64, pub encoded_frames_skipped_for_slow_subscribers: u64, pub sent_frames: u64, - pub send_time_ns: u64, pub send_errors: u64, } @@ -633,13 +536,9 @@ pub struct SharedVideoPipeline { mjpeg_dropped_backpressure_decode: AtomicU64, mjpeg_dropped_workers_busy: AtomicU64, decoded_frames_not_encoded: AtomicU64, - mjpeg_decode_time_ns: AtomicU64, encoded_input_frames: AtomicU64, encoded_frames: AtomicU64, encoded_bytes: AtomicU64, - encode_time_ns: AtomicU64, - encoder_frame_wait_ns: AtomicU64, - encoder_output_wait_ns: AtomicU64, output_backpressure_events: AtomicU64, output_backpressured: AtomicBool, output_backpressure_since_ns: AtomicU64, @@ -701,13 +600,9 @@ impl SharedVideoPipeline { mjpeg_dropped_backpressure_decode: AtomicU64::new(0), mjpeg_dropped_workers_busy: AtomicU64::new(0), decoded_frames_not_encoded: AtomicU64::new(0), - mjpeg_decode_time_ns: AtomicU64::new(0), encoded_input_frames: AtomicU64::new(0), encoded_frames: AtomicU64::new(0), encoded_bytes: AtomicU64::new(0), - encode_time_ns: AtomicU64::new(0), - encoder_frame_wait_ns: AtomicU64::new(0), - encoder_output_wait_ns: AtomicU64::new(0), output_backpressure_events: AtomicU64::new(0), output_backpressured: AtomicBool::new(false), output_backpressure_since_ns: AtomicU64::new(0), @@ -881,19 +776,14 @@ impl SharedVideoPipeline { .load(Ordering::Relaxed); stats.mjpeg_dropped_workers_busy = self.mjpeg_dropped_workers_busy.load(Ordering::Relaxed); stats.decoded_frames_not_encoded = self.decoded_frames_not_encoded.load(Ordering::Relaxed); - stats.mjpeg_decode_time_ns = self.mjpeg_decode_time_ns.load(Ordering::Relaxed); stats.encoded_input_frames = self.encoded_input_frames.load(Ordering::Relaxed); stats.encoded_frames = self.encoded_frames.load(Ordering::Relaxed); stats.encoded_bytes = self.encoded_bytes.load(Ordering::Relaxed); - stats.encode_time_ns = self.encode_time_ns.load(Ordering::Relaxed); - stats.encoder_frame_wait_ns = self.encoder_frame_wait_ns.load(Ordering::Relaxed); - stats.encoder_output_wait_ns = self.encoder_output_wait_ns.load(Ordering::Relaxed); stats.output_backpressure_events = self.output_backpressure_events.load(Ordering::Relaxed); stats.encoded_frames_skipped_for_slow_subscribers = self .encoded_frames_skipped_for_slow_subscribers .load(Ordering::Relaxed); stats.sent_frames = self.delivery_stats.sent_frames.load(Ordering::Relaxed); - stats.send_time_ns = self.delivery_stats.send_time_ns.load(Ordering::Relaxed); stats.send_errors = self.delivery_stats.send_errors.load(Ordering::Relaxed); stats } @@ -1291,9 +1181,7 @@ impl SharedVideoPipeline { } let mut encoder_config = config.clone(); - let admission_policy = MjpegAdmissionPolicy::from_prefetch_setting( - std::env::var("ONE_KVM_MJPEG_PREFETCH").ok().as_deref(), - ); + let admission_policy = MjpegAdmissionPolicy::BoundedPrefetch; if parallel_mjpeg_decode { encoder_config.input_format = PixelFormat::Nv12; info!("Using parallel libyuv MJPEG decode with hardware encoding"); @@ -1314,13 +1202,9 @@ impl SharedVideoPipeline { .store(0, Ordering::Relaxed); self.mjpeg_dropped_workers_busy.store(0, Ordering::Relaxed); self.decoded_frames_not_encoded.store(0, Ordering::Relaxed); - self.mjpeg_decode_time_ns.store(0, Ordering::Relaxed); self.encoded_input_frames.store(0, Ordering::Relaxed); self.encoded_frames.store(0, Ordering::Relaxed); self.encoded_bytes.store(0, Ordering::Relaxed); - self.encode_time_ns.store(0, Ordering::Relaxed); - self.encoder_frame_wait_ns.store(0, Ordering::Relaxed); - self.encoder_output_wait_ns.store(0, Ordering::Relaxed); self.output_backpressure_events.store(0, Ordering::Relaxed); self.output_backpressured.store(false, Ordering::Release); self.output_backpressure_since_ns @@ -1328,7 +1212,6 @@ impl SharedVideoPipeline { self.encoded_frames_skipped_for_slow_subscribers .store(0, Ordering::Relaxed); self.delivery_stats.sent_frames.store(0, Ordering::Relaxed); - self.delivery_stats.send_time_ns.store(0, Ordering::Relaxed); self.delivery_stats.send_errors.store(0, Ordering::Relaxed); for subscriber in self.subscribers.read().iter() { subscriber.needs_keyframe.store(true, Ordering::Release); @@ -1357,17 +1240,12 @@ impl SharedVideoPipeline { let mut encoded_frame_count: u64 = 0; let mut last_fps_time = Instant::now(); let mut fps_frame_count: u64 = 0; - let perf_logging = std::env::var("ONE_KVM_VIDEO_STATS").as_deref() == Ok("1"); - let mut last_perf_time = Instant::now(); - let mut last_perf_stats = pipeline.stats_snapshot(); - let mut last_input_stats = Default::default(); - let mut encode_latencies = Vec::with_capacity(if perf_logging { 256 } else { 0 }); let encode_error_throttler = LogThrottler::with_secs(ENCODE_ERROR_THROTTLE_SECS); let mut suppressed_encode_errors: HashMap = HashMap::new(); while pipeline.running_flag.load(Ordering::Acquire) { - let Some((frame, reservations, waits)) = frame_mailbox - .receive_when_timed(|| pipeline.reserve_outputs(), perf_logging) + let Some((frame, reservations)) = + frame_mailbox.receive_when(|| pipeline.reserve_outputs()) else { break; }; @@ -1383,15 +1261,6 @@ impl SharedVideoPipeline { } continue; } - if perf_logging { - pipeline - .encoder_frame_wait_ns - .fetch_add(waits.frame.as_nanos() as u64, Ordering::Relaxed); - pipeline - .encoder_output_wait_ns - .fetch_add(waits.output.as_nanos() as u64, Ordering::Relaxed); - } - while let Ok(cmd) = cmd_rx.try_recv() { if let Err(e) = pipeline.apply_cmd(&mut encoder_state, cmd) { error!("Failed to apply pipeline command: {}", e); @@ -1402,15 +1271,7 @@ impl SharedVideoPipeline { pipeline .encoded_input_frames .fetch_add(1, Ordering::Relaxed); - let encode_started = Instant::now(); let encode_result = pipeline.encode_frame_sync(&mut encoder_state, &frame); - let encode_elapsed_ns = encode_started.elapsed().as_nanos() as u64; - pipeline - .encode_time_ns - .fetch_add(encode_elapsed_ns, Ordering::Relaxed); - if perf_logging { - encode_latencies.push(encode_elapsed_ns); - } match encode_result { Ok(encoded_frames) => { @@ -1449,28 +1310,6 @@ impl SharedVideoPipeline { pipeline.decoded_frames_not_encoded.load(Ordering::Relaxed), ); } - - if perf_logging && last_perf_time.elapsed() >= Duration::from_secs(5) { - let elapsed = last_perf_time.elapsed().as_secs_f64(); - let stats = pipeline.stats_snapshot(); - encode_latencies.sort_unstable(); - log_pipeline_performance( - &stats, - &last_perf_stats, - elapsed, - &encode_latencies, - ); - if let Some(stats) = encoder_state - .encoder - .as_ref() - .and_then(|encoder| encoder.input_stats()) - { - log_encoder_input_performance(stats, &mut last_input_stats); - } - encode_latencies.clear(); - last_perf_stats = stats; - last_perf_time = Instant::now(); - } } pipeline.clear_cmd_tx(); @@ -1910,7 +1749,6 @@ impl SharedVideoPipeline { let nv12_size = resolution.width as usize * resolution.height as usize * 3 / 2; let mut nv12 = buffer_pool.take(nv12_size); - let decode_started = Instant::now(); if let Err(error) = decoder.decode_into(&owned, &mut nv12) { buffer_pool.put(owned); buffer_pool.put(nv12); @@ -1921,10 +1759,7 @@ impl SharedVideoPipeline { continue; } buffer_pool.put(owned); - pipeline.mjpeg_decode_time_ns.fetch_add( - decode_started.elapsed().as_nanos() as u64, - Ordering::Relaxed, - ); + pipeline .mjpeg_decoded_frames .fetch_add(1, Ordering::Relaxed); @@ -2582,10 +2417,6 @@ mod tests { ] { pipeline.record_mjpeg_drop(reason); } - pipeline.encoder_frame_wait_ns.store(123, Ordering::Relaxed); - pipeline - .encoder_output_wait_ns - .store(456, Ordering::Relaxed); let stats = pipeline.stats_snapshot(); assert_eq!(stats.mjpeg_dropped_before_decode, 5); assert_eq!(stats.mjpeg_dropped_pending_capture, 1); @@ -2593,8 +2424,6 @@ mod tests { assert_eq!(stats.mjpeg_dropped_backpressure_capture, 1); assert_eq!(stats.mjpeg_dropped_backpressure_decode, 1); assert_eq!(stats.mjpeg_dropped_workers_busy, 1); - assert_eq!(stats.encoder_frame_wait_ns, 123); - assert_eq!(stats.encoder_output_wait_ns, 456); } #[test] @@ -2661,15 +2490,6 @@ mod tests { assert_eq!(stats.mjpeg_dropped_pending_decode, 0); } - #[test] - fn encoding_percentiles_use_nearest_rank_and_handle_empty_windows() { - assert_eq!(latency_percentile_ms(&[], 95), 0.0); - assert_eq!(latency_percentile_ms(&[3_000_000], 99), 3.0); - let sorted: Vec = (1..=100).map(|millis| millis * 1_000_000).collect(); - assert_eq!(latency_percentile_ms(&sorted, 95), 95.0); - assert_eq!(latency_percentile_ms(&sorted, 99), 99.0); - } - #[test] fn encoder_packet_pts_is_preserved() { use super::super::encoder_state::{EncodedFrame, VideoEncoderTrait}; @@ -3003,12 +2823,11 @@ mod tests { fn send_statistics_include_successes_errors_and_duration() { let pipeline = delivery_test_pipeline(); let receiver = pipeline.subscribe(); - receiver.record_send(Duration::from_micros(100), true); - receiver.record_send(Duration::from_micros(200), false); + receiver.record_send(true); + receiver.record_send(false); let stats = pipeline.stats_snapshot(); assert_eq!(stats.sent_frames, 1); assert_eq!(stats.send_errors, 1); - assert_eq!(stats.send_time_ns, 300_000); } #[test] diff --git a/src/webrtc/universal_session.rs b/src/webrtc/universal_session.rs index 656aed20..e123bc4d 100644 --- a/src/webrtc/universal_session.rs +++ b/src/webrtc/universal_session.rs @@ -669,7 +669,6 @@ impl UniversalSession { } } - let send_started = Instant::now(); let send_result = video_track .write_frame_bytes_at( encoded_frame.data.clone(), @@ -677,7 +676,7 @@ impl UniversalSession { Some(encoded_frame.pts_ms), ) .await; - frame_rx.record_send(send_started.elapsed(), send_result.is_ok()); + frame_rx.record_send(send_result.is_ok()); match send_result { Ok(()) => { frames_sent += 1; diff --git a/src/webrtc/video_track.rs b/src/webrtc/video_track.rs index 4367e616..55bad84e 100644 --- a/src/webrtc/video_track.rs +++ b/src/webrtc/video_track.rs @@ -4,9 +4,9 @@ use bytes::Bytes; use rtp::codecs::h264::H264Payloader; use rtp::packetizer::Payloader; use std::sync::Arc; -use std::time::{Duration, Instant}; +use std::time::Duration; use tokio::sync::Mutex; -use tracing::{debug, info, trace, warn}; +use tracing::{debug, trace, warn}; use webrtc::media::Sample; use webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecCapability; use webrtc::track::track_local::track_local_static_rtp::TrackLocalStaticRTP; @@ -191,63 +191,6 @@ struct H264TrackState { clock: H264RtpClock, sps: Option>, pps: Option>, - performance: H264SendPerformance, -} - -#[derive(Default)] -struct H264SendTiming { - prepare: Duration, - lock: Duration, - packetize: Duration, - write: Duration, -} - -struct H264SendPerformance { - last_report: Instant, - frames: u64, - packets: u64, - bytes: u64, - timing: H264SendTiming, -} - -impl H264SendPerformance { - fn new() -> Self { - Self { - last_report: Instant::now(), - frames: 0, - packets: 0, - bytes: 0, - timing: H264SendTiming::default(), - } - } - - fn record(&mut self, bytes: usize, packets: usize, timing: H264SendTiming) { - self.frames += 1; - self.packets += packets as u64; - self.bytes += bytes as u64; - self.timing.prepare += timing.prepare; - self.timing.lock += timing.lock; - self.timing.packetize += timing.packetize; - self.timing.write += timing.write; - let elapsed = self.last_report.elapsed(); - if elapsed < Duration::from_secs(5) { - return; - } - let average_ms = |duration: Duration| duration.as_secs_f64() * 1000.0 / self.frames as f64; - info!( - "[VideoSendPerf] window_s={:.2} frames={} packets_per_frame={:.2} frame_kib={:.2} prepare_ms={:.3} lock_ms={:.3} packetize_ms={:.3} write_ms={:.3} write_us_per_packet={:.3}", - elapsed.as_secs_f64(), - self.frames, - self.packets as f64 / self.frames as f64, - self.bytes as f64 / self.frames as f64 / 1024.0, - average_ms(self.timing.prepare), - average_ms(self.timing.lock), - average_ms(self.timing.packetize), - average_ms(self.timing.write), - self.timing.write.as_secs_f64() * 1_000_000.0 / self.packets.max(1) as f64, - ); - *self = Self::new(); - } } pub struct UniversalVideoTrack { @@ -256,7 +199,6 @@ pub struct UniversalVideoTrack { config: UniversalVideoTrackConfig, h265_state: Option>, h264_state: Mutex, - h264_perf_enabled: bool, } impl UniversalVideoTrack { @@ -311,7 +253,6 @@ impl UniversalVideoTrack { increment: 90000 / config.fps.max(1), started: false, }, - performance: H264SendPerformance::new(), }; Self { track, @@ -319,7 +260,6 @@ impl UniversalVideoTrack { config, h265_state, h264_state: Mutex::new(h264_state), - h264_perf_enabled: std::env::var("ONE_KVM_VIDEO_STATS").as_deref() == Ok("1"), } } @@ -369,18 +309,13 @@ impl UniversalVideoTrack { /// Keep the stack's H.264 STAP/FU payloader, with explicit RTP timestamps. async fn write_h264_frame(&self, data: Bytes, pts_ms: Option) -> Result<()> { - let prepare_started = self.h264_perf_enabled.then(Instant::now); let mut data = h264_bitstream::normalize_annex_b(data); let inspection = h264_bitstream::inspect_annex_b(data.as_ref()); let idr = inspection.is_idr; let has_parameter_sets = inspection.sps.is_some() && inspection.pps.is_some(); - let prepare_time = prepare_started.map(|started| started.elapsed()); - let lock_started = self.h264_perf_enabled.then(Instant::now); let mut state = self.h264_state.lock().await; - let lock_time = lock_started.map(|started| started.elapsed()); - let packetize_started = self.h264_perf_enabled.then(Instant::now); { if let Some(sps) = inspection.sps { state.sps = Some(sps.to_vec()); @@ -415,8 +350,6 @@ impl UniversalVideoTrack { } let timestamp = state.clock.next(pts_ms); let count = payloads.len(); - let packetize_time = packetize_started.map(|started| started.elapsed()); - let write_started = self.h264_perf_enabled.then(Instant::now); // Serialize complete access units, including their sequence allocation. for (index, payload) in payloads.into_iter().enumerate() { let sequence_number = state.sequence_number; @@ -436,18 +369,6 @@ impl UniversalVideoTrack { .await .map_err(|e| AppError::WebRtcError(format!("H264 RTP write failed: {e}")))?; } - if let Some(started) = write_started { - state.performance.record( - data.len(), - count, - H264SendTiming { - prepare: prepare_time.unwrap_or_default(), - lock: lock_time.unwrap_or_default(), - packetize: packetize_time.unwrap_or_default(), - write: started.elapsed(), - }, - ); - } Ok(()) } @@ -659,30 +580,6 @@ mod tests { } } - #[test] - fn h264_send_performance_accumulates_stage_timings() { - let mut performance = H264SendPerformance::new(); - for _ in 0..2 { - performance.record( - 1200, - 2, - H264SendTiming { - prepare: Duration::from_micros(100), - lock: Duration::from_micros(20), - packetize: Duration::from_micros(200), - write: Duration::from_micros(300), - }, - ); - } - assert_eq!(performance.frames, 2); - assert_eq!(performance.bytes, 2400); - assert_eq!(performance.packets, 4); - assert_eq!(performance.timing.prepare, Duration::from_micros(200)); - assert_eq!(performance.timing.lock, Duration::from_micros(40)); - assert_eq!(performance.timing.packetize, Duration::from_micros(400)); - assert_eq!(performance.timing.write, Duration::from_micros(600)); - } - #[test] fn srtp_backend_preserves_authentication_and_sequence_rollover() { use webrtc::srtp::context::Context; From 66a13d3d8c13533086f32fab40f1376a24e11423 Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Thu, 1 Oct 2026 23:18:40 +0800 Subject: [PATCH 11/11] chore: bump version to v0.2.8 --- .gitignore | 1 - Cargo.lock | 5652 ++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 +- vcpkg.json | 2 +- web/package.json | 2 +- 5 files changed, 5655 insertions(+), 4 deletions(-) create mode 100644 Cargo.lock diff --git a/.gitignore b/.gitignore index 00b24a54..4e639e99 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ # Rust target/ -Cargo.lock # IDE .idea/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..238a9644 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,5652 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher 0.4.4", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "alsa" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d" +dependencies = [ + "alsa-sys", + "bitflags 2.13.2", + "cfg-if", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad7569085a265dd3f607ebecce7458eaab2132a84393534c95b18dcbc3f31e04" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures 0.2.17", + "password-hash", +] + +[[package]] +name = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive 0.5.1", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive 0.6.0", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.20", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure 0.13.2", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure 0.13.2", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "audiopus" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3743519567e9135cf6f9f1a509851cb0c8e4cb9d66feb286668afb1923bec458" +dependencies = [ + "audiopus_sys", +] + +[[package]] +name = "audiopus_sys" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927791de46f70facea982dbfaf19719a41ce6064443403be631a85de6a58fff9" +dependencies = [ + "log", + "pkg-config", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "base64 0.22.1", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "multer", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be44683b41ccb9ab2d23a5230015c9c3c55be97a25e4428366de8873103f7970" +dependencies = [ + "axum", + "axum-core", + "bytes", + "cookie", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-server" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1df331683d982a0b9492b38127151e6453639cd34926eb9c07d4cd8c6d22bfc" +dependencies = [ + "arc-swap", + "bytes", + "either", + "fs-err", + "http", + "http-body", + "hyper", + "hyper-util", + "openssl", + "openssl-sys", + "pin-project-lite", + "tokio", + "tokio-openssl", + "tower-service", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base32" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.13.2", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex 1.3.0", + "syn 2.0.119", +] + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "bluer" +version = "0.17.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af68112f5c60196495c8b0eea68349817855f565df5b04b2477916d09fb1a901" +dependencies = [ + "custom_debug", + "dbus", + "dbus-crossroads", + "dbus-tokio", + "displaydoc", + "futures", + "hex", + "lazy_static", + "libc", + "log", + "macaddr", + "nix 0.29.0", + "num-derive", + "num-traits", + "pin-project", + "serde", + "serde_json", + "strum", + "tokio", + "tokio-stream", + "uuid", +] + +[[package]] +name = "bstr" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" +dependencies = [ + "memchr", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytecheck" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26333eeac754f0ad8a6bcd0eb0ac012156302e4e16b852b72ee399aea4f12c29" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "rancor", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46d07918caa9eeaaf06b7873925c53a61daac173539b4f7715090745e44e4e69" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher 0.4.4", +] + +[[package]] +name = "cc" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex 2.0.1", +] + +[[package]] +name = "ccm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae3c82e4355234767756212c570e29833699ab63e6ffd161887314cc5b43847" +dependencies = [ + "aead", + "cipher 0.4.4", + "ctr", + "subtle", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20 0.9.1", + "cipher 0.4.4", + "poly1305", + "zeroize", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout 0.1.4", + "zeroize", +] + +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "crypto-common 0.2.2", + "inout 0.2.2", +] + +[[package]] +name = "clang-sys" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "clap_lex" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "cookie" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie-factory" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" +dependencies = [ + "futures", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "coreaudio-rs" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5d7dca3ebcf65a035582c9ad4385371a9d9ee6537474d2a278f4e1e475bb58" +dependencies = [ + "bitflags 2.13.2", + "libc", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", +] + +[[package]] +name = "cpal" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f02e8d0327b42d3e2e4ab2119af397344eb9fc54a34bf0ddeaa1277af8681f1" +dependencies = [ + "alsa", + "block2", + "coreaudio-rs", + "dasp_sample", + "jni", + "js-sys", + "libc", + "mach2 0.6.0", + "ndk", + "ndk-context", + "num-derive", + "num-traits", + "objc2", + "objc2-audio-toolbox", + "objc2-avf-audio", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation", + "web-sys", + "windows", + "windows-core", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher 0.4.4", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "custom_debug" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da7d1ad9567b3e11e877f1d7a0fa0360f04162f94965fc4448fbed41a65298e" +dependencies = [ + "custom_debug_derive", +] + +[[package]] +name = "custom_debug_derive" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a707ceda8652f6c7624f2be725652e9524c815bf3b9d55a0b2320be2303f9c11" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure 0.13.2", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dary_heap" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1e3a325bc115f096c8b77bbf027a7c2592230e70be2d985be950d3d5e60ebe" + +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "futures-channel", + "futures-util", + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "dbus-crossroads" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64bff0bd181fba667660276c6b7ebdc50cff37ce593e7adf9e734f89c8f444e8" +dependencies = [ + "dbus", +] + +[[package]] +name = "dbus-tokio" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007688d459bc677131c063a3a77fb899526e17b7980f390b69644bdbc41fad13" +dependencies = [ + "dbus", + "libc", + "tokio", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs 0.6.2", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs 0.7.2", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "des" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a94e407b54f9034d71dd748234cd1e516ced6284009906ae246f177eafe5a" +dependencies = [ + "cipher 0.5.2", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.2", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dtls" +version = "0.17.2" +dependencies = [ + "aes", + "aes-gcm", + "async-trait", + "bytecheck", + "byteorder", + "cbc", + "ccm", + "chacha20poly1305", + "der-parser 9.0.0", + "hmac", + "log", + "openssl", + "p256", + "p384", + "portable-atomic", + "rand 0.9.5", + "rand_core 0.6.4", + "rcgen 0.13.2", + "ring", + "rkyv", + "rustls-pki-types", + "sec1", + "sha1", + "sha2 0.10.9", + "thiserror 1.0.69", + "tokio", + "webrtc-util", + "x25519-dalek", + "x509-parser 0.16.0", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature 2.2.0", + "spki", +] + +[[package]] +name = "ed25519" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" +dependencies = [ + "signature 1.6.4", +] + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" +dependencies = [ + "serde", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "encoding_rs" +version = "0.8.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5ef0006ac9ab233c38522f5ae99cae3625151de8f706cacee1cba4b8e2832a" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + +[[package]] +name = "enumn" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b" + +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs-err" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91aa448ca50d7e79433bdf3ee8d99215430d2ec02ade5aefab2a073a1822e8a" +dependencies = [ + "autocfg", + "tokio", +] + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gcd" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d758ba1b47b00caf47f24925c0074ecb20d6dfcffe7f6d53395c0465674841a" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "gpio-cdev" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09831ec59b80be69e75d29cf36e16afbbe5fd1af9c1bf4689ad91c77db5aa6a6" +dependencies = [ + "bitflags 2.13.2", + "libc", + "nix 0.27.1", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "h2" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hwcodec" +version = "0.8.0" +dependencies = [ + "bindgen", + "bytes", + "cc", + "log", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.5", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "include-flate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f173716febb1ad596c16ea5637b5f1790ea32de8e627493ff82bc73b0876ce" +dependencies = [ + "include-flate-codegen", + "include-flate-compress", +] + +[[package]] +name = "include-flate-codegen" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a7875b62a72ad3f3203cdd8950d4cf9947db036030b974b8b37ceae90c8d8c0" +dependencies = [ + "include-flate-compress", + "proc-macro-error3", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "include-flate-compress" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fbb9c5ccb9a5b67b4afa2974c27e5507ea1bf6d22828cef418e4dfaeca51dd" +dependencies = [ + "libflate", + "zstd", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "interceptor" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88c11a956a48159f7fe539b8198f12b4db9b709ae5f94385b840db38f97fed74" +dependencies = [ + "async-trait", + "bytes", + "futures", + "log", + "portable-atomic", + "rand 0.9.5", + "rtcp", + "rtp", + "thiserror 1.0.69", + "tokio", + "waitgroup", + "webrtc-srtp", + "webrtc-util", +] + +[[package]] +name = "io-kit-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b" +dependencies = [ + "core-foundation-sys", + "mach2 0.4.3", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.20", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "libflate" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "561a8da1a50e1428d3c51321dafeca849df992a5bb67720c386131234caba82e" +dependencies = [ + "adler32", + "crc32fast", + "dary_heap", + "libflate_lz77", + "no_std_io2", +] + +[[package]] +name = "libflate_lz77" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff7a10e427698aef6eef269482776debfef63384d30f13aad39a1a95e0e098fd" +dependencies = [ + "hashbrown 0.16.1", + "no_std_io2", + "rle-decode-fast", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libsodium-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b779387cd56adfbc02ea4a668e704f729be8d6a6abd2c27ca5ee537849a92fd" +dependencies = [ + "cc", + "libc", + "pkg-config", + "walkdir", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libudev" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b324152da65df7bb95acfcaab55e3097ceaab02fb19b228a9eb74d55f135e0" +dependencies = [ + "libc", + "libudev-sys", +] + +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "libyuv" +version = "0.1.0" +dependencies = [ + "bindgen", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "macaddr" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baee0bbc17ce759db233beb01648088061bf678383130602a298e6998eedb2d8" + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "multiversion" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ca4bea16ffc3f443cf7d866912118196bfef4c6a1556ca00f9f9b00bb43f7c" +dependencies = [ + "multiversion-macros", +] + +[[package]] +name = "multiversion-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d416831a7317ef4b08bee00b69cbbb9c8763da7959a7026244d6266869f9c83" +dependencies = [ + "proc-macro2", + "quote", + "rustversion", + "syn 3.0.6", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[package]] +name = "munge" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c" +dependencies = [ + "munge_macro", +] + +[[package]] +name = "munge_macro" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.2", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.7.1", + "pin-utils", +] + +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.13.2", + "cfg-if", + "libc", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset 0.9.1", +] + +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-audio-toolbox" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" +dependencies = [ + "bitflags 2.13.2", + "libc", + "objc2", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-avf-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" +dependencies = [ + "dispatch2", + "objc2", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-audio-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" +dependencies = [ + "bitflags 2.13.2", + "objc2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.2", + "block2", + "dispatch2", + "libc", + "objc2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.2", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs 0.6.2", +] + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs 0.7.2", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "one-kvm" +version = "0.2.8" +dependencies = [ + "alsa", + "anyhow", + "arc-swap", + "argon2", + "async-stream", + "async-trait", + "audiopus", + "axum", + "axum-extra", + "axum-server", + "base64 0.22.1", + "bytemuck", + "bytes", + "clap", + "cpal", + "des", + "futures", + "gpio-cdev", + "hwcodec", + "libc", + "libyuv", + "mime_guess", + "nix 0.31.3", + "one-kvm-bluetooth-hid", + "openssl", + "parking_lot", + "protobuf", + "protobuf-codegen", + "rand 0.10.3", + "rcgen 0.14.10", + "reqwest", + "rtp", + "rtsp-types", + "rust-embed", + "sdp-types", + "serde", + "serde_json", + "serialport", + "sha2 0.11.0", + "sodiumoxide", + "sqlx", + "tempfile", + "thiserror 2.0.20", + "time", + "tokio", + "tokio-tungstenite", + "tokio-util", + "toml_edit", + "totp-rs", + "tower-http", + "tracing", + "tracing-subscriber", + "turbojpeg", + "typeshare", + "urlencoding", + "uuid", + "v4l2r", + "ventoy-img", + "webrtc", + "windows-sys 0.61.2", + "xxhash-rust", +] + +[[package]] +name = "one-kvm-bluetooth-hid" +version = "0.1.0" +dependencies = [ + "bluer", + "dbus", + "dbus-crossroads", + "dbus-tokio", + "libc", + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" +dependencies = [ + "bitflags 2.13.2", + "cfg-if", + "foreign-types", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-src" +version = "300.6.1+3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46eb8fb9fb3b61ce1c0f8a026c4c1a0714d3a9e138e7fbde78753ce2babc3846" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + +[[package]] +name = "pem" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d354a98a3d1251555de99e8fdd8afda05573c31b82f59063a7b0a29b5527f120" +dependencies = [ + "base64 0.23.1", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-error-attr3" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e564d14133360e1ae169ffde5da25881b5fa47261665b8e5713c212c27799da" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error3" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f0d4471b3436c22106b21913b1dda531558918ae9b7ec55d58aa84b43552233" +dependencies = [ + "proc-macro-error-attr3", + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "protobuf" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" +dependencies = [ + "bytes", + "once_cell", + "protobuf-support", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-codegen" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d3976825c0014bbd2f3b34f0001876604fe87e0c86cd8fa54251530f1544ace" +dependencies = [ + "anyhow", + "once_cell", + "protobuf", + "protobuf-parse", + "regex", + "tempfile", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-parse" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4aeaa1f2460f1d348eeaeed86aea999ce98c1bded6f089ff8514c9d9dbdc973" +dependencies = [ + "anyhow", + "indexmap", + "log", + "protobuf", + "protobuf-support", + "tempfile", + "thiserror 1.0.69", + "which", +] + +[[package]] +name = "protobuf-support" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" +dependencies = [ + "thiserror 1.0.69", +] + +[[package]] +name = "ptr_meta" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743da816b98c921cdbe8628ef7381b76f25ecf4da599fc80aca90eae7ef70cc0" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c8d9ca532f185d5d4db7a7c9d51420b452168ea1c2b913953281bd6fe1fcbd0" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rancor" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b534442d0fcdb55d66f373d9cac6d33b6293a2335bc2136dbd06ce0e87d2572" +dependencies = [ + "ptr_meta", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20 0.10.2", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem 3.0.6", + "ring", + "rustls-pki-types", + "time", + "x509-parser 0.16.0", + "yasna 0.5.2", +] + +[[package]] +name = "rcgen" +version = "0.14.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8774e05a7d0de114588e6a28fe7e71694b82614ed569d86d8b389dfbc98b8ad8" +dependencies = [ + "pem 4.0.0", + "ring", + "rustls-pki-types", + "time", + "x509-parser 0.18.1", + "yasna 0.6.0", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rend" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "663ba70707f96e871406fe10d68128412e619b06d1d47cb91c3a4c6501176240" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rkyv" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9776093b7ca170454ab1406954f7b7d97a57c51dc6c0642957fb2ef25c2d399" +dependencies = [ + "bytecheck", + "bytes", + "hashbrown 0.17.1", + "indexmap", + "munge", + "ptr_meta", + "rancor", + "rend", + "rkyv_derive", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c25ef604ac7dd839d44d64648952ea23c97866f124ff671b0ed2cf3ad9bb06e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "rle-decode-fast" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" + +[[package]] +name = "rtcp" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adad7f6a501162881032fc84b4bc78ae11f1b30748180b9f5cbe8810bf613aab" +dependencies = [ + "bytes", + "thiserror 1.0.69", + "webrtc-util", +] + +[[package]] +name = "rtp" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "149329e78ada26b5e174a4c281a7a7e5c8bda6008adc90daddd6f98fce56db29" +dependencies = [ + "bytes", + "memchr", + "portable-atomic", + "rand 0.9.5", + "serde", + "thiserror 1.0.69", + "webrtc-util", +] + +[[package]] +name = "rtsp-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f47bf238c3da7994ef66a71724f385cb1ee25dcf04a156402d2727ad84dc1433" +dependencies = [ + "cookie-factory", + "nom", + "tinyvec", + "url", +] + +[[package]] +name = "rust-embed" +version = "8.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e7760e252aaba7b09f4be00e36476cf585bdb68a53552ac954cdf504ab4bc9" +dependencies = [ + "include-flate", + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bcfc4d6f53af43755f7a723e4b6b8794fcce052a178dd8c6c1dadc5f5343097" +dependencies = [ + "mime_guess", + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.119", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ffa149f6aa81b58a5b3011d01a857c4ed12c7a732d2c51947a4c7c692185f0" +dependencies = [ + "include-flate", + "sha2 0.11.0", + "walkdir", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sdp" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b6eecfa5151edef84d544ff3885dff98e5b5fe97586757a4b5118bde51c958" +dependencies = [ + "rand 0.9.5", + "substring", + "thiserror 1.0.69", + "url", +] + +[[package]] +name = "sdp-types" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb6c636ace20de73fb9536017f9ef069b258308a6c341a4aaf3d2fd11afc570" +dependencies = [ + "bstr", + "fallible-iterator", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serialport" +version = "4.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba5f8f29aa20853c4e3e85a33ec580eb66be1f057142e77a333834a318bacf2" +dependencies = [ + "bitflags 2.13.2", + "cfg-if", + "core-foundation", + "core-foundation-sys", + "io-kit-sys", + "libudev", + "mach2 0.4.3", + "nix 0.26.4", + "scopeguard", + "unescaper", + "windows-sys 0.52.0", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "sodiumoxide" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e26be3acb6c2d9a7aac28482586a7856436af4cfe7100031d219de2d2ecb0028" +dependencies = [ + "ed25519", + "libc", + "libsodium-sys", + "serde", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" +dependencies = [ + "base64 0.22.1", + "bytes", + "cfg-if", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.16.1", + "hashlink", + "indexmap", + "log", + "memchr", + "percent-encoding", + "serde", + "sha2 0.10.9", + "smallvec", + "thiserror 2.0.20", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.119", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" +dependencies = [ + "cfg-if", + "dotenvy", + "either", + "heck", + "hex", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx-core", + "sqlx-sqlite", + "syn 2.0.119", + "tokio", + "url", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" +dependencies = [ + "atoi", + "flume", + "form_urlencoded", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "thiserror 2.0.20", + "tracing", + "url", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.119", +] + +[[package]] +name = "stun" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e3b8d5ad1dd4c8cd0595440f26521a71dd593cb5873ee8e06b91510b7d97269" +dependencies = [ + "base64 0.22.1", + "crc", + "lazy_static", + "md-5", + "rand 0.9.5", + "ring", + "subtle", + "thiserror 1.0.69", + "tokio", + "url", + "webrtc-util", +] + +[[package]] +name = "substring" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ee6433ecef213b2e72f587ef64a2f5943e7cd16fbd82dbe8bc07486c534c86" +dependencies = [ + "autocfg", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix 1.1.5", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.5", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-openssl" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59df6849caa43bb7567f9a36f863c447d95a11d5903c9cc334ba32576a27eadd" +dependencies = [ + "openssl", + "openssl-sys", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.15+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "totp-rs" +version = "5.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e69a15e21b2ff22c415446983978bded3244195f17d59cb113551c1e806f91" +dependencies = [ + "base32", + "constant_time_eq", + "hmac", + "rand 0.9.5", + "sha1", + "sha2 0.10.9", + "url", + "urlencoding", + "zeroize", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.2", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "tracing", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.5", + "sha1", + "thiserror 2.0.20", +] + +[[package]] +name = "turbojpeg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81313fcd64aaa30ddfd59eaa122968136b2dd9cd41d9619b126cf71e67a01702" +dependencies = [ + "gcd", + "libc", + "thiserror 1.0.69", + "turbojpeg-sys", +] + +[[package]] +name = "turbojpeg-sys" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49b5a974bc59382d35927e3a07eef2ba9a24fe7227cefe52ec44b508e1b90f86" +dependencies = [ + "anyhow", + "cmake", + "libc", + "pkg-config", +] + +[[package]] +name = "turn" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d99249a493335eb44c4d7943a8751b22561a82c9903d8eb2d29780b3927880a7" +dependencies = [ + "async-trait", + "base64 0.22.1", + "futures", + "log", + "md-5", + "portable-atomic", + "rand 0.9.5", + "ring", + "stun", + "thiserror 1.0.69", + "tokio", + "tokio-util", + "webrtc-util", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "typeshare" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da1bf9fe204f358ffea7f8f779b53923a20278b3ab8e8d97962c5e1b3a54edb7" +dependencies = [ + "chrono", + "serde", + "serde_json", + "typeshare-annotation", +] + +[[package]] +name = "typeshare-annotation" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "621963e302416b389a1ec177397e9e62de849a78bd8205d428608553def75350" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "unescaper" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7285e83a80ce76f5e7bce79fa41f68d78ba62d1003cf27bf748ab24413808cf4" +dependencies = [ + "thiserror 2.0.20", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "v4l2r" +version = "0.0.7" +dependencies = [ + "bindgen", + "bitflags 2.13.2", + "enumn", + "log", + "nix 0.31.3", + "thiserror 2.0.20", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "ventoy-img" +version = "0.1.0" +dependencies = [ + "clap", + "tempfile", + "thiserror 2.0.20", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "waitgroup" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1f50000a783467e6c0200f9d10642f4bc424e39efc1b770203e88b488f79292" +dependencies = [ + "atomic-waker", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webrtc" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaacdf9d96224d7b6e2872ba065578f38775f7634367e3ac2cc87c7271da433" +dependencies = [ + "arc-swap", + "async-trait", + "bytes", + "dtls", + "hex", + "interceptor", + "lazy_static", + "log", + "portable-atomic", + "rand 0.9.5", + "rcgen 0.13.2", + "regex", + "ring", + "rtcp", + "rtp", + "sdp", + "serde", + "serde_json", + "sha2 0.10.9", + "smol_str", + "stun", + "thiserror 1.0.69", + "tokio", + "turn", + "unicase", + "url", + "waitgroup", + "webrtc-data", + "webrtc-ice", + "webrtc-mdns", + "webrtc-media", + "webrtc-sctp", + "webrtc-srtp", + "webrtc-util", +] + +[[package]] +name = "webrtc-data" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd470286275809f2fcfcdb1e73ef5f1500be82eff7fe98150ce81b20aad5a2a4" +dependencies = [ + "bytes", + "log", + "portable-atomic", + "thiserror 1.0.69", + "tokio", + "webrtc-sctp", + "webrtc-util", +] + +[[package]] +name = "webrtc-ice" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b7fd30f52e6fda8664779b84b7904b2553b76fee24d9ca665e774ae32b13f53" +dependencies = [ + "arc-swap", + "async-trait", + "crc", + "log", + "portable-atomic", + "rand 0.9.5", + "serde", + "serde_json", + "stun", + "thiserror 1.0.69", + "tokio", + "turn", + "url", + "uuid", + "waitgroup", + "webrtc-mdns", + "webrtc-util", +] + +[[package]] +name = "webrtc-mdns" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ffa0ea00c0fae979aafa5db285fec5eeb360e1ea246ebec529ba4e09f8e03e" +dependencies = [ + "log", + "socket2 0.5.10", + "thiserror 1.0.69", + "tokio", + "webrtc-util", +] + +[[package]] +name = "webrtc-media" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26a6c7335bdd03dc023cb9a3bd7966866d969c92c86987b73aa16dfa39c7ca29" +dependencies = [ + "byteorder", + "bytes", + "rand 0.9.5", + "rtp", + "thiserror 1.0.69", +] + +[[package]] +name = "webrtc-sctp" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c4b637f0d8eb96d900ac0f79b3060ddd21ca88dfefa467e96e67ab0016f2574" +dependencies = [ + "arc-swap", + "async-trait", + "bytes", + "crc", + "log", + "portable-atomic", + "rand 0.9.5", + "thiserror 1.0.69", + "tokio", + "webrtc-util", +] + +[[package]] +name = "webrtc-srtp" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45d2b667a0b5d04eebcb7cb22fd51b2af3721f418097b496e72de16386c8d0c3" +dependencies = [ + "aead", + "aes", + "aes-gcm", + "byteorder", + "bytes", + "ctr", + "hmac", + "log", + "openssl", + "rtcp", + "rtp", + "sha1", + "subtle", + "thiserror 1.0.69", + "tokio", + "webrtc-util", +] + +[[package]] +name = "webrtc-util" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1beae0b4f24969741c26ca282a257dc5823bd9cbe608f7e3ca3775102d323ad9" +dependencies = [ + "async-trait", + "bitflags 1.3.2", + "bytes", + "ipnet", + "lazy_static", + "log", + "nix 0.26.4", + "portable-atomic", + "rand 0.9.5", + "thiserror 1.0.69", + "tokio", + "winapi", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs 0.6.2", + "data-encoding", + "der-parser 9.0.0", + "lazy_static", + "nom", + "oid-registry 0.7.1", + "ring", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs 0.7.2", + "data-encoding", + "der-parser 10.0.0", + "lazy_static", + "nom", + "oid-registry 0.8.1", + "ring", + "rusticata-macros", + "thiserror 2.0.20", + "time", +] + +[[package]] +name = "xxhash-rust" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aee1b19627c7c60102ab80d3a9cbe18de90bfe03bfa6c3715447681f0e8c8af6" + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "yasna" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" +dependencies = [ + "bit-vec", + "time", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure 0.14.0", +] + +[[package]] +name = "zerocopy" +version = "0.8.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure 0.14.0", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.1.0+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef0a8027ec3ee71300ab3bcbcd0393f434aa72b91ca6d635a39941deae8eea0" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 86e6f130..6c263e27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "one-kvm" -version = "0.2.7" +version = "0.2.8" edition = "2021" authors = ["SilentWind"] description = "A open and lightweight IP-KVM solution written in Rust" diff --git a/vcpkg.json b/vcpkg.json index 77e3d478..cc784350 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", "name": "one-kvm", - "version-string": "0.2.7", + "version-string": "0.2.8", "dependencies": [ { "name": "ffmpeg", diff --git a/web/package.json b/web/package.json index 00bf1703..75eeee62 100644 --- a/web/package.json +++ b/web/package.json @@ -1,7 +1,7 @@ { "name": "web", "private": true, - "version": "0.2.7", + "version": "0.2.8", "type": "module", "scripts": { "dev": "vite",