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) => ServerResult {
87 nameserver: server_a.to_string(),
88 records: vec![],
89 error: Some(e.to_string()),
90 },
91 };
92
93 let server_b_result = match result_b {
94 Ok(records) => ServerResult {
95 nameserver: server_b.to_string(),
96 records,
97 error: None,
98 },
99 Err(e) => ServerResult {
100 nameserver: server_b.to_string(),
101 records: vec![],
102 error: Some(e.to_string()),
103 },
104 };
105
106 let values_a: std::collections::HashSet<String> = server_a_result
108 .records
109 .iter()
110 .map(|r| r.format_short())
111 .collect();
112 let values_b: std::collections::HashSet<String> = server_b_result
113 .records
114 .iter()
115 .map(|r| r.format_short())
116 .collect();
117
118 let mut only_in_a: Vec<String> = values_a.difference(&values_b).cloned().collect();
119 let mut only_in_b: Vec<String> = values_b.difference(&values_a).cloned().collect();
120 let mut common: Vec<String> = values_a.intersection(&values_b).cloned().collect();
121
122 only_in_a.sort();
124 only_in_b.sort();
125 common.sort();
126
127 let matches = only_in_a.is_empty()
128 && only_in_b.is_empty()
129 && server_a_result.error.is_none()
130 && server_b_result.error.is_none();
131
132 debug!(
133 matches = matches,
134 common = common.len(),
135 only_in_a = only_in_a.len(),
136 only_in_b = only_in_b.len(),
137 "DNS comparison complete"
138 );
139
140 Ok(DnsComparison {
141 domain: domain.to_string(),
142 record_type,
143 server_a: server_a_result,
144 server_b: server_b_result,
145 matches,
146 only_in_a,
147 only_in_b,
148 common,
149 })
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use crate::dns::{RecordData, RecordType};
157
158 #[test]
159 fn test_dns_comparison_serialization() {
160 let comparison = DnsComparison {
161 domain: "example.com".to_string(),
162 record_type: RecordType::A,
163 server_a: ServerResult {
164 nameserver: "8.8.8.8".to_string(),
165 records: vec![DnsRecord {
166 name: "example.com".to_string(),
167 record_type: RecordType::A,
168 ttl: 300,
169 data: RecordData::A {
170 address: "93.184.216.34".to_string(),
171 },
172 }],
173 error: None,
174 },
175 server_b: ServerResult {
176 nameserver: "1.1.1.1".to_string(),
177 records: vec![DnsRecord {
178 name: "example.com".to_string(),
179 record_type: RecordType::A,
180 ttl: 300,
181 data: RecordData::A {
182 address: "93.184.216.34".to_string(),
183 },
184 }],
185 error: None,
186 },
187 matches: true,
188 only_in_a: vec![],
189 only_in_b: vec![],
190 common: vec!["93.184.216.34".to_string()],
191 };
192
193 let json = serde_json::to_string(&comparison).unwrap();
194 assert!(json.contains("example.com"));
195 assert!(json.contains("93.184.216.34"));
196 assert!(json.contains("\"matches\":true"));
197 }
198
199 #[test]
200 fn test_server_result_with_error() {
201 let result = ServerResult {
202 nameserver: "8.8.8.8".to_string(),
203 records: vec![],
204 error: Some("connection timed out".to_string()),
205 };
206
207 let json = serde_json::to_string(&result).unwrap();
208 assert!(json.contains("connection timed out"));
209 }
210}