From b346af35d3833eaa1cad06da0b48a0423aced8b5 Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Sun, 5 Jul 2026 21:20:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20VNC=E3=80=81RTSP=20=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=9B=91=E5=90=AC=20ipv6=20=E5=9C=B0?= =?UTF-8?q?=E5=9D=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rtsp/service.rs | 12 +++-- src/utils/mod.rs | 35 ++++++++++++++ src/vnc/mod.rs | 12 +++-- src/web/handlers/config/types.rs | 78 ++++++++++++++++++++++++++++++++ 4 files changed, 131 insertions(+), 6 deletions(-) diff --git a/src/rtsp/service.rs b/src/rtsp/service.rs index f7a754cb..0e0897cc 100644 --- a/src/rtsp/service.rs +++ b/src/rtsp/service.rs @@ -8,6 +8,7 @@ use tokio::sync::{broadcast, Mutex, RwLock}; use crate::config::RtspConfig; use crate::error::{AppError, Result}; +use crate::utils::{bind_socket_addr, bind_tcp_listener}; use crate::video::VideoStreamManager; use super::auth::{extract_basic_auth, rtsp_auth_credentials}; @@ -73,13 +74,18 @@ impl RtspService { tracing::debug!("Failed to request keyframe on RTSP start: {}", err); } - let bind_addr: SocketAddr = format!("{}:{}", config.bind, config.port) - .parse() + let bind_addr = bind_socket_addr(&config.bind, config.port) .map_err(|e| AppError::BadRequest(format!("Invalid RTSP bind address: {}", e)))?; - let listener = TcpListener::bind(bind_addr).await.map_err(|e| { + let listener = bind_tcp_listener(bind_addr).map_err(|e| { AppError::Io(io::Error::new(e.kind(), format!("RTSP bind failed: {}", e))) })?; + let listener = TcpListener::from_std(listener).map_err(|e| { + AppError::Io(io::Error::new( + e.kind(), + format!("RTSP listener setup failed: {}", e), + )) + })?; let service_config = self.config.clone(); let video_manager = self.video_manager.clone(); diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 380ccb7f..d1fb32fe 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -15,3 +15,38 @@ pub use host::{hostname_from_etc, hostname_uname}; pub use net::{bind_tcp_listener, bind_udp_socket}; pub use serial::list_serial_ports; pub use throttle::LogThrottler; + +use std::net::{IpAddr, SocketAddr}; + +pub fn bind_socket_addr(bind: &str, port: u16) -> Result { + bind.parse::().map(|ip| SocketAddr::new(ip, port)) +} + +#[cfg(test)] +mod tests { + use super::bind_socket_addr; + + #[test] + fn ipv6_bind_socket_addr_formats_ipv4_and_ipv6() { + assert_eq!( + bind_socket_addr("0.0.0.0", 5900).unwrap().to_string(), + "0.0.0.0:5900" + ); + assert_eq!( + bind_socket_addr("127.0.0.1", 5900).unwrap().to_string(), + "127.0.0.1:5900" + ); + assert_eq!( + bind_socket_addr("::", 8554).unwrap().to_string(), + "[::]:8554" + ); + assert_eq!( + bind_socket_addr("::1", 8554).unwrap().to_string(), + "[::1]:8554" + ); + assert_eq!( + bind_socket_addr("2001:db8::1", 8554).unwrap().to_string(), + "[2001:db8::1]:8554" + ); + } +} diff --git a/src/vnc/mod.rs b/src/vnc/mod.rs index 690e500b..fb9c0716 100644 --- a/src/vnc/mod.rs +++ b/src/vnc/mod.rs @@ -17,6 +17,7 @@ use crate::config::{VncConfig, VncEncoding}; use crate::error::{AppError, Result}; use crate::hid::HidController; use crate::stream::mjpeg::ClientGuard; +use crate::utils::{bind_socket_addr, bind_tcp_listener}; use crate::video::codec::{BitratePreset, VideoCodecType}; use crate::video::stream_manager::VideoStreamManager; @@ -108,15 +109,20 @@ impl VncService { return Err(err); } - let bind_addr: SocketAddr = format!("{}:{}", config.bind, config.port) - .parse() + let bind_addr = bind_socket_addr(&config.bind, config.port) .map_err(|e| AppError::BadRequest(format!("Invalid VNC bind address: {}", e)))?; - let listener = TcpListener::bind(bind_addr).await.map_err(|e| { + let listener = bind_tcp_listener(bind_addr).map_err(|e| { AppError::Io(std::io::Error::new( e.kind(), format!("VNC bind failed: {}", e), )) })?; + let listener = TcpListener::from_std(listener).map_err(|e| { + AppError::Io(std::io::Error::new( + e.kind(), + format!("VNC listener setup failed: {}", e), + )) + })?; let config_ref = self.config.clone(); let video_manager = self.video_manager.clone(); diff --git a/src/web/handlers/config/types.rs b/src/web/handlers/config/types.rs index a6193650..b4ac5546 100644 --- a/src/web/handlers/config/types.rs +++ b/src/web/handlers/config/types.rs @@ -1335,4 +1335,82 @@ mod tests { }; assert!(update.validate().is_err()); } + + #[test] + fn ipv6_bind_vnc_config_accepts_ipv6_literals() { + for bind in ["::", "::1", "2001:db8::1"] { + let update = VncConfigUpdate { + enabled: None, + bind: Some(bind.to_string()), + port: Some(5900), + encoding: None, + jpeg_quality: None, + allow_one_client: None, + password: None, + }; + assert!( + update.validate().is_ok(), + "bind address should pass: {bind}" + ); + } + } + + #[test] + fn ipv6_bind_vnc_config_rejects_non_ip_or_socket_syntax() { + for bind in ["localhost", "127.0.0.1:5900", "[::1]:5900"] { + let update = VncConfigUpdate { + enabled: None, + bind: Some(bind.to_string()), + port: Some(5900), + encoding: None, + jpeg_quality: None, + allow_one_client: None, + password: None, + }; + assert!( + update.validate().is_err(), + "bind address should fail: {bind}" + ); + } + } + + #[test] + fn ipv6_bind_rtsp_config_accepts_ipv6_literals() { + for bind in ["::", "::1", "2001:db8::1"] { + let update = RtspConfigUpdate { + enabled: None, + bind: Some(bind.to_string()), + port: Some(8554), + path: None, + allow_one_client: None, + codec: None, + username: None, + password: None, + }; + assert!( + update.validate().is_ok(), + "bind address should pass: {bind}" + ); + } + } + + #[test] + fn ipv6_bind_rtsp_config_rejects_non_ip_or_socket_syntax() { + for bind in ["localhost", "127.0.0.1:8554", "[::1]:8554"] { + let update = RtspConfigUpdate { + enabled: None, + bind: Some(bind.to_string()), + port: Some(8554), + path: None, + allow_one_client: None, + codec: None, + username: None, + password: None, + }; + assert!( + update.validate().is_err(), + "bind address should fail: {bind}" + ); + } + } }