Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f54cf260 authored by Mike Yu's avatar Mike Yu
Browse files

Implement DoH server for testing

This is the initial implementation of DoH tests. Similar to how
DnsTlsFrontend works, the code in rust is a proxy which forwards
DoH queries to a backend cleartext nameserver. The code in C++ is
a wrapper for tests to communicate with Rust DohFrontend.

Bug: 155855709
Bug: 181642979
Test: m libdoh_frontend_ffi
Change-Id: If1d7ab1bdd242a9b4be4b1f72c453c6a86245dbe
parent 088cdcf4
Loading
Loading
Loading
Loading

tests/doh/Android.bp

0 → 100644
+18 −0
Original line number Diff line number Diff line
rust_ffi_static {
    name: "libdoh_frontend_ffi",
    crate_name: "doh_frontend_ffi",
    srcs: ["src/lib.rs"],
    edition: "2018",

    rlibs: [
        "liblibc",
        "liblog_rust",
        "libandroid_logger",
        "libanyhow",
        "libquiche",
        "libring",
        "liblazy_static",
        "libtokio",
        "libbase64_rust",
    ],
}
+39 −0
Original line number Diff line number Diff line
# For documentation, see: https://github.com/eqrion/cbindgen/blob/master/docs.md

include_version = true
braces = "SameLine"
line_length = 100
tab_width = 4
language = "C++"
pragma_once = true
no_includes = true
sys_includes = ["stdint.h", "sys/types.h"]
header = """
// This file is autogenerated by:
//   cbindgen --config cbindgen.toml src/lib.rs -o include/lib.rs.h
// Don't modify manually.
"""
documentation = true
style = "tag"
namespaces = ["test", "rust"]

[export]
item_types = ["globals", "enums", "structs", "unions", "typedefs", "opaque", "functions", "constants"]

[parse]
parse_deps = true
include = ["doh"]

[fn]
args = "auto"

[struct]
associated_constants_in_body = true

[enum]
add_sentinel = true
derive_helper_methods = true
derive_ostream = true

[macro_expansion]
bitflags = true
+87 −0
Original line number Diff line number Diff line
// This file is autogenerated by:
//   cbindgen --config cbindgen.toml src/lib.rs -o include/lib.rs.h
// Don't modify manually.


#pragma once

/* Generated with cbindgen:0.19.0 */

#include <stdint.h>
#include <sys/types.h>

namespace test {
namespace rust {

static const uintptr_t DNS_HEADER_SIZE = 12;

static const uintptr_t MAX_UDP_PAYLOAD_SIZE = 1350;

/// Frontend object.
struct DohFrontend;

struct Stats {
    uint32_t queries_received;
};

extern "C" {

/// Creates a DohFrontend object by the given IP addresss and ports. Returns the pointer of
/// the object if the creation succeeds; otherwise, returns a null pointer.
///
/// # Safety
///
/// The parameters `addr`, `port`, `backend_addr`, and `backend_port` must all point to null
/// terminated UTF-8 encoded strings.
DohFrontend *frontend_new(const char *addr,
                          const char *port,
                          const char *backend_addr,
                          const char *backend_port);

/// Starts the `DohFrontend` worker thread. Returns true if the worker thread is spawned
/// successfully; otherwise, it returns false.
bool frontend_start(DohFrontend *doh);

/// Stops the `DohFrontend` worker thread.
bool frontend_stop(DohFrontend *doh);

/// Deletes the `DohFrontend` created from `frontend_new`.
/// If the caller has called `frontend_start` to start `DohFrontend`, it has to call
/// call `frontend_stop` to stop the worker thread before deleting the object.
///
/// # Safety
///
/// The DohFrontend is not set to null pointer, caller needs to do it on its own.
void frontend_delete(DohFrontend *doh);

/// Sets server certificate to `DohFrontend`.
///
/// # Safety
///
/// The given certificate must be a null-terminated UTF-8 encoded string.
bool frontend_set_certificate(DohFrontend *doh, const char *certificate);

/// Sets server private key to `DohFrontend`.
///
/// # Safety
///
/// The given private key must be a null-terminated UTF-8 encoded string.
bool frontend_set_private_key(DohFrontend *doh, const char *private_key);

/// Configures the `DohFrontend` not to process DoH queries until a given number of DoH queries
/// are received. This function works even in the middle of the worker thread.
bool frontend_set_delay_queries(DohFrontend *doh, int32_t count);

/// Gets the statistics of the `DohFrontend` and writes the result to |out|.
void frontend_stats(const DohFrontend *doh, Stats *out);

/// Resets `queries_received` field of `Stats` owned by the `DohFrontend`.
bool frontend_stats_clear_queries(const DohFrontend *doh);

/// Enable Rust debug logging.
void init_android_logger();

} // extern "C"

} // namespace rust
} // namespace test
+268 −0
Original line number Diff line number Diff line
/*
 * 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.
 */

//! Client management, including the communication with quiche I/O.

use anyhow::{anyhow, bail, ensure, Result};
use log::{debug, info, warn};
use quiche::h3::NameValue;
use ring::hmac;
use ring::rand::SystemRandom;
use std::collections::{hash_map, HashMap};
use std::net::SocketAddr;
use std::pin::Pin;
use std::time::Duration;

pub const DNS_HEADER_SIZE: usize = 12;
pub const MAX_UDP_PAYLOAD_SIZE: usize = 1350;

pub type ConnectionID = Vec<u8>;

const URL_PATH_PREFIX: &str = "/dns-query?dns=";

/// Manages a QUIC and HTTP/3 connection. No socket I/O operations.
pub struct Client {
    /// QUIC connection.
    conn: Pin<Box<quiche::Connection>>,

    /// HTTP/3 connection.
    h3_conn: Option<quiche::h3::Connection>,

    /// Socket address the client from.
    addr: SocketAddr,

    /// The unique ID for the client.
    id: ConnectionID,

    /// Queues the DNS queries being processed in backend.
    /// <Query ID, Stream ID>
    in_flight_queries: HashMap<[u8; 2], u64>,
}

impl Client {
    fn new(conn: Pin<Box<quiche::Connection>>, addr: &SocketAddr, id: ConnectionID) -> Client {
        Client { conn, h3_conn: None, addr: *addr, id, in_flight_queries: HashMap::new() }
    }

    fn create_http3_connection(&mut self) -> Result<()> {
        ensure!(self.h3_conn.is_none(), "HTTP/3 connection is already created");

        let config = quiche::h3::Config::new()?;
        let conn = quiche::h3::Connection::with_transport(&mut self.conn, &config)?;
        self.h3_conn = Some(conn);
        Ok(())
    }

    // Processes HTTP/3 request and returns the wire format DNS query or an empty vector.
    fn handle_http3_request(&mut self) -> Result<Vec<u8>> {
        ensure!(self.h3_conn.is_some(), "HTTP/3 connection not created");

        let h3_conn = self.h3_conn.as_mut().unwrap();
        let mut ret = vec![];

        loop {
            match h3_conn.poll(&mut self.conn) {
                Ok((stream_id, quiche::h3::Event::Headers { list, has_body })) => {
                    info!(
                        "Processing HTTP/3 Headers {:?} on stream id {} has_body {}",
                        list, stream_id, has_body
                    );

                    // Find ":path" field to get the query.
                    if let Some(target) = list.iter().find(|e| {
                        e.name() == b":path" && e.value().starts_with(URL_PATH_PREFIX.as_bytes())
                    }) {
                        let b64url_query = &target.value()[URL_PATH_PREFIX.len()..];
                        let decoded = base64::decode_config(b64url_query, base64::URL_SAFE_NO_PAD)?;
                        self.in_flight_queries.insert([decoded[0], decoded[1]], stream_id);
                        ret = decoded;
                    }
                }
                Ok((stream_id, quiche::h3::Event::Data)) => {
                    warn!("Received unexpected HTTP/3 data");
                    let mut buf = [0; 65535];
                    if let Ok(read) = h3_conn.recv_body(&mut self.conn, stream_id, &mut buf) {
                        warn!("Got {} bytes of response data on stream {}", read, stream_id);
                    }
                }
                Ok(n) => {
                    debug!("Got event {:?}", n);
                }
                Err(quiche::h3::Error::Done) => {
                    debug!("quiche::h3::Error::Done");
                    break;
                }
                Err(e) => bail!("HTTP/3 processing failed: {:?}", e),
            }
        }

        Ok(ret)
    }

    // Converts the clear-text DNS response to a DoH response, and sends it to the quiche.
    pub fn handle_backend_message(&mut self, response: &[u8]) -> Result<()> {
        ensure!(self.h3_conn.is_some(), "HTTP/3 connection not created");
        ensure!(response.len() >= DNS_HEADER_SIZE, "Insufficient bytes of DNS response");

        let len = response.len();
        let headers = vec![
            quiche::h3::Header::new(b":status", b"200"),
            quiche::h3::Header::new(b"content-type", b"application/dns-message"),
            quiche::h3::Header::new(b"content-length", &len.to_string().as_bytes()),
            // TODO: need to add cache-control?
        ];

        let h3_conn = self.h3_conn.as_mut().unwrap();
        let query_id = u16::from_be_bytes([response[0], response[1]]);
        let stream_id = self
            .in_flight_queries
            .remove(&[response[0], response[1]])
            .ok_or_else(|| anyhow!("query_id {:x} not found", query_id))?;

        info!("Preparing HTTP/3 response {:?} on stream {}", headers, stream_id);

        h3_conn.send_response(&mut self.conn, stream_id, &headers, false)?;
        h3_conn.send_body(&mut self.conn, stream_id, response, true)?;

        Ok(())
    }

    // Returns the data the client wants to send.
    pub fn flush_egress(&mut self) -> Result<Vec<u8>> {
        let mut ret = vec![];
        let mut buf = [0; MAX_UDP_PAYLOAD_SIZE];
        loop {
            let (write, _) = match self.conn.send(&mut buf) {
                Ok(v) => v,
                Err(quiche::Error::Done) => break,

                // Maybe close the connection?
                Err(e) => bail!(e),
            };
            ret.append(&mut buf[..write].to_vec());
        }

        Ok(ret)
    }

    // Processes the packet received from the frontend socket. If |data| is a DoH query,
    // the function returns the wire format DNS query; otherwise, it returns empty vector.
    pub fn handle_frontend_message(&mut self, data: &mut [u8]) -> Result<Vec<u8>> {
        let recv_info = quiche::RecvInfo { from: self.addr };
        self.conn.recv(data, recv_info)?;

        if (self.conn.is_in_early_data() || self.conn.is_established()) && self.h3_conn.is_none() {
            // Create a HTTP3 connection as soon as the QUIC connection is established.
            self.create_http3_connection()?;
            info!("HTTP/3 connection created");
        }

        if self.h3_conn.is_some() {
            return self.handle_http3_request();
        }

        Ok(vec![])
    }

    pub fn is_waiting_for_query(&self, query_id: &[u8; 2]) -> bool {
        self.in_flight_queries.contains_key(query_id)
    }

    pub fn addr(&self) -> SocketAddr {
        self.addr
    }

    pub fn connection_id(&self) -> &ConnectionID {
        self.id.as_ref()
    }

    pub fn timeout(&self) -> Option<Duration> {
        self.conn.timeout()
    }

    pub fn on_timeout(&mut self) {
        self.conn.on_timeout();
    }
}

impl std::fmt::Debug for Client {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        f.debug_struct("Client")
            .field("addr", &self.addr())
            .field("conn_id", &self.conn.trace_id())
            .finish()
    }
}

pub struct ClientMap {
    clients: HashMap<ConnectionID, Client>,
    conn_id_seed: hmac::Key,
    config: quiche::Config,
}

impl ClientMap {
    pub fn new(config: quiche::Config) -> Result<ClientMap> {
        let conn_id_seed = match hmac::Key::generate(hmac::HMAC_SHA256, &SystemRandom::new()) {
            Ok(v) => v,
            Err(e) => bail!("Failed to generate a seed: {}", e),
        };

        Ok(ClientMap { clients: HashMap::new(), conn_id_seed, config })
    }

    pub fn get_or_create(
        &mut self,
        hdr: &quiche::Header,
        addr: &SocketAddr,
    ) -> Result<&mut Client> {
        let dcid = hdr.dcid.as_ref().to_vec();
        let client = if !self.clients.contains_key(&dcid) {
            ensure!(hdr.ty == quiche::Type::Initial, "Packet is not Initial");
            ensure!(quiche::version_is_supported(hdr.version), "Protocol version not supported");

            let scid = generate_conn_id(&self.conn_id_seed, &dcid);
            let conn = quiche::accept(
                &quiche::ConnectionId::from_ref(&scid),
                None, /* odcid */
                *addr,
                &mut self.config,
            )?;
            let client = Client::new(conn, addr, scid.clone());

            info!("New client: {:?}", client);
            self.clients.insert(scid.clone(), client);
            self.clients.get_mut(&scid).unwrap()
        } else {
            self.clients.get_mut(&dcid).unwrap()
        };

        Ok(client)
    }

    pub fn get_mut(&mut self, id: &[u8]) -> Option<&mut Client> {
        self.clients.get_mut(&id.to_vec())
    }

    pub fn get_mut_iter(&mut self) -> hash_map::IterMut<ConnectionID, Client> {
        self.clients.iter_mut()
    }
}

fn generate_conn_id(conn_id_seed: &hmac::Key, dcid: &[u8]) -> ConnectionID {
    let conn_id = hmac::sign(conn_id_seed, dcid);
    let conn_id = &conn_id.as_ref()[..quiche::MAX_CONN_ID_LEN];
    conn_id.to_vec()
}
+30 −0
Original line number Diff line number Diff line
/*
 * 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.
 */

//! Runtime configuration for DohFrontend.

use std::default::Default;

#[derive(Debug, Default)]
pub struct Config {
    pub delay_queries: i32,
}

impl Config {
    pub fn new() -> Self {
        Default::default()
    }
}
Loading