/* * 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 #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 #include #ifdef HAVE_NGHTTP2 #include 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("-n no-tls, TLS is disabled\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"); snprintf(h2_stream->path, strlen(h2_session->endpoint)+ strlen("?dns=")+qb64_size+1, "%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"; if(h2_session->ssl) headers[2].value = (uint8_t*)"https"; else headers[2].value = (uint8_t*)"http"; 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; isession, 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 */ free(qinfo.qname); 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; ssize_t ret; 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; } if(h2_session->ssl) { 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; } ret = read(h2_session->fd, buf, len); if(ret == 0) { return NGHTTP2_ERR_EOF; } else if(ret < 0) { log_err("could not http2 read: %s", strerror(errno)); return NGHTTP2_ERR_EOF; } return ret; } 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; ssize_t ret; if(h2_session->ssl) { 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; } ret = write(h2_session->fd, buf, len); if(ret == 0) { return NGHTTP2_ERR_CALLBACK_FAILURE; } else if(ret < 0) { log_err("could not http2 write: %s", strerror(errno)); return NGHTTP2_ERR_CALLBACK_FAILURE; } return ret; } 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; } if(sldns_buffer_remaining(h2_stream->buf) < len) { log_err("received data chunk does not fit into buffer"); return NGHTTP2_ERR_CALLBACK_FAILURE; } 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"); free(h2_session); 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); nghttp2_session_callbacks_del(callbacks); 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 no_tls, 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; if(!no_tls) { ctx = connect_sslctx_create(NULL, NULL, NULL, 0); if(!ctx) fatal_exit("cannot create ssl ctx"); #ifdef HAVE_SSL_CTX_SET_ALPN_PROTOS SSL_CTX_set_alpn_protos(ctx, (const unsigned char *)"\x02h2", 3); #endif 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; /* handle query */ for(i=0; iquery_count) { http2_read(h2_session); http2_write(h2_session); } /* shutdown */ http2_session_delete(h2_session); if(ssl) { SSL_shutdown(ssl); SSL_free(ssl); } if(ctx) { SSL_CTX_free(ctx); } sock_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, no_tls = 0; struct http2_session* h2_session; #ifdef USE_WINSOCK WSADATA wsa_data; if(WSAStartup(MAKEWORD(2,2), &wsa_data) != 0) { printf("WSAStartup failed\n"); return 1; } #endif checklock_start(); log_init(0, 0, 0); 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:hns:p:P")) != -1) { switch(c) { case 'c': h2_session->content_type = optarg; break; case 'e': h2_session->endpoint = optarg; break; case 'n': no_tls = 1; 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; } if(!no_tls) { #if OPENSSL_VERSION_NUMBER < 0x10100000 || !defined(HAVE_OPENSSL_INIT_SSL) ERR_load_SSL_strings(); #endif #if OPENSSL_VERSION_NUMBER < 0x10100000 || !defined(HAVE_OPENSSL_INIT_CRYPTO) # ifndef S_SPLINT_S OpenSSL_add_all_algorithms(); # endif #else OPENSSL_init_crypto(OPENSSL_INIT_ADD_ALL_CIPHERS | OPENSSL_INIT_ADD_ALL_DIGESTS | OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL); #endif #if OPENSSL_VERSION_NUMBER < 0x10100000 || !defined(HAVE_OPENSSL_INIT_SSL) (void)SSL_library_init(); #else (void)OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS, NULL); #endif } run(h2_session, port, no_tls, argc, argv); checklock_stop(); #ifdef USE_WINSOCK WSACleanup(); #endif return 0; } #else int main(int ATTR_UNUSED(argc), char** ATTR_UNUSED(argv)) { printf("Compiled without nghttp2, cannot run test.\n"); return 1; } #endif /* HAVE_NGHTTP2 */