1 /*
2 * Copyright (C) 2021 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 //! DoH backend for the Android DnsResolver module.
18
19 use anyhow::{anyhow, Context, Result};
20 use lazy_static::lazy_static;
21 use libc::{c_char, size_t, ssize_t};
22 use log::{debug, error, info, warn};
23 use quiche::h3;
24 use ring::rand::SecureRandom;
25 use std::collections::HashMap;
26 use std::net::{IpAddr, SocketAddr};
27 use std::os::unix::io::{AsRawFd, RawFd};
28 use std::str::FromStr;
29 use std::sync::Arc;
30 use std::{ptr, slice};
31 use tokio::net::UdpSocket;
32 use tokio::runtime::{Builder, Runtime};
33 use tokio::sync::{mpsc, oneshot};
34 use tokio::task;
35 use tokio::time::Duration;
36 use url::Url;
37
38 lazy_static! {
39 /// Tokio runtime used to perform doh-handler tasks.
40 static ref RUNTIME_STATIC: Arc<Runtime> = Arc::new(
41 Builder::new_multi_thread()
42 .worker_threads(2)
43 .max_blocking_threads(1)
44 .enable_all()
45 .thread_name("doh-handler")
46 .build()
47 .expect("Failed to create tokio runtime")
48 );
49 }
50
51 const MAX_BUFFERED_CMD_SIZE: usize = 400;
52 const MAX_INCOMING_BUFFER_SIZE_WHOLE: u64 = 10000000;
53 const MAX_INCOMING_BUFFER_SIZE_EACH: u64 = 1000000;
54 const MAX_CONCURRENT_STREAM_SIZE: u64 = 100;
55 const MAX_DATAGRAM_SIZE: usize = 1350;
56 const MAX_DATAGRAM_SIZE_U64: u64 = 1350;
57 const DOH_PORT: u16 = 443;
58 const QUICHE_IDLE_TIMEOUT_MS: u64 = 180000;
59 const SYSTEM_CERT_PATH: &str = "/system/etc/security/cacerts";
60
61 type SCID = [u8; quiche::MAX_CONN_ID_LEN];
62 type Query = Vec<u8>;
63 type Response = Vec<u8>;
64 type CmdSender = mpsc::Sender<Command>;
65 type CmdReceiver = mpsc::Receiver<Command>;
66 type QueryResponder = oneshot::Sender<Option<Response>>;
67
68 #[derive(Debug)]
69 enum Command {
70 DohQuery { query: Query, resp: QueryResponder },
71 }
72
73 /// Context for a running DoH engine.
74 pub struct DohDispatcher {
75 /// Used to submit queries to the I/O thread.
76 query_sender: CmdSender,
77
78 join_handle: task::JoinHandle<Result<()>>,
79 }
80
make_doh_udp_socket(ip_addr: &str, mark: u32) -> Result<std::net::UdpSocket>81 fn make_doh_udp_socket(ip_addr: &str, mark: u32) -> Result<std::net::UdpSocket> {
82 let sock_addr = SocketAddr::new(IpAddr::from_str(&ip_addr)?, DOH_PORT);
83 let bind_addr = match sock_addr {
84 std::net::SocketAddr::V4(_) => "0.0.0.0:0",
85 std::net::SocketAddr::V6(_) => "[::]:0",
86 };
87 let udp_sk = std::net::UdpSocket::bind(bind_addr)?;
88 udp_sk.set_nonblocking(true)?;
89 mark_socket(udp_sk.as_raw_fd(), mark)?;
90 udp_sk.connect(sock_addr)?;
91
92 debug!("connecting to {:} from {:}", sock_addr, udp_sk.local_addr()?);
93 Ok(udp_sk)
94 }
95
96 // DoH dispatcher
97 impl DohDispatcher {
new( url: &str, ip_addr: &str, mark: u32, cert_path: Option<&str>, ) -> Result<Box<DohDispatcher>>98 fn new(
99 url: &str,
100 ip_addr: &str,
101 mark: u32,
102 cert_path: Option<&str>,
103 ) -> Result<Box<DohDispatcher>> {
104 // Setup socket
105 let udp_sk = make_doh_udp_socket(&ip_addr, mark)?;
106 DohDispatcher::new_with_socket(url, ip_addr, mark, cert_path, udp_sk)
107 }
108
new_with_socket( url: &str, ip_addr: &str, mark: u32, cert_path: Option<&str>, udp_sk: std::net::UdpSocket, ) -> Result<Box<DohDispatcher>>109 fn new_with_socket(
110 url: &str,
111 ip_addr: &str,
112 mark: u32,
113 cert_path: Option<&str>,
114 udp_sk: std::net::UdpSocket,
115 ) -> Result<Box<DohDispatcher>> {
116 let url = Url::parse(&url.to_string())?;
117 if url.domain().is_none() {
118 return Err(anyhow!("no domain"));
119 }
120 // Setup quiche config
121 let config = create_quiche_config(cert_path)?;
122 let h3_config = h3::Config::new()?;
123 let mut scid = [0; quiche::MAX_CONN_ID_LEN];
124 ring::rand::SystemRandom::new().fill(&mut scid[..]).context("failed to generate scid")?;
125
126 let (cmd_sender, cmd_receiver) = mpsc::channel::<Command>(MAX_BUFFERED_CMD_SIZE);
127 debug!(
128 "Creating a doh handler task: url={}, ip_addr={}, mark={:#x}, scid {:x?}",
129 url, ip_addr, mark, &scid
130 );
131 let join_handle =
132 RUNTIME_STATIC.spawn(doh_handler(url, udp_sk, config, h3_config, scid, cmd_receiver));
133 Ok(Box::new(DohDispatcher { query_sender: cmd_sender, join_handle }))
134 }
135
query(&self, cmd: Command) -> Result<()>136 fn query(&self, cmd: Command) -> Result<()> {
137 self.query_sender.blocking_send(cmd)?;
138 Ok(())
139 }
140
abort_handler(&self)141 fn abort_handler(&self) {
142 self.join_handle.abort();
143 }
144 }
145
doh_handler( url: url::Url, udp_sk: std::net::UdpSocket, mut config: quiche::Config, h3_config: h3::Config, scid: SCID, mut rx: CmdReceiver, ) -> Result<()>146 async fn doh_handler(
147 url: url::Url,
148 udp_sk: std::net::UdpSocket,
149 mut config: quiche::Config,
150 h3_config: h3::Config,
151 scid: SCID,
152 mut rx: CmdReceiver,
153 ) -> Result<()> {
154 debug!("doh_handler: url={:?}", url);
155
156 let sk = UdpSocket::from_std(udp_sk)?;
157 let mut conn = quiche::connect(url.domain(), &scid, &mut config)?;
158 let mut quic_conn_start = std::time::Instant::now();
159 let mut h3_conn: Option<h3::Connection> = None;
160 let mut is_idle = false;
161 let mut buf = [0; 65535];
162
163 let mut query_map = HashMap::<u64, QueryResponder>::new();
164 let mut pending_cmds: Vec<Command> = Vec::new();
165
166 let mut ts = Duration::from_millis(QUICHE_IDLE_TIMEOUT_MS);
167 loop {
168 tokio::select! {
169 size = sk.recv(&mut buf) => {
170 debug!("recv {:?} ", size);
171 match size {
172 Ok(size) => {
173 let processed = match conn.recv(&mut buf[..size]) {
174 Ok(l) => l,
175 Err(e) => {
176 error!("quic recv failed: {:?}", e);
177 continue;
178 }
179 };
180 debug!("processed {} bytes", processed);
181 },
182 Err(e) => {
183 error!("socket recv failed: {:?}", e);
184 continue;
185 },
186 };
187 }
188 Some(cmd) = rx.recv() => {
189 debug!("recv {:?}", cmd);
190 pending_cmds.push(cmd);
191 }
192 _ = tokio::time::sleep(ts) => {
193 conn.on_timeout();
194 debug!("quic connection timeout");
195 }
196 }
197 if conn.is_closed() {
198 // Show connection statistics after it's closed
199 if !is_idle {
200 info!("connection closed, {:?}, {:?}", quic_conn_start.elapsed(), conn.stats());
201 is_idle = true;
202 if !conn.is_established() {
203 error!("connection handshake timed out after {:?}", quic_conn_start.elapsed());
204 }
205 }
206
207 // If there is any pending query, resume the quic connection.
208 if !pending_cmds.is_empty() {
209 info!("still some pending queries but connection is not avaiable, resume it");
210 conn = quiche::connect(url.domain(), &scid, &mut config)?;
211 quic_conn_start = std::time::Instant::now();
212 h3_conn = None;
213 is_idle = false;
214 }
215 }
216
217 // Create a new HTTP/3 connection once the QUIC connection is established.
218 if conn.is_established() && h3_conn.is_none() {
219 info!("quic ready, creating h3 conn");
220 h3_conn = Some(quiche::h3::Connection::with_transport(&mut conn, &h3_config)?);
221 }
222 // Try to receive query answers from h3 connection.
223 if let Some(h3) = h3_conn.as_mut() {
224 recv_query(h3, &mut conn, &mut query_map).await;
225 }
226
227 // Update the next timeout of quic connection.
228 ts = conn.timeout().unwrap_or_else(|| Duration::from_millis(QUICHE_IDLE_TIMEOUT_MS));
229 info!("next connection timouts {:?}", ts);
230
231 // Process the pending queries
232 while !pending_cmds.is_empty() && conn.is_established() {
233 if let Some(cmd) = pending_cmds.pop() {
234 match cmd {
235 Command::DohQuery { query, resp } => {
236 match send_dns_query(&query, &url, &mut h3_conn, &mut conn) {
237 Ok(stream_id) => {
238 query_map.insert(stream_id, resp);
239 }
240 Err(e) => {
241 info!("failed to send query {}", e);
242 pending_cmds.push(Command::DohQuery { query, resp });
243 }
244 }
245 }
246 }
247 }
248 }
249 flush_tx(&sk, &mut conn).await.unwrap_or_else(|e| {
250 error!("flush error {:?} ", e);
251 });
252 }
253 }
254
send_dns_query( query: &[u8], url: &url::Url, h3_conn: &mut Option<quiche::h3::Connection>, mut conn: &mut quiche::Connection, ) -> Result<u64>255 fn send_dns_query(
256 query: &[u8],
257 url: &url::Url,
258 h3_conn: &mut Option<quiche::h3::Connection>,
259 mut conn: &mut quiche::Connection,
260 ) -> Result<u64> {
261 let h3_conn = h3_conn.as_mut().ok_or_else(|| anyhow!("h3 conn isn't available"))?;
262
263 let mut path = String::from(url.path());
264 path.push_str("?dns=");
265 path.push_str(std::str::from_utf8(&query)?);
266 let _req = vec![
267 quiche::h3::Header::new(":method", "GET"),
268 quiche::h3::Header::new(":scheme", "https"),
269 quiche::h3::Header::new(
270 ":authority",
271 url.host_str().ok_or_else(|| anyhow!("failed to get host"))?,
272 ),
273 quiche::h3::Header::new(":path", &path),
274 quiche::h3::Header::new("user-agent", "quiche"),
275 quiche::h3::Header::new("accept", "application/dns-message"),
276 // TODO: is content-length required?
277 ];
278
279 Ok(h3_conn.send_request(&mut conn, &_req, false /*fin*/)?)
280 }
281
recv_query( h3_conn: &mut h3::Connection, mut conn: &mut quiche::Connection, map: &mut HashMap<u64, QueryResponder>, )282 async fn recv_query(
283 h3_conn: &mut h3::Connection,
284 mut conn: &mut quiche::Connection,
285 map: &mut HashMap<u64, QueryResponder>,
286 ) {
287 // Process HTTP/3 events.
288 let mut buf = [0; MAX_DATAGRAM_SIZE];
289 loop {
290 match h3_conn.poll(&mut conn) {
291 Ok((stream_id, quiche::h3::Event::Headers { list, has_body })) => {
292 info!(
293 "got response headers {:?} on stream id {} has_body {}",
294 list, stream_id, has_body
295 );
296 }
297 Ok((stream_id, quiche::h3::Event::Data)) => {
298 debug!("quiche::h3::Event::Data");
299 if let Ok(read) = h3_conn.recv_body(&mut conn, stream_id, &mut buf) {
300 info!(
301 "got {} bytes of response data on stream {}: {:x?}",
302 read,
303 stream_id,
304 &buf[..read]
305 );
306 if let Some(resp) = map.remove(&stream_id) {
307 resp.send(Some(buf[..read].to_vec())).unwrap_or_else(|e| {
308 warn!("the receiver dropped {:?}", e);
309 });
310 }
311 }
312 }
313 Ok((_stream_id, quiche::h3::Event::Finished)) => {
314 debug!("quiche::h3::Event::Finished");
315 }
316 Ok((_stream_id, quiche::h3::Event::Datagram)) => {
317 debug!("quiche::h3::Event::Datagram");
318 }
319 Ok((_stream_id, quiche::h3::Event::GoAway)) => {
320 debug!("quiche::h3::Event::GoAway");
321 }
322 Err(quiche::h3::Error::Done) => {
323 debug!("quiche::h3::Error::Done");
324 break;
325 }
326 Err(e) => {
327 error!("HTTP/3 processing failed: {:?}", e);
328 break;
329 }
330 }
331 }
332 }
333
flush_tx(sk: &UdpSocket, conn: &mut quiche::Connection) -> Result<()>334 async fn flush_tx(sk: &UdpSocket, conn: &mut quiche::Connection) -> Result<()> {
335 let mut out = [0; MAX_DATAGRAM_SIZE];
336 loop {
337 let write = match conn.send(&mut out) {
338 Ok(v) => v,
339 Err(quiche::Error::Done) => {
340 debug!("done writing");
341 break;
342 }
343 Err(e) => {
344 conn.close(false, 0x1, b"fail").ok();
345 return Err(anyhow::Error::new(e));
346 }
347 };
348 sk.send(&out[..write]).await?;
349 debug!("written {}", write);
350 }
351 Ok(())
352 }
353
create_quiche_config(cert_path: Option<&str>) -> Result<quiche::Config>354 fn create_quiche_config(cert_path: Option<&str>) -> Result<quiche::Config> {
355 let mut config = quiche::Config::new(quiche::PROTOCOL_VERSION)?;
356 config.set_application_protos(h3::APPLICATION_PROTOCOL)?;
357 config.verify_peer(true);
358 config.load_verify_locations_from_directory(cert_path.unwrap_or(SYSTEM_CERT_PATH))?;
359 // Some of these configs are necessary, or the server can't respond the HTTP/3 request.
360 config.set_max_idle_timeout(QUICHE_IDLE_TIMEOUT_MS);
361 config.set_max_udp_payload_size(MAX_DATAGRAM_SIZE_U64);
362 config.set_initial_max_data(MAX_INCOMING_BUFFER_SIZE_WHOLE);
363 config.set_initial_max_stream_data_bidi_local(MAX_INCOMING_BUFFER_SIZE_EACH);
364 config.set_initial_max_stream_data_bidi_remote(MAX_INCOMING_BUFFER_SIZE_EACH);
365 config.set_initial_max_stream_data_uni(MAX_INCOMING_BUFFER_SIZE_EACH);
366 config.set_initial_max_streams_bidi(MAX_CONCURRENT_STREAM_SIZE);
367 config.set_initial_max_streams_uni(MAX_CONCURRENT_STREAM_SIZE);
368 config.set_disable_active_migration(true);
369 Ok(config)
370 }
371
mark_socket(fd: RawFd, mark: u32) -> Result<()>372 fn mark_socket(fd: RawFd, mark: u32) -> Result<()> {
373 // libc::setsockopt is a wrapper function calling into bionic setsockopt.
374 // Both fd and mark are valid, which makes the function call mostly safe.
375 if unsafe {
376 libc::setsockopt(
377 fd,
378 libc::SOL_SOCKET,
379 libc::SO_MARK,
380 &mark as *const _ as *const libc::c_void,
381 std::mem::size_of::<u32>() as libc::socklen_t,
382 )
383 } == 0
384 {
385 Ok(())
386 } else {
387 Err(anyhow::Error::new(std::io::Error::last_os_error()))
388 }
389 }
390
391 /// Performs static initialization fo the DoH engine.
392 #[no_mangle]
doh_init() -> *const c_char393 pub extern "C" fn doh_init() -> *const c_char {
394 android_logger::init_once(android_logger::Config::default().with_min_level(log::Level::Trace));
395 static VERSION: &str = "1.0\0";
396 VERSION.as_ptr() as *const c_char
397 }
398
399 /// Creates and returns a DoH engine instance.
400 /// The returned object must be freed with doh_delete().
401 /// # Safety
402 /// All the pointer args are null terminated strings.
403 #[no_mangle]
doh_new( url: *const c_char, ip_addr: *const c_char, mark: libc::uint32_t, cert_path: *const c_char, ) -> *mut DohDispatcher404 pub unsafe extern "C" fn doh_new(
405 url: *const c_char,
406 ip_addr: *const c_char,
407 mark: libc::uint32_t,
408 cert_path: *const c_char,
409 ) -> *mut DohDispatcher {
410 let (url, ip_addr, cert_path) = match (
411 std::ffi::CStr::from_ptr(url).to_str(),
412 std::ffi::CStr::from_ptr(ip_addr).to_str(),
413 std::ffi::CStr::from_ptr(cert_path).to_str(),
414 ) {
415 (Ok(url), Ok(ip_addr), Ok(cert_path)) => {
416 if !cert_path.is_empty() {
417 (url, ip_addr, Some(cert_path))
418 } else {
419 (url, ip_addr, None)
420 }
421 }
422 _ => {
423 error!("bad input");
424 return ptr::null_mut();
425 }
426 };
427 match DohDispatcher::new(url, ip_addr, mark, cert_path) {
428 Ok(c) => Box::into_raw(c),
429 Err(e) => {
430 error!("doh_new: failed: {:?}", e);
431 ptr::null_mut()
432 }
433 }
434 }
435
436 /// Deletes a DoH engine created by doh_new().
437 /// # Safety
438 /// `doh` must be a non-null pointer previously created by `doh_new()`
439 /// and not yet deleted by `doh_delete()`.
440 #[no_mangle]
doh_delete(doh: *mut DohDispatcher)441 pub unsafe extern "C" fn doh_delete(doh: *mut DohDispatcher) {
442 Box::from_raw(doh).abort_handler()
443 }
444
445 /// Sends a DNS query and waits for the response.
446 /// # Safety
447 /// `doh` must be a non-null pointer previously created by `doh_new()`
448 /// and not yet deleted by `doh_delete()`.
449 /// `query` must point to a buffer at least `query_len` in size.
450 /// `response` must point to a buffer at least `response_len` in size.
451 #[no_mangle]
doh_query( doh: &mut DohDispatcher, query: *mut u8, query_len: size_t, response: *mut u8, response_len: size_t, ) -> ssize_t452 pub unsafe extern "C" fn doh_query(
453 doh: &mut DohDispatcher,
454 query: *mut u8,
455 query_len: size_t,
456 response: *mut u8,
457 response_len: size_t,
458 ) -> ssize_t {
459 let q = slice::from_raw_parts_mut(query, query_len);
460 let (resp_tx, resp_rx) = oneshot::channel();
461 let cmd = Command::DohQuery { query: q.to_vec(), resp: resp_tx };
462 if let Err(e) = doh.query(cmd) {
463 error!("Failed to send the query: {:?}", e);
464 return -1;
465 }
466 match RUNTIME_STATIC.block_on(resp_rx) {
467 Ok(value) => {
468 if let Some(resp) = value {
469 if resp.len() > response_len || resp.len() > isize::MAX as usize {
470 return -1;
471 }
472 let response = slice::from_raw_parts_mut(response, resp.len());
473 response.copy_from_slice(&resp);
474 return resp.len() as ssize_t;
475 }
476 -1
477 }
478 Err(e) => {
479 error!("no result {}", e);
480 -1
481 }
482 }
483 }
484
485 #[cfg(test)]
486 mod tests {
487 use super::*;
488 use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
489
490 const TEST_MARK: u32 = 0xD0033;
491 const LOOPBACK_ADDR: &str = "127.0.0.1";
492
493 #[test]
dohdispatcher_invalid_args()494 fn dohdispatcher_invalid_args() {
495 let test_args = [
496 // Bad url
497 ("foo", "bar"),
498 ("https://1", "bar"),
499 ("https:/", "bar"),
500 // Bad ip
501 ("https://dns.google", "bar"),
502 ("https://dns.google", "256.256.256.256"),
503 ];
504 for args in &test_args {
505 assert!(
506 DohDispatcher::new(args.0, args.1, 0, None).is_err(),
507 "doh dispatcher should not be created"
508 )
509 }
510 }
511
512 #[test]
make_doh_udp_socket()513 fn make_doh_udp_socket() {
514 // Bad ip
515 for ip in &["foo", "1", "333.333.333.333"] {
516 assert!(super::make_doh_udp_socket(ip, 0).is_err(), "udp socket should not be created");
517 }
518 // Make a socket connecting to loopback with a test mark.
519 let sk = super::make_doh_udp_socket(LOOPBACK_ADDR, TEST_MARK).unwrap();
520 // Check if the socket is connected to loopback.
521 assert_eq!(
522 sk.peer_addr().unwrap(),
523 SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), DOH_PORT))
524 );
525
526 // Check if the socket mark is correct.
527 let fd: RawFd = sk.as_raw_fd();
528
529 let mut mark: u32 = 50;
530 let mut size = std::mem::size_of::<u32>() as libc::socklen_t;
531 unsafe {
532 // Safety: fd must be valid.
533 assert_eq!(
534 libc::getsockopt(
535 fd,
536 libc::SOL_SOCKET,
537 libc::SO_MARK,
538 &mut mark as *mut _ as *mut libc::c_void,
539 &mut size as *mut _ as *mut libc::socklen_t,
540 ),
541 0
542 );
543 }
544 assert_eq!(mark, TEST_MARK);
545
546 // Check if the socket is non-blocking.
547 unsafe {
548 // Safety: fd must be valid.
549 assert_eq!(libc::fcntl(fd, libc::F_GETFL, 0) & libc::O_NONBLOCK, libc::O_NONBLOCK);
550 }
551 }
552
553 #[test]
create_quiche_config()554 fn create_quiche_config() {
555 assert!(
556 super::create_quiche_config(None).is_ok(),
557 "quiche config without cert creating failed"
558 );
559 assert!(
560 super::create_quiche_config(Some("data/local/tmp/")).is_ok(),
561 "quiche config with cert creating failed"
562 );
563 }
564
565 const GOOGLE_DNS_URL: &str = "https://dns.google/dns-query";
566 const GOOGLE_DNS_IP: &str = "8.8.8.8";
567 // qtype: A, qname: www.example.com
568 const SAMPLE_QUERY: &str = "q80BAAABAAAAAAAAA3d3dwdleGFtcGxlA2NvbQAAAQAB";
569 #[test]
close_doh()570 fn close_doh() {
571 let udp_sk = super::make_doh_udp_socket(LOOPBACK_ADDR, TEST_MARK).unwrap();
572 let doh =
573 DohDispatcher::new_with_socket(GOOGLE_DNS_URL, GOOGLE_DNS_IP, 0, None, udp_sk).unwrap();
574 let (resp_tx, resp_rx) = oneshot::channel();
575 let cmd = Command::DohQuery { query: SAMPLE_QUERY.as_bytes().to_vec(), resp: resp_tx };
576 assert!(doh.query(cmd).is_ok(), "Send query failed");
577 doh.abort_handler();
578 assert!(RUNTIME_STATIC.block_on(resp_rx).is_err(), "channel should already be closed");
579 }
580
581 #[test]
doh_init()582 fn doh_init() {
583 unsafe {
584 // Safety: the returned pointer of doh_init() must be a null terminated string.
585 assert_eq!(std::ffi::CStr::from_ptr(super::doh_init()).to_str().unwrap(), "1.0");
586 }
587 }
588 }
589