You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

589 lines
20 KiB

/*
* 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.
*/
//! DoH backend for the Android DnsResolver module.
use anyhow::{anyhow, Context, Result};
use lazy_static::lazy_static;
use libc::{c_char, size_t, ssize_t};
use log::{debug, error, info, warn};
use quiche::h3;
use ring::rand::SecureRandom;
use std::collections::HashMap;
use std::net::{IpAddr, SocketAddr};
use std::os::unix::io::{AsRawFd, RawFd};
use std::str::FromStr;
use std::sync::Arc;
use std::{ptr, slice};
use tokio::net::UdpSocket;
use tokio::runtime::{Builder, Runtime};
use tokio::sync::{mpsc, oneshot};
use tokio::task;
use tokio::time::Duration;
use url::Url;
lazy_static! {
/// Tokio runtime used to perform doh-handler tasks.
static ref RUNTIME_STATIC: Arc<Runtime> = Arc::new(
Builder::new_multi_thread()
.worker_threads(2)
.max_blocking_threads(1)
.enable_all()
.thread_name("doh-handler")
.build()
.expect("Failed to create tokio runtime")
);
}
const MAX_BUFFERED_CMD_SIZE: usize = 400;
const MAX_INCOMING_BUFFER_SIZE_WHOLE: u64 = 10000000;
const MAX_INCOMING_BUFFER_SIZE_EACH: u64 = 1000000;
const MAX_CONCURRENT_STREAM_SIZE: u64 = 100;
const MAX_DATAGRAM_SIZE: usize = 1350;
const MAX_DATAGRAM_SIZE_U64: u64 = 1350;
const DOH_PORT: u16 = 443;
const QUICHE_IDLE_TIMEOUT_MS: u64 = 180000;
const SYSTEM_CERT_PATH: &str = "/system/etc/security/cacerts";
type SCID = [u8; quiche::MAX_CONN_ID_LEN];
type Query = Vec<u8>;
type Response = Vec<u8>;
type CmdSender = mpsc::Sender<Command>;
type CmdReceiver = mpsc::Receiver<Command>;
type QueryResponder = oneshot::Sender<Option<Response>>;
#[derive(Debug)]
enum Command {
DohQuery { query: Query, resp: QueryResponder },
}
/// Context for a running DoH engine.
pub struct DohDispatcher {
/// Used to submit queries to the I/O thread.
query_sender: CmdSender,
join_handle: task::JoinHandle<Result<()>>,
}
fn make_doh_udp_socket(ip_addr: &str, mark: u32) -> Result<std::net::UdpSocket> {
let sock_addr = SocketAddr::new(IpAddr::from_str(&ip_addr)?, DOH_PORT);
let bind_addr = match sock_addr {
std::net::SocketAddr::V4(_) => "0.0.0.0:0",
std::net::SocketAddr::V6(_) => "[::]:0",
};
let udp_sk = std::net::UdpSocket::bind(bind_addr)?;
udp_sk.set_nonblocking(true)?;
mark_socket(udp_sk.as_raw_fd(), mark)?;
udp_sk.connect(sock_addr)?;
debug!("connecting to {:} from {:}", sock_addr, udp_sk.local_addr()?);
Ok(udp_sk)
}
// DoH dispatcher
impl DohDispatcher {
fn new(
url: &str,
ip_addr: &str,
mark: u32,
cert_path: Option<&str>,
) -> Result<Box<DohDispatcher>> {
// Setup socket
let udp_sk = make_doh_udp_socket(&ip_addr, mark)?;
DohDispatcher::new_with_socket(url, ip_addr, mark, cert_path, udp_sk)
}
fn new_with_socket(
url: &str,
ip_addr: &str,
mark: u32,
cert_path: Option<&str>,
udp_sk: std::net::UdpSocket,
) -> Result<Box<DohDispatcher>> {
let url = Url::parse(&url.to_string())?;
if url.domain().is_none() {
return Err(anyhow!("no domain"));
}
// Setup quiche config
let config = create_quiche_config(cert_path)?;
let h3_config = h3::Config::new()?;
let mut scid = [0; quiche::MAX_CONN_ID_LEN];
ring::rand::SystemRandom::new().fill(&mut scid[..]).context("failed to generate scid")?;
let (cmd_sender, cmd_receiver) = mpsc::channel::<Command>(MAX_BUFFERED_CMD_SIZE);
debug!(
"Creating a doh handler task: url={}, ip_addr={}, mark={:#x}, scid {:x?}",
url, ip_addr, mark, &scid
);
let join_handle =
RUNTIME_STATIC.spawn(doh_handler(url, udp_sk, config, h3_config, scid, cmd_receiver));
Ok(Box::new(DohDispatcher { query_sender: cmd_sender, join_handle }))
}
fn query(&self, cmd: Command) -> Result<()> {
self.query_sender.blocking_send(cmd)?;
Ok(())
}
fn abort_handler(&self) {
self.join_handle.abort();
}
}
async fn doh_handler(
url: url::Url,
udp_sk: std::net::UdpSocket,
mut config: quiche::Config,
h3_config: h3::Config,
scid: SCID,
mut rx: CmdReceiver,
) -> Result<()> {
debug!("doh_handler: url={:?}", url);
let sk = UdpSocket::from_std(udp_sk)?;
let mut conn = quiche::connect(url.domain(), &scid, &mut config)?;
let mut quic_conn_start = std::time::Instant::now();
let mut h3_conn: Option<h3::Connection> = None;
let mut is_idle = false;
let mut buf = [0; 65535];
let mut query_map = HashMap::<u64, QueryResponder>::new();
let mut pending_cmds: Vec<Command> = Vec::new();
let mut ts = Duration::from_millis(QUICHE_IDLE_TIMEOUT_MS);
loop {
tokio::select! {
size = sk.recv(&mut buf) => {
debug!("recv {:?} ", size);
match size {
Ok(size) => {
let processed = match conn.recv(&mut buf[..size]) {
Ok(l) => l,
Err(e) => {
error!("quic recv failed: {:?}", e);
continue;
}
};
debug!("processed {} bytes", processed);
},
Err(e) => {
error!("socket recv failed: {:?}", e);
continue;
},
};
}
Some(cmd) = rx.recv() => {
debug!("recv {:?}", cmd);
pending_cmds.push(cmd);
}
_ = tokio::time::sleep(ts) => {
conn.on_timeout();
debug!("quic connection timeout");
}
}
if conn.is_closed() {
// Show connection statistics after it's closed
if !is_idle {
info!("connection closed, {:?}, {:?}", quic_conn_start.elapsed(), conn.stats());
is_idle = true;
if !conn.is_established() {
error!("connection handshake timed out after {:?}", quic_conn_start.elapsed());
}
}
// If there is any pending query, resume the quic connection.
if !pending_cmds.is_empty() {
info!("still some pending queries but connection is not avaiable, resume it");
conn = quiche::connect(url.domain(), &scid, &mut config)?;
quic_conn_start = std::time::Instant::now();
h3_conn = None;
is_idle = false;
}
}
// Create a new HTTP/3 connection once the QUIC connection is established.
if conn.is_established() && h3_conn.is_none() {
info!("quic ready, creating h3 conn");
h3_conn = Some(quiche::h3::Connection::with_transport(&mut conn, &h3_config)?);
}
// Try to receive query answers from h3 connection.
if let Some(h3) = h3_conn.as_mut() {
recv_query(h3, &mut conn, &mut query_map).await;
}
// Update the next timeout of quic connection.
ts = conn.timeout().unwrap_or_else(|| Duration::from_millis(QUICHE_IDLE_TIMEOUT_MS));
info!("next connection timouts {:?}", ts);
// Process the pending queries
while !pending_cmds.is_empty() && conn.is_established() {
if let Some(cmd) = pending_cmds.pop() {
match cmd {
Command::DohQuery { query, resp } => {
match send_dns_query(&query, &url, &mut h3_conn, &mut conn) {
Ok(stream_id) => {
query_map.insert(stream_id, resp);
}
Err(e) => {
info!("failed to send query {}", e);
pending_cmds.push(Command::DohQuery { query, resp });
}
}
}
}
}
}
flush_tx(&sk, &mut conn).await.unwrap_or_else(|e| {
error!("flush error {:?} ", e);
});
}
}
fn send_dns_query(
query: &[u8],
url: &url::Url,
h3_conn: &mut Option<quiche::h3::Connection>,
mut conn: &mut quiche::Connection,
) -> Result<u64> {
let h3_conn = h3_conn.as_mut().ok_or_else(|| anyhow!("h3 conn isn't available"))?;
let mut path = String::from(url.path());
path.push_str("?dns=");
path.push_str(std::str::from_utf8(&query)?);
let _req = vec![
quiche::h3::Header::new(":method", "GET"),
quiche::h3::Header::new(":scheme", "https"),
quiche::h3::Header::new(
":authority",
url.host_str().ok_or_else(|| anyhow!("failed to get host"))?,
),
quiche::h3::Header::new(":path", &path),
quiche::h3::Header::new("user-agent", "quiche"),
quiche::h3::Header::new("accept", "application/dns-message"),
// TODO: is content-length required?
];
Ok(h3_conn.send_request(&mut conn, &_req, false /*fin*/)?)
}
async fn recv_query(
h3_conn: &mut h3::Connection,
mut conn: &mut quiche::Connection,
map: &mut HashMap<u64, QueryResponder>,
) {
// Process HTTP/3 events.
let mut buf = [0; MAX_DATAGRAM_SIZE];
loop {
match h3_conn.poll(&mut conn) {
Ok((stream_id, quiche::h3::Event::Headers { list, has_body })) => {
info!(
"got response headers {:?} on stream id {} has_body {}",
list, stream_id, has_body
);
}
Ok((stream_id, quiche::h3::Event::Data)) => {
debug!("quiche::h3::Event::Data");
if let Ok(read) = h3_conn.recv_body(&mut conn, stream_id, &mut buf) {
info!(
"got {} bytes of response data on stream {}: {:x?}",
read,
stream_id,
&buf[..read]
);
if let Some(resp) = map.remove(&stream_id) {
resp.send(Some(buf[..read].to_vec())).unwrap_or_else(|e| {
warn!("the receiver dropped {:?}", e);
});
}
}
}
Ok((_stream_id, quiche::h3::Event::Finished)) => {
debug!("quiche::h3::Event::Finished");
}
Ok((_stream_id, quiche::h3::Event::Datagram)) => {
debug!("quiche::h3::Event::Datagram");
}
Ok((_stream_id, quiche::h3::Event::GoAway)) => {
debug!("quiche::h3::Event::GoAway");
}
Err(quiche::h3::Error::Done) => {
debug!("quiche::h3::Error::Done");
break;
}
Err(e) => {
error!("HTTP/3 processing failed: {:?}", e);
break;
}
}
}
}
async fn flush_tx(sk: &UdpSocket, conn: &mut quiche::Connection) -> Result<()> {
let mut out = [0; MAX_DATAGRAM_SIZE];
loop {
let write = match conn.send(&mut out) {
Ok(v) => v,
Err(quiche::Error::Done) => {
debug!("done writing");
break;
}
Err(e) => {
conn.close(false, 0x1, b"fail").ok();
return Err(anyhow::Error::new(e));
}
};
sk.send(&out[..write]).await?;
debug!("written {}", write);
}
Ok(())
}
fn create_quiche_config(cert_path: Option<&str>) -> Result<quiche::Config> {
let mut config = quiche::Config::new(quiche::PROTOCOL_VERSION)?;
config.set_application_protos(h3::APPLICATION_PROTOCOL)?;
config.verify_peer(true);
config.load_verify_locations_from_directory(cert_path.unwrap_or(SYSTEM_CERT_PATH))?;
// Some of these configs are necessary, or the server can't respond the HTTP/3 request.
config.set_max_idle_timeout(QUICHE_IDLE_TIMEOUT_MS);
config.set_max_udp_payload_size(MAX_DATAGRAM_SIZE_U64);
config.set_initial_max_data(MAX_INCOMING_BUFFER_SIZE_WHOLE);
config.set_initial_max_stream_data_bidi_local(MAX_INCOMING_BUFFER_SIZE_EACH);
config.set_initial_max_stream_data_bidi_remote(MAX_INCOMING_BUFFER_SIZE_EACH);
config.set_initial_max_stream_data_uni(MAX_INCOMING_BUFFER_SIZE_EACH);
config.set_initial_max_streams_bidi(MAX_CONCURRENT_STREAM_SIZE);
config.set_initial_max_streams_uni(MAX_CONCURRENT_STREAM_SIZE);
config.set_disable_active_migration(true);
Ok(config)
}
fn mark_socket(fd: RawFd, mark: u32) -> Result<()> {
// libc::setsockopt is a wrapper function calling into bionic setsockopt.
// Both fd and mark are valid, which makes the function call mostly safe.
if unsafe {
libc::setsockopt(
fd,
libc::SOL_SOCKET,
libc::SO_MARK,
&mark as *const _ as *const libc::c_void,
std::mem::size_of::<u32>() as libc::socklen_t,
)
} == 0
{
Ok(())
} else {
Err(anyhow::Error::new(std::io::Error::last_os_error()))
}
}
/// Performs static initialization fo the DoH engine.
#[no_mangle]
pub extern "C" fn doh_init() -> *const c_char {
android_logger::init_once(android_logger::Config::default().with_min_level(log::Level::Trace));
static VERSION: &str = "1.0\0";
VERSION.as_ptr() as *const c_char
}
/// Creates and returns a DoH engine instance.
/// The returned object must be freed with doh_delete().
/// # Safety
/// All the pointer args are null terminated strings.
#[no_mangle]
pub unsafe extern "C" fn doh_new(
url: *const c_char,
ip_addr: *const c_char,
mark: libc::uint32_t,
cert_path: *const c_char,
) -> *mut DohDispatcher {
let (url, ip_addr, cert_path) = match (
std::ffi::CStr::from_ptr(url).to_str(),
std::ffi::CStr::from_ptr(ip_addr).to_str(),
std::ffi::CStr::from_ptr(cert_path).to_str(),
) {
(Ok(url), Ok(ip_addr), Ok(cert_path)) => {
if !cert_path.is_empty() {
(url, ip_addr, Some(cert_path))
} else {
(url, ip_addr, None)
}
}
_ => {
error!("bad input");
return ptr::null_mut();
}
};
match DohDispatcher::new(url, ip_addr, mark, cert_path) {
Ok(c) => Box::into_raw(c),
Err(e) => {
error!("doh_new: failed: {:?}", e);
ptr::null_mut()
}
}
}
/// Deletes a DoH engine created by doh_new().
/// # Safety
/// `doh` must be a non-null pointer previously created by `doh_new()`
/// and not yet deleted by `doh_delete()`.
#[no_mangle]
pub unsafe extern "C" fn doh_delete(doh: *mut DohDispatcher) {
Box::from_raw(doh).abort_handler()
}
/// Sends a DNS query and waits for the response.
/// # Safety
/// `doh` must be a non-null pointer previously created by `doh_new()`
/// and not yet deleted by `doh_delete()`.
/// `query` must point to a buffer at least `query_len` in size.
/// `response` must point to a buffer at least `response_len` in size.
#[no_mangle]
pub unsafe extern "C" fn doh_query(
doh: &mut DohDispatcher,
query: *mut u8,
query_len: size_t,
response: *mut u8,
response_len: size_t,
) -> ssize_t {
let q = slice::from_raw_parts_mut(query, query_len);
let (resp_tx, resp_rx) = oneshot::channel();
let cmd = Command::DohQuery { query: q.to_vec(), resp: resp_tx };
if let Err(e) = doh.query(cmd) {
error!("Failed to send the query: {:?}", e);
return -1;
}
match RUNTIME_STATIC.block_on(resp_rx) {
Ok(value) => {
if let Some(resp) = value {
if resp.len() > response_len || resp.len() > isize::MAX as usize {
return -1;
}
let response = slice::from_raw_parts_mut(response, resp.len());
response.copy_from_slice(&resp);
return resp.len() as ssize_t;
}
-1
}
Err(e) => {
error!("no result {}", e);
-1
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
const TEST_MARK: u32 = 0xD0033;
const LOOPBACK_ADDR: &str = "127.0.0.1";
#[test]
fn dohdispatcher_invalid_args() {
let test_args = [
// Bad url
("foo", "bar"),
("https://1", "bar"),
("https:/", "bar"),
// Bad ip
("https://dns.google", "bar"),
("https://dns.google", "256.256.256.256"),
];
for args in &test_args {
assert!(
DohDispatcher::new(args.0, args.1, 0, None).is_err(),
"doh dispatcher should not be created"
)
}
}
#[test]
fn make_doh_udp_socket() {
// Bad ip
for ip in &["foo", "1", "333.333.333.333"] {
assert!(super::make_doh_udp_socket(ip, 0).is_err(), "udp socket should not be created");
}
// Make a socket connecting to loopback with a test mark.
let sk = super::make_doh_udp_socket(LOOPBACK_ADDR, TEST_MARK).unwrap();
// Check if the socket is connected to loopback.
assert_eq!(
sk.peer_addr().unwrap(),
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), DOH_PORT))
);
// Check if the socket mark is correct.
let fd: RawFd = sk.as_raw_fd();
let mut mark: u32 = 50;
let mut size = std::mem::size_of::<u32>() as libc::socklen_t;
unsafe {
// Safety: fd must be valid.
assert_eq!(
libc::getsockopt(
fd,
libc::SOL_SOCKET,
libc::SO_MARK,
&mut mark as *mut _ as *mut libc::c_void,
&mut size as *mut _ as *mut libc::socklen_t,
),
0
);
}
assert_eq!(mark, TEST_MARK);
// Check if the socket is non-blocking.
unsafe {
// Safety: fd must be valid.
assert_eq!(libc::fcntl(fd, libc::F_GETFL, 0) & libc::O_NONBLOCK, libc::O_NONBLOCK);
}
}
#[test]
fn create_quiche_config() {
assert!(
super::create_quiche_config(None).is_ok(),
"quiche config without cert creating failed"
);
assert!(
super::create_quiche_config(Some("data/local/tmp/")).is_ok(),
"quiche config with cert creating failed"
);
}
const GOOGLE_DNS_URL: &str = "https://dns.google/dns-query";
const GOOGLE_DNS_IP: &str = "8.8.8.8";
// qtype: A, qname: www.example.com
const SAMPLE_QUERY: &str = "q80BAAABAAAAAAAAA3d3dwdleGFtcGxlA2NvbQAAAQAB";
#[test]
fn close_doh() {
let udp_sk = super::make_doh_udp_socket(LOOPBACK_ADDR, TEST_MARK).unwrap();
let doh =
DohDispatcher::new_with_socket(GOOGLE_DNS_URL, GOOGLE_DNS_IP, 0, None, udp_sk).unwrap();
let (resp_tx, resp_rx) = oneshot::channel();
let cmd = Command::DohQuery { query: SAMPLE_QUERY.as_bytes().to_vec(), resp: resp_tx };
assert!(doh.query(cmd).is_ok(), "Send query failed");
doh.abort_handler();
assert!(RUNTIME_STATIC.block_on(resp_rx).is_err(), "channel should already be closed");
}
#[test]
fn doh_init() {
unsafe {
// Safety: the returned pointer of doh_init() must be a null terminated string.
assert_eq!(std::ffi::CStr::from_ptr(super::doh_init()).to_str().unwrap(), "1.0");
}
}
}