fix(dns): 非法域名不再导致 panic

排查发现比报告更深一层的问题: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 合法/非法样例。
This commit is contained in:
lbl
2026-08-21 02:45:46 +08:00
parent 90a00215ca
commit cc80bd2d37
+62 -1
View File
@@ -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<Packet<'a>> {
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)));
}
}