CH-J Toolsbainisteoireacht freastalaí SSH
Roghchlár
Foinse foilsithe

CH-J Tools

Brabhsáil comhadlanna agus comhaid chun feidhmchlár áirithe a scaoileadh.

Íosluchtaigh foinse ZIP
CH-J / Server Manager 1704 Comhaid
0.1.2-beta Suimigh: · Tógtha:
dns.rs Suimigh: Dáta gan taifeadadh 26.8 KB · 610 líne
Cód Íosluchtaigh comhad
1//! DNS diagnostics (port of `diagnostics/dnsDiagnostics.js`): OS resolver for address
2//! lookups plus an own UDP/TCP wire client for record queries (A/AAAA/CNAME/MX/TXT/NS/SOA/
3//! PTR/SRV/CAA/DS/DNSKEY), TCP fallback on truncation, CNAME chains and consistency checks.
4//! DNSSEC is reported as "not-validated"; AD/DS/DNSKEY are observations only.
6use crate::core::diagnostics::common::{error_result, hostname, map_limit, now_ms, DiagError, Options};
7use rand::RngCore;
8use serde_json::{json, Value};
9use std::collections::HashSet;
10use std::net::{IpAddr, Ipv6Addr, SocketAddr};
11use std::time::Duration;
12use tokio::io::{AsyncReadExt, AsyncWriteExt};
13use tokio_util::sync::CancellationToken;
15pub const TYPES: [(&str, u16); 12] = [
16 ("A", 1), ("NS", 2), ("CNAME", 5), ("SOA", 6), ("PTR", 12), ("MX", 15), ("TXT", 16), ("AAAA", 28), ("SRV", 33), ("DS", 43), ("DNSKEY", 48), ("CAA", 257),
17];
19pub fn type_code(name: &str) -> u16 {
20 TYPES.iter().find(|(n, _)| *n == name).map(|(_, c)| *c).unwrap_or(0)
23fn type_name(code: u16) -> Option<&'static str> {
24 TYPES.iter().find(|(_, c)| *c == code).map(|(n, _)| *n)
27pub fn provider(name: &str) -> Option<[&'static str; 2]> {
28 match name {
29 "cloudflare" => Some(["1.1.1.1", "2606:4700:4700::1111"]),
30 "google" => Some(["8.8.8.8", "2001:4860:4860::8888"]),
31 "quad9" => Some(["9.9.9.9", "2620:fe::fe"]),
32 _ => None,
33 }
36fn malformed() -> DiagError {
37 DiagError::new("DNS_MALFORMED")
40fn u16_at(b: &[u8], i: usize) -> Result<u16, DiagError> {
41 b.get(i..i + 2).map(|s| u16::from_be_bytes([s[0], s[1]])).ok_or_else(malformed)
44fn u32_at(b: &[u8], i: usize) -> Result<u32, DiagError> {
45 b.get(i..i + 4).map(|s| u32::from_be_bytes([s[0], s[1], s[2], s[3]])).ok_or_else(malformed)
48pub fn read_name(buffer: &[u8], position: usize) -> Result<(String, usize), DiagError> {
49 let mut cursor = position;
50 let mut end: Option<usize> = None;
51 let mut labels: Vec<String> = Vec::new();
52 let mut seen = HashSet::new();
53 for _ in 0..128 {
54 if cursor >= buffer.len() || !seen.insert(cursor) {
55 return Err(malformed());
56 }
57 let length = buffer[cursor] as usize;
58 cursor += 1;
59 if length & 0xc0 == 0xc0 {
60 if cursor >= buffer.len() {
61 return Err(malformed());
62 }
63 end.get_or_insert(cursor + 1);
64 cursor = ((length & 0x3f) << 8) | buffer[cursor] as usize;
65 continue;
66 }
67 if length & 0xc0 != 0 || length > 63 || cursor + length > buffer.len() {
68 return Err(malformed());
69 }
70 if length == 0 {
71 return Ok((labels.join("."), end.unwrap_or(cursor)));
72 }
73 labels.push(buffer[cursor..cursor + length].iter().map(|b| *b as char).collect());
74 cursor += length;
75 }
76 Err(malformed())
79pub fn parse_response(buffer: &[u8], id: u16, qtype: &str) -> Result<Value, DiagError> {
80 if buffer.len() < 12 || u16_at(buffer, 0)? != id || buffer[2] & 0x80 == 0 {
81 return Err(malformed());
82 }
83 let rcode = buffer[3] & 15;
84 let status = match rcode { 0 => "success", 2 => "servfail", 3 => "nxdomain", 5 => "refused", _ => "error" };
85 let questions = u16_at(buffer, 4)?;
86 let answers = u16_at(buffer, 6)? as usize;
87 let total = answers + u16_at(buffer, 8)? as usize + u16_at(buffer, 10)? as usize;
88 if questions != 1 || total > 1024 {
89 return Err(malformed());
90 }
91 let (question_name, mut cursor) = read_name(buffer, 12)?;
92 if cursor + 4 > buffer.len() || u16_at(buffer, cursor)? != type_code(qtype) || u16_at(buffer, cursor + 2)? != 1 {
93 return Err(malformed());
94 }
95 cursor += 4;
96 let mut records = Vec::new();
97 for index in 0..total {
98 let (owner, next) = read_name(buffer, cursor)?;
99 cursor = next;
100 if cursor + 10 > buffer.len() {
101 return Err(malformed());
102 }
103 let number = u16_at(buffer, cursor)?;
104 let class = u16_at(buffer, cursor + 2)?;
105 let ttl = u32_at(buffer, cursor + 4)?;
106 let length = u16_at(buffer, cursor + 8)? as usize;
107 cursor += 10;
108 let end = cursor + length;
109 if end > buffer.len() {
110 return Err(malformed());
111 }
112 let data = &buffer[cursor..end];
113 let value: Option<Value> = match number {
114 1 if length == 4 => Some(json!(data.iter().map(|b| b.to_string()).collect::<Vec<_>>().join("."))),
115 28 if length == 16 => Some(json!((0..8).map(|n| format!("{:x}", u16::from_be_bytes([data[n * 2], data[n * 2 + 1]]))).collect::<Vec<_>>().join(":"))),
116 2 | 5 | 12 => Some(json!(read_name(buffer, cursor)?.0)),
117 15 if length >= 3 => Some(json!({ "priority": u16_at(buffer, cursor)?, "exchange": read_name(buffer, cursor + 2)?.0 })),
118 33 if length >= 7 => Some(json!({ "priority": u16_at(buffer, cursor)?, "weight": u16_at(buffer, cursor + 2)?, "port": u16_at(buffer, cursor + 4)?, "name": read_name(buffer, cursor + 6)?.0 })),
119 16 => {
120 let mut parts = Vec::new();
121 let mut p = cursor;
122 while p < end {
123 let size = buffer[p] as usize;
124 p += 1;
125 if p + size > end {
126 return Err(malformed());
127 }
128 parts.push(Value::String(String::from_utf8_lossy(&buffer[p..p + size]).to_string()));
129 p += size;
130 }
131 Some(Value::Array(parts))
132 }
133 6 => {
134 let (ns, after_ns) = read_name(buffer, cursor)?;
135 let (host, after_host) = read_name(buffer, after_ns)?;
136 if after_host + 20 > end {
137 return Err(malformed());
138 }
139 Some(json!({
140 "nsname": ns, "hostmaster": host,
141 "serial": u32_at(buffer, after_host)?, "refresh": u32_at(buffer, after_host + 4)?, "retry": u32_at(buffer, after_host + 8)?,
142 "expire": u32_at(buffer, after_host + 12)?, "minttl": u32_at(buffer, after_host + 16)?
143 }))
144 }
145 43 if length >= 4 => Some(json!({ "keyTag": u16_at(buffer, cursor)?, "algorithm": data[2], "digestType": data[3], "digest": hex::encode(&data[4..]) })),
146 48 if length >= 4 => {
147 use base64::Engine;
148 Some(json!({ "flags": u16_at(buffer, cursor)?, "protocol": data[2], "algorithm": data[3], "publicKey": base64::engine::general_purpose::STANDARD.encode(&data[4..]) }))
149 }
150 257 if length >= 2 && 2 + data[1] as usize <= length => {
151 let tag_len = data[1] as usize;
152 Some(json!({ "flags": data[0], "tag": String::from_utf8_lossy(&data[2..2 + tag_len]), "value": String::from_utf8_lossy(&data[2 + tag_len..]) }))
153 }
154 _ => None,
155 };
156 if let (Some(name), 1, Some(value)) = (type_name(number), class, value) {
157 records.push(json!({ "name": owner, "type": name, "ttl": ttl, "value": value, "section": if index < answers { "answer" } else { "authority/additional" } }));
158 }
159 cursor = end;
160 }
161 let has_answer = records.iter().any(|r| r["type"] == qtype && r["section"] == "answer");
162 Ok(json!({
163 "questionName": question_name,
164 "status": if status == "success" && !has_answer { "nodata" } else { status },
165 "records": records,
166 "truncated": buffer[2] & 2 != 0,
167 "dnssec": "not-validated",
168 "resolverAdFlag": buffer[3] & 32 != 0
169 }))
172/// Reverse lookup name; IPv6 is expanded to 32 nibbles (embedded IPv4 becomes hex groups).
173pub fn reverse_name(address: &str) -> Result<String, DiagError> {
174 match address.parse::<IpAddr>() {
175 Ok(IpAddr::V4(v4)) => Ok(format!("{}.in-addr.arpa", v4.octets().iter().rev().map(|o| o.to_string()).collect::<Vec<_>>().join("."))),
176 Ok(IpAddr::V6(v6)) => {
177 let hex = v6.octets().iter().map(|b| format!("{b:02x}")).collect::<String>();
178 Ok(format!("{}.ip6.arpa", hex.chars().rev().map(|c| c.to_string()).collect::<Vec<_>>().join(".")))
179 }
180 Err(_) => Err(DiagError::new("INVALID_IP")),
181 }
184pub fn server_address(value: &str) -> Result<SocketAddr, DiagError> {
185 if let Some(rest) = value.strip_prefix('[') {
186 let (host, port) = rest.split_once(']').ok_or_else(|| DiagError::new("INVALID_DNS_SERVER"))?;
187 let ip: Ipv6Addr = host.parse().map_err(|_| DiagError::new("INVALID_DNS_SERVER"))?;
188 let port = port.strip_prefix(':').map(|p| p.parse().map_err(|_| DiagError::new("INVALID_DNS_SERVER"))).transpose()?.unwrap_or(53);
189 return Ok(SocketAddr::new(IpAddr::V6(ip), port));
190 }
191 if let Ok(ip) = value.parse::<IpAddr>() {
192 return Ok(SocketAddr::new(ip, 53));
193 }
194 if let Some((host, port)) = value.rsplit_once(':') {
195 if let (Ok(IpAddr::V4(ip)), Ok(port)) = (host.parse::<IpAddr>(), port.parse::<u16>()) {
196 return Ok(SocketAddr::new(IpAddr::V4(ip), port));
197 }
198 }
199 Err(DiagError::new("INVALID_DNS_SERVER"))
202async fn exchange(packet: &[u8], server: &str, cancel: &CancellationToken, tcp: bool) -> Result<Vec<u8>, DiagError> {
203 if cancel.is_cancelled() {
204 return Err(DiagError::new("CANCELLED"));
205 }
206 let address = server_address(server)?;
207 let work = async {
208 if tcp {
209 let mut stream = tokio::net::TcpStream::connect(address).await.map_err(DiagError::from_io)?;
210 let mut framed = (packet.len() as u16).to_be_bytes().to_vec();
211 framed.extend_from_slice(packet);
212 stream.write_all(&framed).await.map_err(DiagError::from_io)?;
213 let mut size = [0u8; 2];
214 stream.read_exact(&mut size).await.map_err(|_| DiagError::new("DNS_INCOMPLETE"))?;
215 let mut response = vec![0u8; u16::from_be_bytes(size) as usize];
216 stream.read_exact(&mut response).await.map_err(|_| DiagError::new("DNS_INCOMPLETE"))?;
217 Ok(response)
218 } else {
219 let bind: SocketAddr = if address.is_ipv6() { "[::]:0".parse().unwrap() } else { "0.0.0.0:0".parse().unwrap() };
220 let socket = tokio::net::UdpSocket::bind(bind).await.map_err(DiagError::from_io)?;
221 socket.connect(address).await.map_err(DiagError::from_io)?;
222 socket.send(packet).await.map_err(DiagError::from_io)?;
223 let mut buffer = vec![0u8; 65535];
224 let n = socket.recv(&mut buffer).await.map_err(DiagError::from_io)?;
225 buffer.truncate(n);
226 Ok(buffer)
227 }
228 };
229 tokio::select! {
230 result = work => result,
231 _ = cancel.cancelled() => Err(DiagError::new("CANCELLED")),
232 }
235/// Configured system DNS servers (resolv.conf / Windows adapter settings).
236pub fn system_servers() -> Vec<String> {
237 match hickory_resolver::system_conf::read_system_conf() {
238 Ok((config, _)) => {
239 let mut servers: Vec<String> = Vec::new();
240 for ns in config.name_servers() {
241 let ip = ns.socket_addr.ip().to_string();
242 if !servers.contains(&ip) {
243 servers.push(ip);
244 }
245 }
246 servers
247 }
248 Err(_) => Vec::new(),
249 }
252fn servers(options: &Options) -> Vec<String> {
253 if options.resolver == "custom" {
254 return vec![options.custom_dns.clone()];
255 }
256 if let Some(list) = provider(&options.resolver) {
257 return list.iter().map(|s| s.to_string()).collect();
258 }
259 system_servers()
262pub async fn query(name: &str, qtype: &str, options: &Options, cancel: &CancellationToken) -> Result<Value, DiagError> {
263 let started = now_ms();
264 let query_name = hostname(name)?;
265 let mut id_bytes = [0u8; 2];
266 rand::thread_rng().fill_bytes(&mut id_bytes);
267 let id = u16::from_be_bytes(id_bytes);
268 let mut packet = Vec::with_capacity(64);
269 packet.extend_from_slice(&id.to_be_bytes());
270 packet.extend_from_slice(&[0x01, 0x00, 0, 1, 0, 0, 0, 0, 0, 0]);
271 for part in query_name.split('.').filter(|p| !p.is_empty()) {
272 packet.push(part.len() as u8);
273 packet.extend_from_slice(part.as_bytes());
274 }
275 let code = type_code(qtype);
276 packet.extend_from_slice(&[0, (code >> 8) as u8, (code & 0xff) as u8, 0, 1]);
277 let limit = cancel.child_token();
278 let deadline = tokio::time::sleep(Duration::from_millis(options.timeout_ms));
279 tokio::pin!(deadline);
280 let mut failure: Option<DiagError> = None;
281 for server in servers(options) {
282 let attempt = async {
283 let mut response = exchange(&packet, &server, &limit, false).await?;
284 if response.len() > 2 && response[2] & 2 != 0 {
285 response = exchange(&packet, &server, &limit, true).await?;
286 }
287 let result = parse_response(&response, id, qtype)?;
288 if !result["questionName"].as_str().unwrap_or("").is_empty() && result["questionName"].as_str() != Some(query_name.as_str()) {
289 return Err(malformed());
290 }
291 Ok(result)
292 };
293 let outcome = tokio::select! {
294 result = attempt => result,
295 _ = &mut deadline => { limit.cancel(); Err(DiagError::new("TIMEOUT")) }
296 };
297 match outcome {
298 Ok(result) => {
299 let mut value = json!({ "name": query_name, "type": qtype, "server": server, "durationMs": now_ms() - started });
300 if let (Some(target), Value::Object(fields)) = (value.as_object_mut(), result) {
301 target.extend(fields);
302 }
303 return Ok(value);
304 }
305 Err(error) => {
306 let stop = error.code == "TIMEOUT" || cancel.is_cancelled();
307 failure = Some(error);
308 if stop {
309 break;
310 }
311 }
312 }
313 }
314 Err(failure.unwrap_or_else(|| DiagError::new("DNS_UNAVAILABLE")))
317pub async fn system_lookup(host: &str, mode: &str, timeout_ms: u64, cancel: &CancellationToken) -> Result<Vec<Value>, DiagError> {
318 let started = now_ms();
319 let lookup = tokio::net::lookup_host((host.to_string(), 0u16));
320 let addresses = tokio::select! {
321 result = tokio::time::timeout(Duration::from_millis(timeout_ms), lookup) => match result {
322 Ok(Ok(list)) => list.collect::<Vec<_>>(),
323 Ok(Err(error)) => return Err(DiagError::with_message("ENOTFOUND", error.to_string())),
324 Err(_) => return Err(DiagError::new("TIMEOUT")),
325 },
326 _ = cancel.cancelled() => return Err(DiagError::new("CANCELLED")),
327 };
328 let mut seen = HashSet::new();
329 Ok(addresses
330 .into_iter()
331 .map(|a| a.ip())
332 .filter(|ip| match mode { "ipv4" => ip.is_ipv4(), "ipv6" => ip.is_ipv6(), _ => true })
333 .filter(|ip| seen.insert(*ip))
334 .map(|ip| json!({ "address": ip.to_string(), "family": if ip.is_ipv4() { 4 } else { 6 }, "dnsMs": now_ms() - started, "method": "OS resolver (hosts file / system DNS); TTL unavailable" }))
335 .collect())
338pub async fn resolve(host: &str, options: &Options, cancel: &CancellationToken) -> Result<Vec<Value>, DiagError> {
339 if cancel.is_cancelled() {
340 return Err(DiagError::new("CANCELLED"));
341 }
342 if let Ok(ip) = host.parse::<IpAddr>() {
343 return Ok(vec![json!({ "address": host, "family": if ip.is_ipv4() { 4 } else { 6 }, "dnsMs": 0 })]);
344 }
345 if options.resolver == "system" {
346 let list = system_lookup(host, &options.mode, options.timeout_ms, cancel).await?;
347 if list.is_empty() {
348 return Err(DiagError::new("DNS_NO_ADDRESS"));
349 }
350 return Ok(list);
351 }
352 let types: Vec<&str> = match options.mode.as_str() { "ipv4" => vec!["A"], "ipv6" => vec!["AAAA"], _ => vec!["A", "AAAA"] };
353 let mut all = Vec::new();
354 for qtype in types {
355 let lookup = async {
356 let mut name = host.to_string();
357 let mut duration = 0.0;
358 let mut seen = HashSet::new();
359 for _ in 0..8 {
360 if !seen.insert(name.clone()) {
361 return Err(DiagError::new("DNS_CNAME_LOOP"));
362 }
363 let result = query(&name, qtype, options, cancel).await?;
364 duration += result["durationMs"].as_f64().unwrap_or(0.0);
365 let records = result["records"].as_array().cloned().unwrap_or_default();
366 let addresses: Vec<Value> = records
367 .iter()
368 .filter(|r| r["type"] == qtype && r["section"] == "answer")
369 .map(|r| json!({ "address": r["value"], "family": if qtype == "A" { 4 } else { 6 }, "dnsMs": duration, "ttl": r["ttl"] }))
370 .collect();
371 if !addresses.is_empty() {
372 return Ok(addresses);
373 }
374 match records.iter().find(|r| r["type"] == "CNAME" && r["section"] == "answer") {
375 Some(cname) => name = cname["value"].as_str().unwrap_or("").to_string(),
376 None => return Ok(Vec::new()),
377 }
378 }
379 Err(DiagError::new("DNS_CNAME_LIMIT"))
380 };
381 match lookup.await {
382 Ok(list) => all.extend(list),
383 Err(error) => {
384 if cancel.is_cancelled() {
385 return Err(error);
386 }
387 }
388 }
389 }
390 if all.is_empty() {
391 return Err(DiagError::new("DNS_NO_ADDRESS"));
392 }
393 Ok(all)
396/// Fallback for SSH name resolution when the OS resolver returns nothing (like Node resolve4/resolve6).
397pub async fn resolve_both(host: &str) -> Vec<IpAddr> {
398 let options = Options { resolver: "system-wire".into(), mode: "both".into(), timeout_ms: 5000, ..Options::default() };
399 let cancel = CancellationToken::new();
400 let mut result = Vec::new();
401 for qtype in ["A", "AAAA"] {
402 if let Ok(answer) = query(host, qtype, &options, &cancel).await {
403 for record in answer["records"].as_array().cloned().unwrap_or_default() {
404 if record["type"] == qtype && record["section"] == "answer" {
405 if let Some(ip) = record["value"].as_str().and_then(|v| v.parse().ok()) {
406 result.push(ip);
407 }
408 }
409 }
410 }
411 }
412 result
415fn canonical_ip(value: &str) -> String {
416 value.parse::<IpAddr>().map(|ip| ip.to_string()).unwrap_or_else(|_| value.to_string())
419pub async fn run(host: &str, options: &Options, cancel: &CancellationToken, progress: &(dyn Fn(Value) + Send + Sync)) -> Result<Value, DiagError> {
420 let is_ip = host.parse::<IpAddr>().is_ok();
421 let name = if is_ip { reverse_name(host)? } else { host.to_string() };
422 let types: Vec<String> = if is_ip { vec!["PTR".to_string()] } else { TYPES.iter().map(|(n, _)| n.to_string()).collect() };
423 let queries: Vec<Value> = map_limit(types, 3, |qtype: String| {
424 let name = name.clone();
425 async move {
426 let qtype = qtype.as_str();
427 if cancel.is_cancelled() {
428 return Err(DiagError::new("CANCELLED"));
429 }
430 let result = match query(&name, qtype, options, cancel).await {
431 Ok(result) => result,
432 Err(error) => {
433 let mut value = json!({ "type": qtype, "name": name });
434 if let (Some(t), Value::Object(e)) = (value.as_object_mut(), error_result(&error)) {
435 t.extend(e);
436 }
437 value
438 }
439 };
440 let mut event = json!({ "kind": "dns-record" });
441 if let (Some(t), Some(r)) = (event.as_object_mut(), result.as_object()) {
442 t.extend(r.clone());
443 }
444 progress(event);
445 Ok(result)
446 }
447 })
448 .await?;
449 let mut cnames: Vec<Value> = queries
450 .iter()
451 .flat_map(|q| q["records"].as_array().cloned().unwrap_or_default())
452 .filter(|r| r["type"] == "CNAME" && r["section"] == "answer")
453 .collect();
454 let mut seen: HashSet<String> = HashSet::from([host.to_string()]);
455 let mut next = cnames.iter().find(|r| r["name"] == host).and_then(|r| r["value"].as_str().map(str::to_string));
456 let mut cname_error: Option<String> = None;
457 let mut index = 0;
458 while let Some(current) = next.clone() {
459 if index >= 8 {
460 break;
461 }
462 index += 1;
463 if cancel.is_cancelled() {
464 return Err(DiagError::new("CANCELLED"));
465 }
466 if !seen.insert(current.clone()) {
467 cname_error = Some("DNS_CNAME_LOOP".into());
468 break;
469 }
470 match query(&current, "CNAME", options, cancel).await {
471 Ok(answer) => {
472 let record = answer["records"].as_array().and_then(|r| r.iter().find(|r| r["type"] == "CNAME" && r["section"] == "answer" && r["name"] == current.as_str()).cloned());
473 match record {
474 None => {
475 next = None;
476 break;
477 }
478 Some(record) => {
479 next = record["value"].as_str().map(str::to_string);
480 cnames.push(record);
481 }
482 }
483 }
484 Err(error) => {
485 if cancel.is_cancelled() {
486 return Err(error);
487 }
488 cname_error = Some(error.code.clone());
489 break;
490 }
491 }
492 }
493 if next.is_some() && cname_error.is_none() {
494 cname_error = Some("DNS_CNAME_LIMIT".into());
495 }
496 let mut consistency = Vec::new();
497 if !is_ip {
498 for qtype in ["A", "AAAA"] {
499 if cancel.is_cancelled() {
500 return Err(DiagError::new("CANCELLED"));
501 }
502 let first = queries.iter().find(|q| q["type"] == qtype).cloned().unwrap_or(json!({}));
503 match query(host, qtype, options, cancel).await {
504 Ok(second) => {
505 let values = |q: &Value| {
506 let mut list: Vec<String> = q["records"]
507 .as_array()
508 .cloned()
509 .unwrap_or_default()
510 .iter()
511 .filter(|r| (r["type"] == qtype || r["type"] == "CNAME") && r["section"] == "answer")
512 .map(|r| json!([r["name"], r["type"], r["value"]]).to_string())
513 .collect();
514 list.sort();
515 list
516 };
517 let (a, b) = (values(&first), values(&second));
518 consistency.push(json!({ "type": qtype, "status": if first["status"] == second["status"] && a == b { "consistent" } else { "changed" }, "first": a, "second": b }));
519 }
520 Err(error) => {
521 if cancel.is_cancelled() {
522 return Err(error);
523 }
524 let mut entry = json!({ "type": qtype });
525 if let (Some(t), Value::Object(e)) = (entry.as_object_mut(), error_result(&error)) {
526 t.extend(e);
527 }
528 consistency.push(entry);
529 }
530 }
531 }
532 } else {
533 let family_mode = if host.parse::<IpAddr>().map(|ip| ip.is_ipv4()).unwrap_or(true) { "ipv4" } else { "ipv6" };
534 let ptrs: Vec<Value> = queries.first().and_then(|q| q["records"].as_array().cloned()).unwrap_or_default().into_iter().filter(|r| r["type"] == "PTR").take(4).collect();
535 for record in ptrs {
536 let target = record["value"].as_str().unwrap_or("").to_string();
537 let options = Options { mode: family_mode.into(), ..options.clone() };
538 match resolve(&target, &options, cancel).await {
539 Ok(answers) => {
540 let matches = answers.iter().any(|a| canonical_ip(a["address"].as_str().unwrap_or("")) == canonical_ip(host));
541 consistency.push(json!({ "type": "forward-confirmed-PTR", "name": target, "status": if matches { "consistent" } else { "changed" } }));
542 }
543 Err(error) => {
544 if cancel.is_cancelled() {
545 return Err(error);
546 }
547 let mut entry = json!({ "type": "forward-confirmed-PTR", "name": target });
548 if let (Some(t), Value::Object(e)) = (entry.as_object_mut(), error_result(&error)) {
549 t.extend(e);
550 }
551 consistency.push(entry);
552 }
553 }
554 }
555 }
556 let mut chain: Vec<String> = Vec::new();
557 for record in &cnames {
558 let line = format!("{} → {}", record["name"].as_str().unwrap_or(""), record["value"].as_str().unwrap_or(""));
559 if !chain.contains(&line) {
560 chain.push(line);
561 }
562 }
563 Ok(json!({
564 "status": if queries.iter().any(|q| q["status"] == "success") { "success" } else { "warning" },
565 "queries": queries,
566 "cnameChain": chain,
567 "cnameError": cname_error,
568 "consistency": consistency,
569 "dnssec": "not-validated",
570 "note": "Repeated answers / forward-confirmed PTR from the selected resolver. Changes can reflect load balancing; this is not authoritative consistency or DNSSEC validation."
571 }))
574#[cfg(test)]
575mod tests {
576 use super::*;
578 #[test]
579 fn reverse_names() {
580 assert_eq!(reverse_name("192.0.2.1").unwrap(), "1.2.0.192.in-addr.arpa");
581 assert!(reverse_name("2001:db8::1").unwrap().starts_with("1.0.0.0.0.0.0.0"));
582 assert!(reverse_name("2001:db8::1").unwrap().ends_with("8.b.d.0.1.0.0.2.ip6.arpa"));
583 assert_eq!(reverse_name("::ffff:1.2.3.4").unwrap().split('.').take(8).collect::<Vec<_>>(), vec!["4", "0", "3", "0", "2", "0", "1", "0"]);
584 }
586 #[test]
587 fn parses_a_response_with_compression() {
588 // Query id 0x1234 for example.com A, one answer 93.184.216.34 with a compression pointer.
589 let mut packet = vec![0x12, 0x34, 0x81, 0x80, 0, 1, 0, 1, 0, 0, 0, 0];
590 packet.extend_from_slice(&[7, b'e', b'x', b'a', b'm', b'p', b'l', b'e', 3, b'c', b'o', b'm', 0, 0, 1, 0, 1]);
591 packet.extend_from_slice(&[0xc0, 0x0c, 0, 1, 0, 1, 0, 0, 0x0e, 0x10, 0, 4, 93, 184, 216, 34]);
592 let parsed = parse_response(&packet, 0x1234, "A").unwrap();
593 assert_eq!(parsed["status"], "success");
594 assert_eq!(parsed["records"][0]["value"], "93.184.216.34");
595 assert_eq!(parsed["records"][0]["ttl"], 3600);
596 assert!(parse_response(&packet, 0x9999, "A").is_err());
597 let mut looped = packet.clone();
598 looped[12] = 0xc0;
599 looped[13] = 0x0c;
600 assert!(parse_response(&looped, 0x1234, "A").is_err());
601 }
603 #[test]
604 fn server_addresses() {
605 assert_eq!(server_address("1.1.1.1").unwrap().port(), 53);
606 assert_eq!(server_address("[2606:4700:4700::1111]:5353").unwrap().port(), 5353);
607 assert_eq!(server_address("127.0.0.1:5300").unwrap().port(), 5300);
608 assert!(server_address("example.com").is_err());
609 }
SHA-256f0ba20e04c36023be244ab166a7c066b20db2c80e36691fa83c1403a4755ba93

CH-J Proprietary Software License 1.14 · Tá Foinse ar fáil faoin CH-J Proprietary Software License 1.14. Ní chuireann a infhaighteacht a athrú ar na téarmaí ceadúnais nó cearta breise a dheonú.

Cartlann SHA-256ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
Comhaid