• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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