]> git.ipfire.org Git - thirdparty/unbound.git/commitdiff
- Add dohclient, for use in tests
authorRalph Dolmans <ralph@nlnetlabs.nl>
Wed, 24 Jun 2020 12:59:56 +0000 (14:59 +0200)
committerRalph Dolmans <ralph@nlnetlabs.nl>
Wed, 24 Jun 2020 12:59:56 +0000 (14:59 +0200)
testcode/dohclient.c [new file with mode: 0644]

diff --git a/testcode/dohclient.c b/testcode/dohclient.c
new file mode 100644 (file)
index 0000000..10f3658
--- /dev/null
@@ -0,0 +1,577 @@
+/*
+ * testcode/dohclient.c - debug program. Perform multiple DNS queries using DoH.
+ *
+ * Copyright (c) 2020, NLnet Labs. All rights reserved.
+ *
+ * This software is open source.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * 
+ * Neither the name of the NLNET LABS nor the names of its contributors may
+ * be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/** 
+ * \file
+ *
+ * Simple DNS-over-HTTPS client. For testing and debugging purposes.
+ * No authentication of TLS cert.
+ */
+
+#include "config.h"
+#ifdef HAVE_GETOPT_H
+#include <getopt.h>
+#endif
+#include "sldns/wire2str.h"
+#include "sldns/sbuffer.h"
+#include "sldns/str2wire.h"
+#include "sldns/parseutil.h"
+#include "util/data/msgencode.h"
+#include "util/data/msgreply.h"
+#include "util/data/msgparse.h"
+#include "util/net_help.h"
+#include <openssl/ssl.h>
+#include <openssl/err.h>
+#ifdef HAVE_NGHTTP2
+#include <nghttp2/nghttp2.h>
+
+struct http2_session {
+       nghttp2_session* session;
+       SSL* ssl;
+       int fd;
+       int query_count;
+       /* Use POST :method if 1 */
+       int post;
+       int block_select;
+       const char* authority;
+       const char* endpoint;
+       const char* content_type;
+};
+
+struct http2_stream {
+       int32_t stream_id;
+       int res_status;
+       struct sldns_buffer* buf;
+       char* path;
+};
+
+static void usage(char* argv[])
+{
+       printf("usage: %s [options] name type class ...\n", argv[0]);
+       printf("        sends the name-type-class queries over "
+                       "DNS-over-HTTPS.\n");
+       printf("-s server       IP address to send the queries to, "
+                       "default: 127.0.0.1\n");
+       printf("-p              Port to connect to, default: %d\n",
+               UNBOUND_DNS_OVER_HTTPS_PORT);
+       printf("-P              Use POST method instead of default GET\n");
+       printf("-e              HTTP endpoint, default: /dns-query\n");
+       printf("-c              Content-type in request, default: "
+               "application/dns-message\n");
+       printf("-h              This help text\n");
+       exit(1);
+}
+
+/** open TCP socket to svr */
+static int
+open_svr(const char* svr, int port)
+{
+       struct sockaddr_storage addr;
+       socklen_t addrlen;
+       int fd = -1;
+       int r;
+       if(!ipstrtoaddr(svr, port, &addr, &addrlen)) {
+               printf("fatal: bad server specs '%s'\n", svr);
+               exit(1);
+       }
+
+       fd = socket(addr_is_ip6(&addr, addrlen)?PF_INET6:PF_INET,
+               SOCK_STREAM, 0);
+       if(fd == -1) {
+               perror("socket() error");
+               exit(1);
+       }
+       r = connect(fd, (struct sockaddr*)&addr, addrlen);
+       if(r < 0 && r != EINPROGRESS) {
+               perror("connect() error");
+               exit(1);
+       }
+       return fd;
+}
+
+static ssize_t http2_submit_request_read_cb(
+       nghttp2_session* ATTR_UNUSED(session),
+       int32_t ATTR_UNUSED(stream_id), uint8_t* buf, size_t length,
+       uint32_t* data_flags, nghttp2_data_source* source,
+       void* ATTR_UNUSED(cb_arg))
+{
+       if(length > sldns_buffer_remaining(source->ptr))
+               length = sldns_buffer_remaining(source->ptr);
+
+       memcpy(buf, sldns_buffer_current(source->ptr), length);
+       sldns_buffer_skip(source->ptr, length);
+
+       if(sldns_buffer_remaining(source->ptr) == 0) {
+               *data_flags |= NGHTTP2_DATA_FLAG_EOF;
+       }
+
+       return length;
+}
+
+static void
+submit_query(struct http2_session* h2_session, struct sldns_buffer* buf)
+{
+       int32_t stream_id;
+       struct http2_stream* h2_stream;
+       nghttp2_nv headers[5];
+       char* qb64;
+       size_t qb64_size;
+       size_t qb64_expected_size;
+       size_t i;
+       nghttp2_data_provider data_prd;
+
+       h2_stream = calloc(1,  sizeof(*h2_stream));
+       if(!h2_stream)
+               fatal_exit("could not malloc http2 stream");
+       h2_stream->buf = buf;
+
+       if(h2_session->post) {
+               data_prd.source.ptr = buf;
+               data_prd.read_callback = http2_submit_request_read_cb;
+               h2_stream->path = (char*)h2_session->endpoint;
+       } else {
+               qb64_expected_size = sldns_b64_ntop_calculate_size(
+                               sldns_buffer_remaining(buf));
+               qb64 = malloc(qb64_expected_size);
+               if(!qb64) fatal_exit("out of memory");
+               qb64_size = sldns_b64url_ntop(sldns_buffer_begin(buf),
+                       sldns_buffer_remaining(buf), qb64, qb64_expected_size);
+               h2_stream->path = malloc(strlen(
+                       h2_session->endpoint)+strlen("?dns=")+qb64_size+1);
+               if(!h2_stream->path) fatal_exit("out of memory");
+               sprintf(h2_stream->path, "%s?dns=%s", h2_session->endpoint,
+                       qb64);
+               free(qb64);
+       }
+
+       headers[0].name = (uint8_t*)":method";
+       if(h2_session->post)
+               headers[0].value = (uint8_t*)"POST";
+       else
+               headers[0].value = (uint8_t*)"GET";
+       headers[1].name = (uint8_t*)":path";
+       headers[1].value = (uint8_t*)h2_stream->path;
+       headers[2].name = (uint8_t*)":scheme";
+       headers[2].value = (uint8_t*)"https";
+       headers[3].name = (uint8_t*)":authority";
+       headers[3].value = (uint8_t*)h2_session->authority;
+       headers[4].name = (uint8_t*)"content-type";
+       headers[4].value = (uint8_t*)h2_session->content_type;
+
+       printf("Request headers\n");
+       for(i=0; i<sizeof(headers)/sizeof(headers[0]); i++) {
+               headers[i].namelen = strlen((char*)headers[i].name);
+               headers[i].valuelen = strlen((char*)headers[i].value);
+               headers[i].flags = NGHTTP2_NV_FLAG_NONE;
+               printf("%s: %s\n", headers[i].name, headers[i].value);
+       }
+
+       stream_id = nghttp2_submit_request(h2_session->session, NULL, headers,
+               sizeof(headers)/sizeof(headers[0]),
+               (h2_session->post) ? &data_prd : NULL, h2_stream);
+       if(stream_id < 0) {
+               printf("Failed to submit nghttp2 request");
+               exit(1);
+       }
+       h2_session->query_count++;
+       h2_stream->stream_id = stream_id;
+}
+
+static sldns_buffer*
+make_query(char* qname, char* qtype, char* qclass)
+{
+       struct query_info qinfo;
+       struct edns_data edns;
+       sldns_buffer* buf = sldns_buffer_new(65553);
+       if(!buf) fatal_exit("out of memory");
+       qinfo.qname = sldns_str2wire_dname(qname, &qinfo.qname_len);
+       if(!qinfo.qname) {
+               printf("cannot parse query name: '%s'\n", qname);
+               exit(1);
+       }
+
+       qinfo.qtype = sldns_get_rr_type_by_name(qtype);
+       qinfo.qclass = sldns_get_rr_class_by_name(qclass);
+       qinfo.local_alias = NULL;
+
+       qinfo_query_encode(buf, &qinfo); /* flips buffer */
+       sldns_buffer_write_u16_at(buf, 0, 0x0000);
+       sldns_buffer_write_u16_at(buf, 2, BIT_RD);
+       memset(&edns, 0, sizeof(edns));
+       edns.edns_present = 1;
+       edns.bits = EDNS_DO;
+       edns.udp_size = 4096;
+       if(sldns_buffer_capacity(buf) >=
+               sldns_buffer_limit(buf)+calc_edns_field_size(&edns))
+               attach_edns_record(buf, &edns);
+       return buf;
+}
+
+static ssize_t http2_recv_cb(nghttp2_session* ATTR_UNUSED(session),
+       uint8_t* buf, size_t len, int ATTR_UNUSED(flags), void* cb_arg)
+{
+       struct http2_session* h2_session = (struct http2_session*)cb_arg;
+       int r;
+       struct timeval tv, *waittv;
+       fd_set rfd;
+       ERR_clear_error();
+
+       memset(&tv, 0, sizeof(tv));
+
+       if(h2_session->block_select && h2_session->query_count <= 0) {
+               return NGHTTP2_ERR_WOULDBLOCK;
+       }
+       if(h2_session->block_select)
+               waittv = NULL;
+       else
+               waittv = &tv;
+       memset(&rfd, 0, sizeof(rfd));
+       FD_ZERO(&rfd);
+       FD_SET(h2_session->fd, &rfd);
+       r = select(h2_session->fd+1, &rfd, NULL, NULL, waittv);
+       if(r <= 0) {
+               return NGHTTP2_ERR_WOULDBLOCK;
+       }
+
+       r = SSL_read(h2_session->ssl, buf, len);
+       if(r <= 0) {
+               int want = SSL_get_error(h2_session->ssl, r);
+               if(want == SSL_ERROR_ZERO_RETURN) {
+                       return NGHTTP2_ERR_EOF;
+               }
+               log_crypto_err("could not SSL_read");
+               return NGHTTP2_ERR_EOF;
+       }
+       return r;
+}
+
+static ssize_t http2_send_cb(nghttp2_session* ATTR_UNUSED(session),
+       const uint8_t* buf, size_t len, int ATTR_UNUSED(flags), void* cb_arg)
+{
+       struct http2_session* h2_session = (struct http2_session*)cb_arg;
+
+       int r;
+       ERR_clear_error();
+       r = SSL_write(h2_session->ssl, buf, len);
+       if(r <= 0) {
+               int want = SSL_get_error(h2_session->ssl, r);
+               if(want == SSL_ERROR_ZERO_RETURN) {
+                       return NGHTTP2_ERR_CALLBACK_FAILURE;
+               }
+               log_crypto_err("could not SSL_write");
+               return NGHTTP2_ERR_CALLBACK_FAILURE;
+       }
+       return r;
+}
+
+static int http2_stream_close_cb(nghttp2_session* ATTR_UNUSED(session),
+       int32_t ATTR_UNUSED(stream_id),
+       nghttp2_error_code ATTR_UNUSED(error_code), void *cb_arg)
+{
+       struct http2_session* h2_session = (struct http2_session*)cb_arg;
+       struct http2_stream* h2_stream;
+       if(!(h2_stream = nghttp2_session_get_stream_user_data(
+               h2_session->session, stream_id))) {
+               return 0;
+       }
+       h2_session->query_count--;
+       sldns_buffer_free(h2_stream->buf);
+       if(!h2_session->post)
+               free(h2_stream->path);
+       free(h2_stream);
+       h2_stream = NULL;
+       return 0;
+}
+
+static int http2_data_chunk_recv_cb(nghttp2_session* ATTR_UNUSED(session),
+       uint8_t ATTR_UNUSED(flags), int32_t stream_id, const uint8_t* data,
+       size_t len, void* cb_arg)
+{
+       struct http2_session* h2_session = (struct http2_session*)cb_arg;
+       struct http2_stream* h2_stream;
+
+       if(!(h2_stream = nghttp2_session_get_stream_user_data(
+               h2_session->session, stream_id))) {
+               return 0;
+       }
+
+       sldns_buffer_write(h2_stream->buf, data, len);
+
+       return 0;
+}
+
+static int http2_frame_recv_cb(nghttp2_session *session,
+       const nghttp2_frame *frame, void* ATTR_UNUSED(cb_arg))
+{
+       struct http2_stream* h2_stream;
+
+       if(!(h2_stream = nghttp2_session_get_stream_user_data(
+               session, frame->hd.stream_id)))
+               return 0;
+       if(frame->hd.type == NGHTTP2_HEADERS &&
+               frame->headers.cat == NGHTTP2_HCAT_RESPONSE) {
+               sldns_buffer_clear(h2_stream->buf);
+       }
+       if(((frame->hd.type != NGHTTP2_DATA &&
+               frame->hd.type != NGHTTP2_HEADERS) ||
+               frame->hd.flags & NGHTTP2_FLAG_END_STREAM) &&
+                       h2_stream->res_status == 200) {
+                       char* pktstr;
+                       sldns_buffer_flip(h2_stream->buf);
+                       pktstr = sldns_wire2str_pkt(
+                               sldns_buffer_begin(h2_stream->buf),
+                               sldns_buffer_limit(h2_stream->buf));
+                       printf("%s\n", pktstr);
+                       free(pktstr);
+                       return 0;
+       }
+       return 0;
+}
+static int http2_header_cb(nghttp2_session* ATTR_UNUSED(session),
+       const nghttp2_frame* frame, const uint8_t* name, size_t namelen,
+       const uint8_t* value, size_t ATTR_UNUSED(valuelen),
+       uint8_t ATTR_UNUSED(flags), void* cb_arg)
+{
+       struct http2_stream* h2_stream;
+       struct http2_session* h2_session = (struct http2_session*)cb_arg;
+       printf("%s %s\n", name, value);
+       if(namelen == 7 && memcmp(":status", name, namelen) == 0) {
+               if(!(h2_stream = nghttp2_session_get_stream_user_data(
+                       h2_session->session, frame->hd.stream_id))) {
+                       return 0;
+               }
+               h2_stream->res_status = atoi((char*)value);
+       }
+       return 0;
+}
+
+static struct http2_session*
+http2_session_create()
+{
+       struct http2_session* h2_session = calloc(1,
+               sizeof(struct http2_session));
+       nghttp2_session_callbacks* callbacks;
+       if(!h2_session)
+               fatal_exit("out of memory");
+
+       if(nghttp2_session_callbacks_new(&callbacks) == NGHTTP2_ERR_NOMEM) {
+               log_err("failed to initialize nghttp2 callback");
+               return NULL;
+       }
+       nghttp2_session_callbacks_set_recv_callback(callbacks, http2_recv_cb);
+       nghttp2_session_callbacks_set_send_callback(callbacks, http2_send_cb);
+       nghttp2_session_callbacks_set_on_stream_close_callback(callbacks,
+               http2_stream_close_cb);
+       nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks,
+               http2_data_chunk_recv_cb);
+       nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks,
+               http2_frame_recv_cb);
+       nghttp2_session_callbacks_set_on_header_callback(callbacks,
+               http2_header_cb);
+       nghttp2_session_client_new(&h2_session->session, callbacks, h2_session);
+       return h2_session;
+}
+
+static void
+http2_session_delete(struct http2_session* h2_session)
+{
+       nghttp2_session_del(h2_session->session);
+       free(h2_session);
+}
+
+static void
+http2_submit_setting(struct http2_session* h2_session)
+{
+       int ret;
+       nghttp2_settings_entry settings[1] = {
+               {NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS,
+                100}};
+
+       ret = nghttp2_submit_settings(h2_session->session, NGHTTP2_FLAG_NONE,
+               settings, 1);
+       if(ret) {
+               printf("http2: submit_settings failed, "
+                       "error: %s\n", nghttp2_strerror(ret));
+               exit(1);
+       }
+}
+
+static void
+http2_write(struct http2_session* h2_session)
+{
+       if(nghttp2_session_want_write(h2_session->session)) {
+               if(nghttp2_session_send(h2_session->session)) {
+                       printf("nghttp2 session send failed\n");
+                       exit(1);
+               }
+       }
+}
+
+static void
+http2_read(struct http2_session* h2_session)
+{
+       if(nghttp2_session_want_read(h2_session->session)) {
+               if(nghttp2_session_recv(h2_session->session)) {
+                       printf("nghttp2 session mem_recv failed\n");
+                       exit(1);
+               }
+       }
+}
+
+static void
+run(struct http2_session* h2_session, int port, int count, char** q)
+{
+       int i;
+       SSL_CTX* ctx = NULL;
+       SSL* ssl = NULL;
+       int fd;
+       struct sldns_buffer* buf = NULL;
+
+       fd = open_svr(h2_session->authority, port);
+       h2_session->fd = fd;
+
+       ctx = connect_sslctx_create(NULL, NULL, NULL, 0);
+       if(!ctx) fatal_exit("cannot create ssl ctx");
+       SSL_CTX_set_alpn_protos(ctx, (const unsigned char *)"\x02h2", 3);
+       ssl = outgoing_ssl_fd(ctx, fd);
+       if(!ssl) {
+               printf("cannot create ssl\n");
+               exit(1);
+       }
+       h2_session->ssl = ssl;
+       while(1) {
+               int r;
+               ERR_clear_error();
+               if( (r=SSL_do_handshake(ssl)) == 1)
+                       break;
+               r = SSL_get_error(ssl, r);
+               if(r != SSL_ERROR_WANT_READ &&
+                       r != SSL_ERROR_WANT_WRITE) {
+                       log_crypto_err("could not ssl_handshake");
+                       exit(1);
+               }
+       }
+
+       http2_submit_setting(h2_session);
+       http2_write(h2_session);
+       http2_read(h2_session); /* Read setting from remote peer */
+
+       h2_session->block_select = 1;
+
+       /* hande query */
+       for(i=0; i<count; i+=3) {
+               buf = make_query(q[i], q[i+1], q[i+2]);
+               submit_query(h2_session, buf);
+       }
+       http2_write(h2_session);
+       while(h2_session->query_count) {
+               http2_read(h2_session);
+               http2_write(h2_session);
+       }
+
+       /* shutdown */
+       http2_session_delete(h2_session);
+       SSL_shutdown(ssl);
+       SSL_free(ssl);
+       SSL_CTX_free(ctx);
+       close(fd);
+}
+
+/** getopt global, in case header files fail to declare it. */
+extern int optind;
+/** getopt global, in case header files fail to declare it. */
+extern char* optarg;
+int main(int argc, char** argv)
+{
+       int c;
+       int port = UNBOUND_DNS_OVER_HTTPS_PORT;
+       struct http2_session* h2_session = http2_session_create();
+       if(!h2_session) fatal_exit("out of memory");
+
+       if(argc == 1) {
+               usage(argv);
+       }
+       
+       h2_session->authority = "127.0.0.1";
+       h2_session->post = 0;
+       h2_session->endpoint = "/dns-query";
+       h2_session->content_type = "application/dns-message";
+
+       while((c=getopt(argc, argv, "c:e:hs:p:P")) != -1) {
+               switch(c) {
+                       case 'c':
+                               h2_session->content_type = optarg;
+                               break;
+                       case 'e':
+                               h2_session->endpoint = optarg;
+                               break;
+                       case 'p':
+                               if(atoi(optarg)==0 && strcmp(optarg,"0")!=0) {
+                                       printf("error parsing port, "
+                                           "number expected: %s\n", optarg);
+                                       return 1;
+                               }
+                               port = atoi(optarg);
+                               break;
+                       case 'P':
+                               h2_session->post = 1;
+                               break;
+                       case 's':
+                               h2_session->authority = optarg;
+                               break;
+                       case 'h':
+                       case '?':
+                       default:
+                               usage(argv);
+               }
+       }
+       argc -= optind;
+       argv += optind;
+       if(argc%3!=0) {
+               printf("Invalid input. Specify qname, qtype, and qclass.\n");
+               return 1;
+       }
+
+
+       run(h2_session, port, argc, argv);
+
+       return 0;
+}
+#else
+int main(int ATTR_UNUSED(argc), char** ATTR_UNUSED(argv))
+{
+       printf("Compiled without nghttp2, cannot run test.\n");
+}
+#endif /*  HAVE_NGHTTP2 */