1use serde::{Deserialize, Serialize};
2use tracing::{debug, instrument};
3
4use crate::dns::{DnsRecord, DnsResolver, RecordType};
5use crate::error::Result;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct ServerResult {
10 pub nameserver: String,
11 pub records: Vec<DnsRecord>,
12 pub error: Option<String>,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct DnsComparison {
21 pub domain: String,
22 pub record_type: RecordType,
23 pub server_a: ServerResult,
24 pub server_b: ServerResult,
25 pub matches: bool,
26 pub only_in_a: Vec<String>,
27 pub only_in_b: Vec<String>,
28 pub common: Vec<String>,
29}
30
31pub struct DnsComparator {
36 resolver: DnsResolver,
37}
38
39impl Default for DnsComparator {
40 fn default() -> Self {
41 Self::new()
42 }
43}
44
45impl DnsComparator {
46 pub fn new() -> Self {
48 Self {
49 resolver: DnsResolver::new(),
50 }
51 }
52
53 #[instrument(skip(self), fields(domain = %domain, record_type = %record_type, server_a = %server_a, server_b = %server_b))]
65 pub async fn compare(
66 &self,
67 domain: &str,
68 record_type: RecordType,
69 server_a: &str,
70 server_b: &str,
71 ) -> Result<DnsComparison> {
72 let domain = crate::validation::normalize_domain(domain)?;
73
74 let (result_a, result_b) = tokio::join!(
76 self.resolver.resolve(&domain, record_type, Some(server_a)),
77 self.resolver.resolve(&domain, record_type, Some(server_b))
78 );
79
80 let server_a_result = match result_a {
81 Ok(records) => ServerResult {
82 nameserver: server_a.to_string(),
83 records,
84 error: None,
85 },
86 Err(e) => {
87 debug!(server = %server_a, error = %e, "DNS compare query failed");
88 ServerResult {
89 nameserver: server_a.to_string(),
90 records: vec![],
91 error: Some(e.sanitized_message()),
93 }
94 }
95 };
96
97 let server_b_result = match result_b {
98 Ok(records) => ServerResult {
99 nameserver: server_b.to_string(),
100 records,
101 error: None,
102 },
103 Err(e) => {
104 debug!(server = %server_b, error = %e, "DNS compare query failed");
105 ServerResult {
106 nameserver: server_b.to_string(),
107 records: vec![],
108 error: Some(e.sanitized_message()),
110 }
111 }
112 };
113
114 let values_a: std::collections::HashSet<String> = server_a_result
116 .records
117 .iter()
118 .map(|r| r.format_short())
119 .collect();
120 let values_b: std::collections::HashSet<String> = server_b_result
121 .records
122 .iter()
123 .map(|r| r.format_short())
124 .collect();
125
126 let mut only_in_a: Vec<String> = values_a.difference(&values_b).cloned().collect();
127 let mut only_in_b: Vec<String> = values_b.difference(&values_a).cloned().collect();
128 let mut common: Vec<String> = values_a.intersection(&values_b).cloned().collect();
129
130 only_in_a.sort();
132 only_in_b.sort();
133 common.sort();
134
135 let matches = only_in_a.is_empty()
136 && only_in_b.is_empty()
137 && server_a_result.error.is_none()
138 && server_b_result.error.is_none();
139
140 debug!(
141 matches = matches,
142 common = common.len(),
143 only_in_a = only_in_a.len(),
144 only_in_b = only_in_b.len(),
145 "DNS comparison complete"
146 );
147
148 Ok(DnsComparison {
149 domain: domain.to_string(),
150 record_type,
151 server_a: server_a_result,
152 server_b: server_b_result,
153 matches,
154 only_in_a,
155 only_in_b,
156 common,
157 })
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use crate::dns::{RecordData, RecordType};
165
166 #[test]
167 fn test_dns_comparison_serialization() {
168 let comparison = DnsComparison {
169 domain: "example.com".to_string(),
170 record_type: RecordType::A,
171 server_a: ServerResult {
172 nameserver: "8.8.8.8".to_string(),
173 records: vec![DnsRecord {
174 name: "example.com".to_string(),
175 record_type: RecordType::A,
176 ttl: 300,
177 data: RecordData::A {
178 address: "93.184.216.34".to_string(),
179 },
180 }],
181 error: None,
182 },
183 server_b: ServerResult {
184 nameserver: "1.1.1.1".to_string(),
185 records: vec![DnsRecord {
186 name: "example.com".to_string(),
187 record_type: RecordType::A,
188 ttl: 300,
189 data: RecordData::A {
190 address: "93.184.216.34".to_string(),
191 },
192 }],
193 error: None,
194 },
195 matches: true,
196 only_in_a: vec![],
197 only_in_b: vec![],
198 common: vec!["93.184.216.34".to_string()],
199 };
200
201 let json = serde_json::to_string(&comparison).unwrap();
202 assert!(json.contains("example.com"));
203 assert!(json.contains("93.184.216.34"));
204 assert!(json.contains("\"matches\":true"));
205 }
206
207 #[test]
208 fn test_server_result_with_error() {
209 let result = ServerResult {
210 nameserver: "8.8.8.8".to_string(),
211 records: vec![],
212 error: Some("connection timed out".to_string()),
213 };
214
215 let json = serde_json::to_string(&result).unwrap();
216 assert!(json.contains("connection timed out"));
217 }
218}