1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct EchConfig {
12 pub config: Vec<u8>,
14 pub retry_configs: Vec<u8>,
16 pub version: u16,
18}
19
20impl EchConfig {
21 pub fn new(config: Vec<u8>) -> Self {
23 let version = if config.len() >= 2 {
24 u16::from_be_bytes([config[0], config[1]])
25 } else {
26 0xfe0a };
28 Self {
29 config,
30 retry_configs: Vec::new(),
31 version,
32 }
33 }
34
35 pub fn with_retry(config: Vec<u8>, retry_configs: Vec<u8>) -> Self {
37 let version = if config.len() >= 2 {
38 u16::from_be_bytes([config[0], config[1]])
39 } else {
40 0xfe0a
41 };
42 Self {
43 config,
44 retry_configs,
45 version,
46 }
47 }
48
49 pub fn is_empty(&self) -> bool {
51 self.config.is_empty()
52 }
53
54 pub fn version(&self) -> u16 {
56 self.version
57 }
58}
59
60impl Default for EchConfig {
61 fn default() -> Self {
62 Self {
63 config: Vec::new(),
64 retry_configs: Vec::new(),
65 version: 0xfe0a,
66 }
67 }
68}
69
70#[derive(Debug, Clone)]
72pub struct EchFetchResult {
73 pub config: Option<EchConfig>,
75 pub supports_ech: bool,
77}
78
79impl EchFetchResult {
80 pub fn with_config(config: EchConfig) -> Self {
82 Self {
83 config: Some(config),
84 supports_ech: true,
85 }
86 }
87
88 pub fn without_config() -> Self {
90 Self {
91 config: None,
92 supports_ech: false,
93 }
94 }
95
96 pub fn supported_only() -> Self {
98 Self {
99 config: None,
100 supports_ech: true,
101 }
102 }
103}
104
105#[derive(Debug, Clone, Deserialize)]
107#[serde(rename_all = "camelCase")]
108#[allow(dead_code)]
109struct DnsResponse {
110 status: u32,
111 answer: Option<Vec<DnsRecord>>,
112}
113
114#[derive(Debug, Clone, Deserialize)]
116#[allow(dead_code)]
117struct DnsRecord {
118 name: String,
119 #[serde(rename = "type")]
120 rr_type: u16,
121 data: Option<String>,
122}
123
124pub async fn fetch_ech_configs(domain: &str) -> EchFetchResult {
152 if !domain_supports_ech(domain) {
154 return EchFetchResult::without_config();
155 }
156
157 let doh_url = format!("https://1.1.1.1/dns-query?name={}&type=HTTPS", domain);
160
161 #[cfg(feature = "ech")]
163 {
164 match fetch_doh_https(&doh_url).await {
165 Ok(configs) => {
166 if let Some(config) = configs.first() {
167 EchFetchResult::with_config(config.clone())
168 } else {
169 EchFetchResult::supported_only()
170 }
171 }
172 Err(_) => EchFetchResult::supported_only(),
173 }
174 }
175
176 #[cfg(not(feature = "ech"))]
177 {
178 let _ = doh_url; EchFetchResult::supported_only()
180 }
181}
182
183#[cfg(feature = "ech")]
185async fn fetch_doh_https(url: &str) -> Result<Vec<EchConfig>, Box<dyn std::error::Error>> {
186 use std::time::Duration;
187
188 let client = reqwest::Client::builder()
190 .timeout(Duration::from_secs(5))
191 .build()?;
192
193 let response = client
195 .get(url)
196 .header("Accept", "application/dns-json")
197 .send()
198 .await?;
199
200 let dns_response: DnsResponse = response.json().await?;
202
203 let mut configs = Vec::new();
205
206 if let Some(records) = dns_response.answer {
207 for record in records {
208 if record.rr_type == 65 {
210 if let Some(data) = record.data {
213 if let Some(ech_config) = parse_https_record_ech(&data) {
214 configs.push(ech_config);
215 }
216 }
217 }
218 }
219 }
220
221 Ok(configs)
222}
223
224#[cfg(feature = "ech")]
226fn parse_https_record_ech(data: &str) -> Option<EchConfig> {
227 use base64::Engine;
228
229 let wire_data = base64::engine::general_purpose::URL_SAFE_NO_PAD
231 .decode(data)
232 .ok()?;
233
234 parse_ech_config_from_wire(&wire_data)
237}
238
239#[allow(dead_code)]
247fn parse_ech_config_from_wire(data: &[u8]) -> Option<EchConfig> {
248 if data.len() < 4 {
249 return None;
250 }
251
252 let mut pos = 4;
254 if pos >= data.len() {
255 return None;
256 }
257
258 let list_len = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
262 pos += 2;
263
264 if pos + list_len > data.len() {
265 return None;
266 }
267
268 let config_len = if pos + 2 <= data.len() {
270 u16::from_be_bytes([data[pos], data[pos + 1]]) as usize
271 } else {
272 return None;
273 };
274 pos += 2;
275
276 if pos + config_len > data.len() {
277 return None;
278 }
279
280 let config_bytes = data[pos..pos + config_len].to_vec();
281 Some(EchConfig::new(config_bytes))
282}
283
284pub fn parse_ech_config(data: &[u8]) -> Option<EchConfig> {
298 if data.len() < 4 {
299 return None;
300 }
301
302 let _version = u16::from_be_bytes([data[0], data[1]]);
304
305 let _length = u16::from_be_bytes([data[2], data[3]]);
307
308 Some(EchConfig::with_retry(data.to_vec(), Vec::new()))
316}
317
318pub fn domain_supports_ech(domain: &str) -> bool {
322 let domain = domain.to_lowercase();
323
324 if domain.ends_with(".cloudflare.com")
326 || domain.ends_with(".cloudflareinsights.com")
327 || domain == "cloudflare.com"
328 {
329 return true;
330 }
331
332 if domain.ends_with(".fastly.com") || domain.ends_with(".fastly.net") {
334 return true;
335 }
336
337 if domain.ends_with(".google.com")
339 || domain.ends_with(".googlevideo.com")
340 || domain.ends_with(".googleapis.com")
341 || domain == "google.com"
342 {
343 return true;
344 }
345
346 if domain.ends_with(".cloudfront.net") || domain.ends_with(".awscloudfront.com") {
348 return true;
349 }
350
351 if domain.contains("firefox.settings.services.mozilla.com")
353 || domain.contains("shavar.services.mozilla.com")
354 {
355 return true;
356 }
357
358 false
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364
365 #[test]
366 fn test_ech_config_empty() {
367 let config = EchConfig::default();
368 assert!(config.is_empty());
369 }
370
371 #[test]
372 fn test_ech_config_new() {
373 let config = EchConfig::new(vec![0xfe, 0x0a, 0x00, 0x01, 0x02]);
374 assert!(!config.is_empty());
375 assert_eq!(config.version(), 0xfe0a);
376 }
377
378 #[test]
379 fn test_domain_supports_ech() {
380 assert!(domain_supports_ech("cloudflare.com"));
382 assert!(domain_supports_ech("example.cloudflare.com"));
383
384 assert!(domain_supports_ech("example.fastly.com"));
386
387 assert!(domain_supports_ech("google.com"));
389 assert!(domain_supports_ech("www.google.com"));
390
391 assert!(!domain_supports_ech("example.com"));
393 }
394
395 #[test]
396 fn test_parse_ech_config_empty() {
397 assert!(parse_ech_config(&[]).is_none());
398 assert!(parse_ech_config(&[0x01, 0x02]).is_none());
399 }
400
401 #[test]
402 fn test_parse_ech_config_valid() {
403 let data = vec![0xfe, 0x0a, 0x00, 0x03, 0x01, 0x02, 0x03];
408 let config = parse_ech_config(&data).unwrap();
409 assert_eq!(config.version, 0xfe0a);
410 assert_eq!(config.config, data);
411 }
412}