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:
@@ -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>(
|
async fn query<'a>(
|
||||||
udp: &UdpSocket,
|
udp: &UdpSocket,
|
||||||
domain: &str,
|
domain: &str,
|
||||||
@@ -128,9 +140,21 @@ async fn query<'a>(
|
|||||||
record_type: QueryType,
|
record_type: QueryType,
|
||||||
buf: &'a mut [u8],
|
buf: &'a mut [u8],
|
||||||
) -> io::Result<Packet<'a>> {
|
) -> 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);
|
let mut builder = Builder::new_query(1, true);
|
||||||
builder.add_question(domain, false, record_type, QueryClass::IN);
|
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?;
|
udp.connect(name_server).await?;
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
@@ -249,3 +273,40 @@ pub async fn aaaa_dns(
|
|||||||
}
|
}
|
||||||
Ok(rs)
|
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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user