aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorandroid-build-team Robot <android-build-team-robot@google.com>2021-04-09 21:11:16 +0000
committerandroid-build-team Robot <android-build-team-robot@google.com>2021-04-09 21:11:16 +0000
commit0063a5d094316314cd5c4469164a0cb7ef5157b5 (patch)
tree185981b61b5d74083492105827d6655ed4831181
parent9dec0a6d698e9d2cec4740c2ec1f81b1dd1a404e (diff)
parent334aa49d72a106ba6bdd01fd20e49d7e58610ec3 (diff)
downloadDnsResolver-android11-mainline-media-release.tar.gz
Snap for 7272189 from 334aa49d72a106ba6bdd01fd20e49d7e58610ec3 to mainline-media-releaseandroid-mainline-11.0.0_r39android11-mainline-media-release
Change-Id: Ib857848370e9aef95c3f7d80ea58d91795938fdd
-rw-r--r--Android.bp54
-rw-r--r--DnsTlsSocket.cpp2
-rw-r--r--cbindgen.toml36
-rw-r--r--doh.h47
-rw-r--r--doh.rs474
-rw-r--r--doh_ffi_test.cpp33
-rw-r--r--tests/resolv_integration_test.cpp4
7 files changed, 647 insertions, 3 deletions
diff --git a/Android.bp b/Android.bp
index 2bb2f9f3..8032a1b1 100644
--- a/Android.bp
+++ b/Android.bp
@@ -305,3 +305,57 @@ filegroup {
"PrivateDnsConfigurationTest.cpp",
],
}
+
+// rust_ffi_static {
+// name: "libdoh_ffi",
+// enabled: support_rust_toolchain,
+// crate_name: "doh",
+// srcs: ["doh.rs"],
+// edition: "2018",
+
+// rlibs: [
+// "libandroid_logger",
+// "libanyhow",
+// "liblazy_static",
+// "liblibc",
+// "liblog_rust",
+// "libquiche",
+// "libring",
+// "libtokio",
+// "liburl",
+// ],
+// prefer_rlib: true,
+
+// shared_libs: [
+// "libcrypto",
+// "libssl",
+// ],
+
+// apex_available: [
+// "//apex_available:platform", // Needed by doh_ffi_test
+// "com.android.resolv"
+// ],
+// min_sdk_version: "29",
+// }
+
+// cc_test {
+// name: "doh_ffi_test",
+// enabled: support_rust_toolchain,
+// test_suites: [
+// "general-tests",
+// ],
+// defaults: ["netd_defaults"],
+// srcs: ["doh_ffi_test.cpp"],
+// static_libs: [
+// "libdoh_ffi",
+// "libgmock",
+// "liblog",
+// "libring-core",
+// ],
+// // These are not carried over from libdoh_ffi.
+// shared_libs: [
+// "libcrypto",
+// "libssl",
+// ],
+// min_sdk_version: "29",
+// }
diff --git a/DnsTlsSocket.cpp b/DnsTlsSocket.cpp
index ad123160..80b64175 100644
--- a/DnsTlsSocket.cpp
+++ b/DnsTlsSocket.cpp
@@ -109,7 +109,7 @@ Status DnsTlsSocket::tcpConnect() {
sizeof(mServer.ss)) != 0 &&
errno != EINPROGRESS) {
const int err = errno;
- PLOG(ERROR) << "Socket failed to connect";
+ PLOG(WARNING) << "Socket failed to connect";
mSslFd.reset();
return Status(err);
}
diff --git a/cbindgen.toml b/cbindgen.toml
new file mode 100644
index 00000000..c9074692
--- /dev/null
+++ b/cbindgen.toml
@@ -0,0 +1,36 @@
+# For documentation, see: https://github.com/eqrion/cbindgen/blob/master/docs.md
+
+include_version = true
+braces = "SameLine"
+line_length = 100
+tab_width = 2
+language = "C++"
+pragma_once = true
+no_includes = true
+sys_includes = ["stdint.h", "sys/types.h"]
+header = "// This file is autogenerated by:\n// cbindgen --config cbindgen.toml doh.rs >doh.h\n// Don't modify manually."
+documentation = true
+style = "tag"
+
+[export]
+item_types = ["globals", "enums", "structs", "unions", "typedefs", "opaque", "functions", "constants"]
+
+[parse]
+parse_deps = true
+include = ["doh"]
+
+[fn]
+args = "Horizontal"
+
+[struct]
+associated_constants_in_body = true
+derive_eq = true
+derive_ostream = true
+
+[enum]
+add_sentinel = true
+derive_helper_methods = true
+derive_ostream = true
+
+[macro_expansion]
+bitflags = true
diff --git a/doh.h b/doh.h
new file mode 100644
index 00000000..d04feb45
--- /dev/null
+++ b/doh.h
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+// This file is autogenerated by:
+// cbindgen --config cbindgen.toml doh.rs >doh.h
+// Don't modify manually.
+
+#pragma once
+
+/* Generated with cbindgen:0.15.0 */
+
+#include <stdint.h>
+#include <sys/types.h>
+
+/// Context for a running DoH engine and associated thread.
+struct DohServer;
+
+extern "C" {
+
+/// Performs static initialization fo the DoH engine.
+const char* doh_init();
+
+/// Creates and returns a DoH engine instance.
+/// The returned object must be freed with doh_delete().
+DohServer* doh_new(const char* url, const char* ip_addr, uint32_t mark, const char* cert_path);
+
+/// Deletes a DoH engine created by doh_new().
+void doh_delete(DohServer* doh);
+
+/// Sends a DNS query and waits for the response.
+ssize_t doh_query(DohServer* doh, uint8_t* query, size_t query_len, uint8_t* response,
+ size_t response_len);
+
+} // extern "C"
diff --git a/doh.rs b/doh.rs
new file mode 100644
index 00000000..babdeef2
--- /dev/null
+++ b/doh.rs
@@ -0,0 +1,474 @@
+/*
+ * 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>> {
+ let url = Url::parse(&url.to_string())?;
+ if url.domain().is_none() {
+ return Err(anyhow!("no domain"));
+ }
+ // Setup socket
+ let udp_sk = make_doh_udp_socket(&ip_addr, mark)?;
+
+ // 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
+ }
+ }
+}
diff --git a/doh_ffi_test.cpp b/doh_ffi_test.cpp
new file mode 100644
index 00000000..5004156a
--- /dev/null
+++ b/doh_ffi_test.cpp
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+#include "doh.h"
+
+#include <gmock/gmock-matchers.h>
+#include <gtest/gtest.h>
+
+TEST(DoHFFITest, SmokeTest) {
+ EXPECT_STREQ(doh_init(), "1.0");
+ DohServer* doh = doh_new("https://dns.google/dns-query", "8.8.8.8", 0, "");
+ EXPECT_TRUE(doh != nullptr);
+
+ // www.example.com
+ uint8_t query[] = "q80BAAABAAAAAAAAA3d3dwdleGFtcGxlA2NvbQAAAQAB";
+ uint8_t answer[8192];
+ ssize_t len = doh_query(doh, query, sizeof query, answer, sizeof answer);
+ EXPECT_GT(len, 0);
+ doh_delete(doh);
+}
diff --git a/tests/resolv_integration_test.cpp b/tests/resolv_integration_test.cpp
index e94e3322..5efa13e5 100644
--- a/tests/resolv_integration_test.cpp
+++ b/tests/resolv_integration_test.cpp
@@ -5499,8 +5499,8 @@ TEST_F(ResolverTest, DnsServerSelection) {
EXPECT_GT(dns1Count, 0);
EXPECT_GT(dns2Count, 0);
EXPECT_GT(dns3Count, 0);
- EXPECT_GT(dns1Count, dns2Count);
- EXPECT_GT(dns2Count, dns3Count);
+ EXPECT_GE(dns1Count, dns2Count);
+ EXPECT_GE(dns2Count, dns3Count);
const int averageTime = accumulatedTime / queryNum;
LOG(INFO) << "ResolverTest#DnsServerSelection: averageTime " << averageTime << "us";