From cc80bd2d379b4093c74a447dc103489b2b96d39f Mon Sep 17 00:00:00 2001 From: lbl <1791778603@qq.com> Date: Fri, 21 Aug 2026 02:45:46 +0800 Subject: [PATCH] =?UTF-8?q?fix(dns):=20=E9=9D=9E=E6=B3=95=E5=9F=9F?= =?UTF-8?q?=E5=90=8D=E4=B8=8D=E5=86=8D=E5=AF=BC=E8=87=B4=20panic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 排查发现比报告更深一层的问题:dns-parser 0.8 的 add_question 对 超长 label 直接 assert panic(builder.build() 的 unwrap 只是第二层), 配置中的非法域名(如 label 超 63 字节)会使整个进程 panic。 修复: - query() 调用前先经 is_valid_domain 校验(label 非空 ≤63 字节、 全长 ≤253 字节,允许 FQDN 尾点),非法域名返回 InvalidInput。 - build().unwrap() 同步改为 map_err 作为纵深防御。 测试:非法域名 query 返回 InvalidInput 而非 panic; is_valid_domain 合法/非法样例。 --- vnt-core/src/utils/dns_query.rs | 63 ++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/vnt-core/src/utils/dns_query.rs b/vnt-core/src/utils/dns_query.rs index e26bd7b..a662e59 100644 --- a/vnt-core/src/utils/dns_query.rs +++ b/vnt-core/src/utils/dns_query.rs @@ -121,6 +121,18 @@ pub async fn dns_query_all( } } +/// 校验域名格式:label 非空且不超过 63 字节,全长不超过 253 字节。 +/// dns-parser 的 add_question 对非法 label 直接 assert panic, +/// 必须在调用前拦截 +fn is_valid_domain(domain: &str) -> bool { + let domain = domain.strip_suffix('.').unwrap_or(domain); + !domain.is_empty() + && domain.len() <= 253 + && domain + .split('.') + .all(|label| !label.is_empty() && label.len() <= 63) +} + async fn query<'a>( udp: &UdpSocket, domain: &str, @@ -128,9 +140,21 @@ async fn query<'a>( record_type: QueryType, buf: &'a mut [u8], ) -> io::Result> { + if !is_valid_domain(domain) { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("invalid domain {domain:?}"), + )); + } let mut builder = Builder::new_query(1, true); builder.add_question(domain, false, record_type, QueryClass::IN); - let packet = builder.build().unwrap(); + // 非法域名(如 label 超长)build 会失败,不能 unwrap panic + let packet = builder.build().map_err(|e| { + io::Error::new( + io::ErrorKind::InvalidInput, + format!("invalid domain {domain:?}: {e:?}"), + ) + })?; udp.connect(name_server).await?; let mut count = 0; @@ -249,3 +273,40 @@ pub async fn aaaa_dns( } Ok(rs) } + + +#[cfg(test)] +mod tests { + use super::*; + + /// 非法域名(label 超过 63 字节)必须返回错误而不是 panic + #[tokio::test] + async fn test_query_invalid_domain_no_panic() { + let udp = UdpSocket::bind("0.0.0.0:0").await.unwrap(); + let mut buf = vec![0u8; 512]; + let bad_domain = format!("{}.com", "a".repeat(64)); + let rs = query( + &udp, + &bad_domain, + "127.0.0.1:53".parse().unwrap(), + QueryType::A, + &mut buf, + ) + .await; + let err = rs.expect_err("invalid domain must be rejected"); + assert_eq!(err.kind(), io::ErrorKind::InvalidInput); + } + + #[test] + fn test_is_valid_domain() { + assert!(is_valid_domain("example.com")); + assert!(is_valid_domain("a-b_1.example.com")); + assert!(is_valid_domain("example.com.")); // FQDN 尾点合法 + assert!(is_valid_domain(&format!("{}.com", "a".repeat(63)))); + + assert!(!is_valid_domain("")); + assert!(!is_valid_domain(&format!("{}.com", "a".repeat(64)))); + assert!(!is_valid_domain("a..b")); + assert!(!is_valid_domain(&"a".repeat(254))); + } +}