116 lines
4.6 KiB
Rust
116 lines
4.6 KiB
Rust
/*
|
|
* Copyright (C) 2021 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
//! Format DoH requests
|
|
|
|
use anyhow::{anyhow, Context, Result};
|
|
use quiche::h3;
|
|
use ring::rand::SecureRandom;
|
|
use url::Url;
|
|
|
|
pub type DnsRequest = Vec<quiche::h3::Header>;
|
|
|
|
const NS_T_AAAA: u8 = 28;
|
|
const NS_C_IN: u8 = 1;
|
|
// Used to randomly generate query prefix and query id.
|
|
const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\
|
|
abcdefghijklmnopqrstuvwxyz\
|
|
0123456789";
|
|
|
|
/// Produces a DNS query with randomized query ID and random 6-byte charset-legal prefix to produce
|
|
/// a request for a domain of the form:
|
|
/// ??????-dnsohttps-ds.metric.gstatic.com
|
|
#[rustfmt::skip]
|
|
pub fn probe_query() -> Result<String> {
|
|
let mut rnd = [0; 8];
|
|
ring::rand::SystemRandom::new().fill(&mut rnd).context("failed to generate probe rnd")?;
|
|
let c = |byte| CHARSET[(byte as usize) % CHARSET.len()];
|
|
let query = vec![
|
|
rnd[6], rnd[7], // [0-1] query ID
|
|
1, 0, // [2-3] flags; query[2] = 1 for recursion desired (RD).
|
|
0, 1, // [4-5] QDCOUNT (number of queries)
|
|
0, 0, // [6-7] ANCOUNT (number of answers)
|
|
0, 0, // [8-9] NSCOUNT (number of name server records)
|
|
0, 0, // [10-11] ARCOUNT (number of additional records)
|
|
19, c(rnd[0]), c(rnd[1]), c(rnd[2]), c(rnd[3]), c(rnd[4]), c(rnd[5]), b'-', b'd', b'n',
|
|
b's', b'o', b'h', b't', b't', b'p', b's', b'-', b'd', b's',
|
|
6, b'm', b'e', b't', b'r', b'i', b'c', 7, b'g', b's',
|
|
b't', b'a', b't', b'i', b'c', 3, b'c', b'o', b'm',
|
|
0, // null terminator of FQDN (root TLD)
|
|
0, NS_T_AAAA, // QTYPE
|
|
0, NS_C_IN // QCLASS
|
|
];
|
|
Ok(base64::encode_config(query, base64::URL_SAFE_NO_PAD))
|
|
}
|
|
|
|
/// Takes in a base64-encoded copy of a traditional DNS request and a
|
|
/// URL at which the DoH server is running and produces a set of HTTP/3 headers
|
|
/// corresponding to a DoH request for it.
|
|
pub fn dns_request(base64_query: &str, url: &Url) -> Result<DnsRequest> {
|
|
let mut path = String::from(url.path());
|
|
path.push_str("?dns=");
|
|
path.push_str(base64_query);
|
|
let req = vec![
|
|
h3::Header::new(b":method", b"GET"),
|
|
h3::Header::new(b":scheme", b"https"),
|
|
h3::Header::new(
|
|
b":authority",
|
|
url.host_str().ok_or_else(|| anyhow!("failed to get host"))?.as_bytes(),
|
|
),
|
|
h3::Header::new(b":path", path.as_bytes()),
|
|
h3::Header::new(b"user-agent", b"quiche"),
|
|
h3::Header::new(b"accept", b"application/dns-message"),
|
|
];
|
|
|
|
Ok(req)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use quiche::h3::NameValue;
|
|
use url::Url;
|
|
|
|
const PROBE_QUERY_SIZE: usize = 56;
|
|
const H3_DNS_REQUEST_HEADER_SIZE: usize = 6;
|
|
const LOCALHOST_URL: &str = "https://mylocal.com/dns-query";
|
|
|
|
#[test]
|
|
fn make_probe_query_and_request() {
|
|
let probe_query = super::probe_query().unwrap();
|
|
let url = Url::parse(LOCALHOST_URL).unwrap();
|
|
let request = super::dns_request(&probe_query, &url).unwrap();
|
|
// Verify H3 DNS request.
|
|
assert_eq!(request.len(), H3_DNS_REQUEST_HEADER_SIZE);
|
|
assert_eq!(request[0].name(), b":method");
|
|
assert_eq!(request[0].value(), b"GET");
|
|
assert_eq!(request[1].name(), b":scheme");
|
|
assert_eq!(request[1].value(), b"https");
|
|
assert_eq!(request[2].name(), b":authority");
|
|
assert_eq!(request[2].value(), url.host_str().unwrap().as_bytes());
|
|
assert_eq!(request[3].name(), b":path");
|
|
let mut path = String::from(url.path());
|
|
path.push_str("?dns=");
|
|
path.push_str(&probe_query);
|
|
assert_eq!(request[3].value(), path.as_bytes());
|
|
assert_eq!(request[5].name(), b"accept");
|
|
assert_eq!(request[5].value(), b"application/dns-message");
|
|
|
|
// Verify DNS probe packet.
|
|
let bytes = base64::decode_config(probe_query, base64::URL_SAFE_NO_PAD).unwrap();
|
|
assert_eq!(bytes.len(), PROBE_QUERY_SIZE);
|
|
}
|
|
}
|