公開済みのソース
ソースの ZIP をダウンロード
CH-J Tools
特定のアプリケーションリリースのディレクトリとファイルを閲覧できます。
CH-J / Server Manager
1704 ファイル
0.1.2-beta
追加日時: · ビルド日時:
1
//! DNS diagnostics (port of `diagnostics/dnsDiagnostics.js`): OS resolver for address2
//! 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.6
use crate::core::diagnostics::common::{error_result, hostname, map_limit, now_ms, DiagError, Options};7
use rand::RngCore;8
use serde_json::{json, Value};9
use std::collections::HashSet;10
use std::net::{IpAddr, Ipv6Addr, SocketAddr};11
use std::time::Duration;12
use tokio::io::{AsyncReadExt, AsyncWriteExt};13
use tokio_util::sync::CancellationToken;15
pub 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
];19
pub fn type_code(name: &str) -> u16 {20
TYPES.iter().find(|(n, _)| *n == name).map(|(_, c)| *c).unwrap_or(0)21
}23
fn type_name(code: u16) -> Option<&'static str> {24
TYPES.iter().find(|(_, c)| *c == code).map(|(n, _)| *n)25
}27
pub 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
}34
}36
fn malformed() -> DiagError {37
DiagError::new("DNS_MALFORMED")38
}40
fn 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)42
}44
fn 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)46
}48
pub 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())77
}79
pub 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 != 0169
}))170
}172
/// Reverse lookup name; IPv6 is expanded to 32 nibbles (embedded IPv4 becomes hex groups).173
pub 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
}182
}184
pub 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"))200
}202
async 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
}233
}235
/// Configured system DNS servers (resolv.conf / Windows adapter settings).236
pub 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
servers247
}248
Err(_) => Vec::new(),249
}250
}252
fn 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()260
}262
pub 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")))315
}317
pub 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(addresses330
.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())336
}338
pub 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> = records367
.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)394
}396
/// Fallback for SSH name resolution when the OS resolver returns nothing (like Node resolve4/resolve6).397
pub 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
result413
}415
fn canonical_ip(value: &str) -> String {416
value.parse::<IpAddr>().map(|ip| ip.to_string()).unwrap_or_else(|_| value.to_string())417
}419
pub 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
value438
}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> = queries450
.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(¤t, "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
list516
};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
}))572
}574
#[cfg(test)]575
mod 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
}610
}SHA-256
f0ba20e04c36023be244ab166a7c066b20db2c80e36691fa83c1403a4755ba93CH-J Proprietary Software License 1.14 · ソースは CH-J Proprietary Software License 1.14 に基づいて提供されています。公開されていても、ライセンス条項が変わったり、追加の権利が付与されたりすることはありません。
アーカイブの SHA-256
ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f