diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/Makefile.am | 8 | ||||
-rw-r--r-- | src/Makefile.in | 32 | ||||
-rw-r--r-- | src/config.h.in | 6 | ||||
-rw-r--r-- | src/dns.c | 5 | ||||
-rw-r--r-- | src/dnsperf.1.in | 33 | ||||
-rw-r--r-- | src/dnsperf.c | 50 | ||||
-rw-r--r-- | src/net.c | 42 | ||||
-rw-r--r-- | src/net.h | 26 | ||||
-rw-r--r-- | src/net_doh.c | 1017 | ||||
-rw-r--r-- | src/net_dot.c | 13 | ||||
-rw-r--r-- | src/net_tcp.c | 6 | ||||
-rw-r--r-- | src/net_udp.c | 6 | ||||
-rw-r--r-- | src/opt.c | 140 | ||||
-rw-r--r-- | src/opt.h | 5 | ||||
-rw-r--r-- | src/parse_uri.c | 112 | ||||
-rw-r--r-- | src/parse_uri.h | 41 | ||||
-rw-r--r-- | src/resperf.1.in | 19 | ||||
-rw-r--r-- | src/resperf.c | 98 | ||||
-rw-r--r-- | src/test/Makefile.in | 2 | ||||
-rwxr-xr-x | src/test/test2.sh | 3 | ||||
-rwxr-xr-x | src/test/test5.sh | 6 | ||||
-rw-r--r-- | src/util.h | 14 |
22 files changed, 1598 insertions, 86 deletions
diff --git a/src/Makefile.am b/src/Makefile.am index d08b272..45d2dec 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -30,19 +30,19 @@ bin_PROGRAMS = dnsperf resperf dist_bin_SCRIPTS = resperf-report _libperf_sources = datafile.c dns.c log.c net.c opt.c os.c strerror.c qtype.c \ - edns.c tsig.c net_udp.c net_tcp.c net_dot.c + edns.c tsig.c net_udp.c net_tcp.c net_dot.c net_doh.c parse_uri.c _libperf_headers = datafile.h dns.h log.h net.h opt.h os.h util.h strerror.h \ - list.h result.h buffer.h qtype.h edns.h tsig.h + list.h result.h buffer.h qtype.h edns.h tsig.h parse_uri.h dnsperf_SOURCES = $(_libperf_sources) dnsperf.c dist_dnsperf_SOURCES = $(_libperf_headers) dnsperf_LDADD = $(PTHREAD_LIBS) $(libssl_LIBS) $(libcrypto_LIBS) \ - $(libldns_LIBS) + $(libldns_LIBS) $(libnghttp2_LIBS) resperf_SOURCES = $(_libperf_sources) resperf.c dist_resperf_SOURCES = $(_libperf_headers) resperf_LDADD = $(PTHREAD_LIBS) $(libssl_LIBS) $(libcrypto_LIBS) \ - $(libldns_LIBS) + $(libldns_LIBS) $(libnghttp2_LIBS) man1_MANS = dnsperf.1 resperf.1 diff --git a/src/Makefile.in b/src/Makefile.in index d129516..335c659 100644 --- a/src/Makefile.in +++ b/src/Makefile.in @@ -130,14 +130,16 @@ PROGRAMS = $(bin_PROGRAMS) am__objects_1 = datafile.$(OBJEXT) dns.$(OBJEXT) log.$(OBJEXT) \ net.$(OBJEXT) opt.$(OBJEXT) os.$(OBJEXT) strerror.$(OBJEXT) \ qtype.$(OBJEXT) edns.$(OBJEXT) tsig.$(OBJEXT) \ - net_udp.$(OBJEXT) net_tcp.$(OBJEXT) net_dot.$(OBJEXT) + net_udp.$(OBJEXT) net_tcp.$(OBJEXT) net_dot.$(OBJEXT) \ + net_doh.$(OBJEXT) parse_uri.$(OBJEXT) am_dnsperf_OBJECTS = $(am__objects_1) dnsperf.$(OBJEXT) am__objects_2 = dist_dnsperf_OBJECTS = $(am__objects_2) dnsperf_OBJECTS = $(am_dnsperf_OBJECTS) $(dist_dnsperf_OBJECTS) am__DEPENDENCIES_1 = dnsperf_DEPENDENCIES = $(am__DEPENDENCIES_1) $(am__DEPENDENCIES_1) \ - $(am__DEPENDENCIES_1) $(am__DEPENDENCIES_1) + $(am__DEPENDENCIES_1) $(am__DEPENDENCIES_1) \ + $(am__DEPENDENCIES_1) AM_V_lt = $(am__v_lt_@AM_V@) am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@) am__v_lt_0 = --silent @@ -146,7 +148,8 @@ am_resperf_OBJECTS = $(am__objects_1) resperf.$(OBJEXT) dist_resperf_OBJECTS = $(am__objects_2) resperf_OBJECTS = $(am_resperf_OBJECTS) $(dist_resperf_OBJECTS) resperf_DEPENDENCIES = $(am__DEPENDENCIES_1) $(am__DEPENDENCIES_1) \ - $(am__DEPENDENCIES_1) $(am__DEPENDENCIES_1) + $(am__DEPENDENCIES_1) $(am__DEPENDENCIES_1) \ + $(am__DEPENDENCIES_1) am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`; am__vpath_adj = case $$p in \ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \ @@ -192,9 +195,10 @@ depcomp = $(SHELL) $(top_srcdir)/depcomp am__maybe_remake_depfiles = depfiles am__depfiles_remade = ./$(DEPDIR)/datafile.Po ./$(DEPDIR)/dns.Po \ ./$(DEPDIR)/dnsperf.Po ./$(DEPDIR)/edns.Po ./$(DEPDIR)/log.Po \ - ./$(DEPDIR)/net.Po ./$(DEPDIR)/net_dot.Po \ - ./$(DEPDIR)/net_tcp.Po ./$(DEPDIR)/net_udp.Po \ - ./$(DEPDIR)/opt.Po ./$(DEPDIR)/os.Po ./$(DEPDIR)/qtype.Po \ + ./$(DEPDIR)/net.Po ./$(DEPDIR)/net_doh.Po \ + ./$(DEPDIR)/net_dot.Po ./$(DEPDIR)/net_tcp.Po \ + ./$(DEPDIR)/net_udp.Po ./$(DEPDIR)/opt.Po ./$(DEPDIR)/os.Po \ + ./$(DEPDIR)/parse_uri.Po ./$(DEPDIR)/qtype.Po \ ./$(DEPDIR)/resperf.Po ./$(DEPDIR)/strerror.Po \ ./$(DEPDIR)/tsig.Po am__mv = mv -f @@ -405,6 +409,8 @@ libdir = @libdir@ libexecdir = @libexecdir@ libldns_CFLAGS = @libldns_CFLAGS@ libldns_LIBS = @libldns_LIBS@ +libnghttp2_CFLAGS = @libnghttp2_CFLAGS@ +libnghttp2_LIBS = @libnghttp2_LIBS@ libssl_CFLAGS = @libssl_CFLAGS@ libssl_LIBS = @libssl_LIBS@ localedir = @localedir@ @@ -435,20 +441,20 @@ AM_CFLAGS = -I$(srcdir) \ EXTRA_DIST = dnsperf.1.in resperf-report resperf.1.in dist_bin_SCRIPTS = resperf-report _libperf_sources = datafile.c dns.c log.c net.c opt.c os.c strerror.c qtype.c \ - edns.c tsig.c net_udp.c net_tcp.c net_dot.c + edns.c tsig.c net_udp.c net_tcp.c net_dot.c net_doh.c parse_uri.c _libperf_headers = datafile.h dns.h log.h net.h opt.h os.h util.h strerror.h \ - list.h result.h buffer.h qtype.h edns.h tsig.h + list.h result.h buffer.h qtype.h edns.h tsig.h parse_uri.h dnsperf_SOURCES = $(_libperf_sources) dnsperf.c dist_dnsperf_SOURCES = $(_libperf_headers) dnsperf_LDADD = $(PTHREAD_LIBS) $(libssl_LIBS) $(libcrypto_LIBS) \ - $(libldns_LIBS) + $(libldns_LIBS) $(libnghttp2_LIBS) resperf_SOURCES = $(_libperf_sources) resperf.c dist_resperf_SOURCES = $(_libperf_headers) resperf_LDADD = $(PTHREAD_LIBS) $(libssl_LIBS) $(libcrypto_LIBS) \ - $(libldns_LIBS) + $(libldns_LIBS) $(libnghttp2_LIBS) man1_MANS = dnsperf.1 resperf.1 all: config.h @@ -605,11 +611,13 @@ distclean-compile: @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/edns.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/log.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/net.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/net_doh.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/net_dot.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/net_tcp.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/net_udp.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/opt.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/os.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/parse_uri.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/qtype.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/resperf.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/strerror.Po@am__quote@ # am--include-marker @@ -902,11 +910,13 @@ distclean: distclean-recursive -rm -f ./$(DEPDIR)/edns.Po -rm -f ./$(DEPDIR)/log.Po -rm -f ./$(DEPDIR)/net.Po + -rm -f ./$(DEPDIR)/net_doh.Po -rm -f ./$(DEPDIR)/net_dot.Po -rm -f ./$(DEPDIR)/net_tcp.Po -rm -f ./$(DEPDIR)/net_udp.Po -rm -f ./$(DEPDIR)/opt.Po -rm -f ./$(DEPDIR)/os.Po + -rm -f ./$(DEPDIR)/parse_uri.Po -rm -f ./$(DEPDIR)/qtype.Po -rm -f ./$(DEPDIR)/resperf.Po -rm -f ./$(DEPDIR)/strerror.Po @@ -966,11 +976,13 @@ maintainer-clean: maintainer-clean-recursive -rm -f ./$(DEPDIR)/edns.Po -rm -f ./$(DEPDIR)/log.Po -rm -f ./$(DEPDIR)/net.Po + -rm -f ./$(DEPDIR)/net_doh.Po -rm -f ./$(DEPDIR)/net_dot.Po -rm -f ./$(DEPDIR)/net_tcp.Po -rm -f ./$(DEPDIR)/net_udp.Po -rm -f ./$(DEPDIR)/opt.Po -rm -f ./$(DEPDIR)/os.Po + -rm -f ./$(DEPDIR)/parse_uri.Po -rm -f ./$(DEPDIR)/qtype.Po -rm -f ./$(DEPDIR)/resperf.Po -rm -f ./$(DEPDIR)/strerror.Po diff --git a/src/config.h.in b/src/config.h.in index 6ddcd00..49e5f44 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -21,9 +21,15 @@ /* Define to 1 if you have the `m' library (-lm). */ #undef HAVE_LIBM +/* Define to 1 if you have the `nghttp2' library (-lnghttp2). */ +#undef HAVE_LIBNGHTTP2 + /* Define to 1 if you have the <memory.h> header file. */ #undef HAVE_MEMORY_H +/* Define to 1 if you have the <nghttp2.h> header file. */ +#undef HAVE_NGHTTP2_H + /* Define if you have POSIX threads libraries and header files. */ #undef HAVE_PTHREAD @@ -53,6 +53,7 @@ const char* perf_dns_rcode_strings[] = { perf_result_t perf_dname_fromstring(const char* str, size_t len, perf_buffer_t* target) { size_t label_len, at; + ssize_t max = 255; const char* orig_str = str; bool is_quoted; @@ -101,6 +102,10 @@ perf_result_t perf_dname_fromstring(const char* str, size_t len, perf_buffer_t* if (label_len > 63) { return PERF_R_FAILURE; } + max -= label_len + 1; + if (max < 0) { + return PERF_R_FAILURE; + } perf_buffer_putuint8(target, label_len); if (is_quoted) { for (at = 0; at < len; at++) { diff --git a/src/dnsperf.1.in b/src/dnsperf.1.in index e38aec1..55cbdf1 100644 --- a/src/dnsperf.1.in +++ b/src/dnsperf.1.in @@ -44,6 +44,7 @@ dnsperf \- test the performance of a DNS server [\fB\-W\fR] [\fB\-x\ \fIlocal_port\fR] [\fB\-y\ \fI[alg:]name:secret\fR] +[\fB\-O\ \fIoption=value\fR] .ad .hy .SH DESCRIPTION @@ -170,6 +171,11 @@ test over a local Ethernet connection, it should be zero. If one or more packets has been dropped, there may be a problem with the network connection. In that case, the results should be considered suspect and the test repeated. +.SS "Using DNS-over-HTTPS" +When using DNS-over-HTTPS you must set the \fB-O doh\-uri=...\fR to something +that works with the server you're sending to. +Also note that the value for maximum outstanding queries will be used to +control the maximum concurrent streams within the HTTP/2 connection. .SH OPTIONS \fB-a \fIlocal_addr\fR @@ -259,7 +265,7 @@ times; if a time limit is set, the file may be read fewer times. .br .RS Sets the port on which the DNS packets are sent. -If not specified, the standard DNS port (udp/tcp 53, DoT 853) is used. +If not specified, the standard DNS port (udp/tcp 53, DoT 853, DoH 443) is used. .RE \fB-q \fInum_queries\fR @@ -281,7 +287,7 @@ There is no default limit. \fB-m \fImode\fR .br .RS -Specifies the transport mode to use, "udp", "tcp" or "dot". +Specifies the transport mode to use, "udp", "tcp", "dot" or "doh". Default is "udp". .RE @@ -368,6 +374,29 @@ the secret is expressed as a base-64 encoded string. Available algorithms are: hmac-md5, hmac-sha1, hmac-sha224, hmac-sha256, hmac-sha384 and hmac-sha512. .RE + +\fB-O \fIoption=value\fR +.br +.RS +Set an extended long option for various things to control different aspects +of testing or protocol modules, see EXTENDED OPTIONS for list of available +options. +.RE +.SH "EXTENDED OPTIONS" + +\fBdoh-uri=\fIURI\fR +.br +.RS +The URI to use for DNS-over-HTTPS, default value is +"https://localhost/dns-query". +.RE + +\fBdoh-method=\fIHTTP_METHOD\fR +.br +.RS +The HTTP method to use when querying with DNS-over-HTTPS, default is GET. +Available methods are: GET, POST. +.RE .SH "SEE ALSO" \fBresperf\fR(1) .SH AUTHOR diff --git a/src/dnsperf.c b/src/dnsperf.c index c3466ee..0f312ce 100644 --- a/src/dnsperf.c +++ b/src/dnsperf.c @@ -52,7 +52,8 @@ #define DEFAULT_SERVER_NAME "127.0.0.1" #define DEFAULT_SERVER_PORT 53 #define DEFAULT_SERVER_DOT_PORT 853 -#define DEFAULT_SERVER_PORTS "udp/tcp 53 or DoT 853" +#define DEFAULT_SERVER_DOH_PORT 443 +#define DEFAULT_SERVER_PORTS "udp/tcp 53, DoT 853 or DoH 443" #define DEFAULT_LOCAL_PORT 0 #define DEFAULT_MAX_OUTSTANDING 100 #define DEFAULT_TIMEOUT 5 @@ -407,6 +408,8 @@ setup(int argc, char** argv, config_t* config) const char* edns_option = NULL; const char* tsigkey = NULL; const char* mode = 0; + const char* doh_uri = DEFAULT_DOH_URI; + const char* doh_method = DEFAULT_DOH_METHOD; memset(config, 0, sizeof(*config)); config->argc = argc; @@ -422,7 +425,7 @@ setup(int argc, char** argv, config_t* config) perf_opt_add('f', perf_opt_string, "family", "address family of DNS transport, inet or inet6", "any", &family); - perf_opt_add('m', perf_opt_string, "mode", "set transport mode: udp, tcp or dot", "udp", &mode); + perf_opt_add('m', perf_opt_string, "mode", "set transport mode: udp, tcp, dot or doh", "udp", &mode); perf_opt_add('s', perf_opt_string, "server_addr", "the server to query", DEFAULT_SERVER_NAME, &server_name); perf_opt_add('p', perf_opt_port, "port", @@ -480,6 +483,11 @@ setup(int argc, char** argv, config_t* config) perf_opt_add('v', perf_opt_boolean, NULL, "verbose: report each query and additional information to stdout", NULL, &config->verbose); + perf_long_opt_add("doh-uri", perf_opt_string, "doh_uri", + "the URI to use for DNS-over-HTTPS", DEFAULT_DOH_URI, &doh_uri); + perf_long_opt_add("doh-method", perf_opt_string, "doh_method", + "the HTTP method to use for DNS-over-HTTPS: GET or POST", DEFAULT_DOH_METHOD, &doh_method); + bool log_stdout = false; perf_opt_add('W', perf_opt_boolean, NULL, "log warnings and errors to stdout instead of stderr", NULL, &log_stdout); @@ -493,9 +501,27 @@ setup(int argc, char** argv, config_t* config) config->mode = perf_net_parsemode(mode); if (!server_port) { - server_port = config->mode == sock_dot ? DEFAULT_SERVER_DOT_PORT : DEFAULT_SERVER_PORT; + switch (config->mode) { + case sock_doh: + server_port = DEFAULT_SERVER_DOH_PORT; + break; + case sock_dot: + server_port = DEFAULT_SERVER_DOT_PORT; + break; + default: + server_port = DEFAULT_SERVER_PORT; + break; + } } + if (doh_uri) { + perf_net_doh_parse_uri(doh_uri); + } + if (doh_method) { + perf_net_doh_parse_method(doh_method); + } + perf_net_doh_set_max_concurrent_streams(config->max_outstanding); + if (family != NULL) config->family = perf_net_parsefamily(family); perf_net_parseserver(config->family, server_name, server_port, @@ -1181,13 +1207,11 @@ threadinfo_init(threadinfo_t* tinfo, const config_t* config, tinfo->socks[i] = perf_net_opensocket(config->mode, &config->server_addr, &config->local_addr, socket_offset++, - config->bufsize); + config->bufsize, + tinfo, perf__net_sent, perf__net_event); if (!tinfo->socks[i]) { perf_log_fatal("perf_net_opensocket(): no socket returned, out of memory?"); } - tinfo->socks[i]->data = tinfo; - tinfo->socks[i]->sent = perf__net_sent; - tinfo->socks[i]->event = perf__net_event; } tinfo->current_sock = 0; @@ -1204,14 +1228,16 @@ threadinfo_stop(threadinfo_t* tinfo) } static void -threadinfo_cleanup(threadinfo_t* tinfo, times_t* times) +threadinfo_cleanup(config_t* config, threadinfo_t* tinfo, times_t* times) { unsigned int i; if (interrupted) cancel_queries(tinfo); - for (i = 0; i < tinfo->nsocks; i++) + for (i = 0; i < tinfo->nsocks; i++) { + perf_net_stats_compile(config->mode, tinfo->socks[i]); perf_net_close(tinfo->socks[i]); + } if (tinfo->last_recv > times->end_time) times->end_time = tinfo->last_recv; } @@ -1246,6 +1272,7 @@ int main(int argc, char** argv) switch (config.mode) { case sock_tcp: case sock_dot: + case sock_doh: // block SIGPIPE for TCP/DOT mode, if connection is closed it will generate a signal perf_os_blocksignal(SIGPIPE, true); break; @@ -1296,13 +1323,16 @@ int main(int argc, char** argv) if (config.stats_interval > 0) PERF_JOIN(stats_thread.sender, NULL); + perf_net_stats_init(config.mode); + for (i = 0; i < config.threads; i++) - threadinfo_cleanup(&threads[i], ×); + threadinfo_cleanup(&config, &threads[i], ×); print_final_status(&config); sum_stats(&config, &total_stats); print_statistics(&config, ×, &total_stats); + perf_net_stats_print(config.mode); cleanup(&config); #if OPENSSL_VERSION_NUMBER < 0x10100000L @@ -39,6 +39,8 @@ enum perf_net_mode perf_net_parsemode(const char* mode) return sock_tcp; } else if (!strcmp(mode, "tls") || !strcmp(mode, "dot")) { return sock_dot; + } else if (!strcmp(mode, "doh")) { + return sock_doh; } perf_log_warning("invalid socket mode"); @@ -189,7 +191,7 @@ void perf_net_parselocal(int family, const char* name, unsigned int port, exit(1); } -struct perf_net_socket* perf_net_opensocket(enum perf_net_mode mode, const perf_sockaddr_t* server, const perf_sockaddr_t* local, unsigned int offset, size_t bufsize) +struct perf_net_socket* perf_net_opensocket(enum perf_net_mode mode, const perf_sockaddr_t* server, const perf_sockaddr_t* local, unsigned int offset, size_t bufsize, void* data, perf_net_sent_cb_t sent, perf_net_event_cb_t event) { int port; perf_sockaddr_t tmp; @@ -209,14 +211,46 @@ struct perf_net_socket* perf_net_opensocket(enum perf_net_mode mode, const perf_ switch (mode) { case sock_udp: - return perf_net_udp_opensocket(server, &tmp, bufsize); + return perf_net_udp_opensocket(server, &tmp, bufsize, data, sent, event); case sock_tcp: - return perf_net_tcp_opensocket(server, &tmp, bufsize); + return perf_net_tcp_opensocket(server, &tmp, bufsize, data, sent, event); case sock_dot: - return perf_net_dot_opensocket(server, &tmp, bufsize); + return perf_net_dot_opensocket(server, &tmp, bufsize, data, sent, event); + case sock_doh: + return perf_net_doh_opensocket(server, &tmp, bufsize, data, sent, event); default: perf_log_fatal("perf_net_opensocket(): invalid mode"); } return 0; } + +void perf_net_stats_init(enum perf_net_mode mode) +{ + switch (mode) { + case sock_doh: + perf_net_doh_stats_init(); + default: + break; + } +} + +void perf_net_stats_compile(enum perf_net_mode mode, struct perf_net_socket* sock) +{ + switch (mode) { + case sock_doh: + perf_net_doh_stats_compile(sock); + default: + break; + } +} + +void perf_net_stats_print(enum perf_net_mode mode) +{ + switch (mode) { + case sock_doh: + perf_net_doh_stats_print(); + default: + break; + } +} @@ -47,7 +47,8 @@ enum perf_net_mode { sock_pipe, sock_udp, sock_tcp, - sock_dot + sock_dot, + sock_doh }; struct perf_net_socket; @@ -158,10 +159,25 @@ static inline int perf_sockaddr_isinet6(const perf_sockaddr_t* sockaddr) return sockaddr->sa.sa.sa_family == AF_INET6; } -struct perf_net_socket* perf_net_opensocket(enum perf_net_mode mode, const perf_sockaddr_t* server, const perf_sockaddr_t* local, unsigned int offset, size_t bufsize); +struct perf_net_socket* perf_net_opensocket(enum perf_net_mode mode, const perf_sockaddr_t* server, const perf_sockaddr_t* local, unsigned int offset, size_t bufsize, void* data, perf_net_sent_cb_t sent, perf_net_event_cb_t event); -struct perf_net_socket* perf_net_udp_opensocket(const perf_sockaddr_t*, const perf_sockaddr_t*, size_t); -struct perf_net_socket* perf_net_tcp_opensocket(const perf_sockaddr_t*, const perf_sockaddr_t*, size_t); -struct perf_net_socket* perf_net_dot_opensocket(const perf_sockaddr_t*, const perf_sockaddr_t*, size_t); +struct perf_net_socket* perf_net_udp_opensocket(const perf_sockaddr_t*, const perf_sockaddr_t*, size_t, void* data, perf_net_sent_cb_t sent, perf_net_event_cb_t event); +struct perf_net_socket* perf_net_tcp_opensocket(const perf_sockaddr_t*, const perf_sockaddr_t*, size_t, void* data, perf_net_sent_cb_t sent, perf_net_event_cb_t event); +struct perf_net_socket* perf_net_dot_opensocket(const perf_sockaddr_t*, const perf_sockaddr_t*, size_t, void* data, perf_net_sent_cb_t sent, perf_net_event_cb_t event); +struct perf_net_socket* perf_net_doh_opensocket(const perf_sockaddr_t*, const perf_sockaddr_t*, size_t, void* data, perf_net_sent_cb_t sent, perf_net_event_cb_t event); + +#define DEFAULT_DOH_URI "https://localhost/dns-query" +#define DEFAULT_DOH_METHOD "GET" + +void perf_net_doh_parse_uri(const char*); +void perf_net_doh_parse_method(const char*); +void perf_net_doh_set_max_concurrent_streams(size_t); + +void perf_net_stats_init(enum perf_net_mode); +void perf_net_stats_compile(enum perf_net_mode, struct perf_net_socket*); +void perf_net_stats_print(enum perf_net_mode); +void perf_net_doh_stats_init(); +void perf_net_doh_stats_compile(struct perf_net_socket*); +void perf_net_doh_stats_print(); #endif diff --git a/src/net_doh.c b/src/net_doh.c new file mode 100644 index 0000000..0689fc6 --- /dev/null +++ b/src/net_doh.c @@ -0,0 +1,1017 @@ +/* + * Copyright 2019-2021 OARC, Inc. + * Copyright 2017-2018 Akamai Technologies + * Copyright 2006-2016 Nominum, Inc. + * All rights reserved. + * + * 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. + */ +/* + * Based on HTTP/2 module in DNS shotgun by Tomáš Křížek (CZ.NIC) + * https://gitlab.nic.cz/knot/shotgun/-/blob/master/replay/dnssim/src/output/dnssim/https2.c + * + * Initial PoC implementation by Atanas Argirov (PeeriX) + * https://github.com/m0rcq + */ + +#include "config.h" + +#include "net.h" +#include "edns.h" +#include "parse_uri.h" +#include "log.h" +#include "strerror.h" +#include "util.h" +#include "os.h" +#include "opt.h" + +#include <errno.h> +#include <assert.h> +#include <fcntl.h> +#include <unistd.h> +#include <openssl/err.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <ck_pr.h> +#include <nghttp2/nghttp2.h> +#include <openssl/bio.h> +#include <openssl/evp.h> + +#define DNS_GET_REQUEST_VAR "?dns=" +#define DNS_MSG_MAX_SIZE 65535 + +static SSL_CTX* ssl_ctx = 0; +static struct URI doh_uri; +enum perf_doh_method { + doh_method_get, + doh_method_post +}; +static enum perf_doh_method doh_method = doh_method_get; +static size_t doh_max_concurr = 100; + +#define self ((struct perf__doh_socket*)sock) + +#define MAKE_NV(NAME, VALUE) \ + { \ + (uint8_t*)NAME, (uint8_t*)VALUE, sizeof(NAME) - 1, sizeof(VALUE) - 1, \ + NGHTTP2_NV_FLAG_NONE \ + } + +#define MAKE_NV_LEN(NAME, VALUE, VALUELEN) \ + { \ + (uint8_t*)NAME, (uint8_t*)VALUE, sizeof(NAME) - 1, VALUELEN, \ + NGHTTP2_NV_FLAG_NONE \ + } + +typedef struct { + uint8_t *buf, *bufp; + size_t len, buf_len; +} http2_data_provider_t; + +typedef struct { + nghttp2_session* session; + http2_data_provider_t payload; + bool settings_sent; + char dnsmsg[DNS_MSG_MAX_SIZE]; + size_t dnsmsg_at; + bool dnsmsg_completed; +} http2_session_t; + +struct _doh_stats { + size_t code[500]; + size_t unknown_code; +}; + +struct perf__doh_socket { + struct perf_net_socket base; + + pthread_mutex_t lock; + SSL* ssl; + + bool is_ready, is_conn_ready, is_ssl_ready, is_sending, is_post_sending; + // bool have_more; TODO + bool do_reconnect; + + perf_sockaddr_t server, local; + size_t bufsize; + + uint16_t qid; + + uint64_t conn_ts; + perf_socket_event_t conn_event, conning_event; + + http2_session_t http2; // http2 session data + int http2_code; + bool http2_is_dns; + + struct _doh_stats stats; +}; + +static pthread_mutex_t _nghttp2_lock = PTHREAD_MUTEX_INITIALIZER; +static nghttp2_session_callbacks* _nghttp2_callbacks = 0; +static nghttp2_option* _nghttp2_option = 0; + +void perf_net_doh_parse_uri(const char* uri) +{ + if (parse_uri(&doh_uri, uri)) { + perf_log_warning("invalid DNS-over-HTTPS URI"); + perf_opt_usage(); + exit(1); + } +} + +void perf_net_doh_parse_method(const char* method) +{ + if (!strcmp(method, "GET")) { + doh_method = doh_method_get; + return; + } else if (!strcmp(method, "POST")) { + doh_method = doh_method_post; + return; + } + + perf_log_warning("invalid DNS-over-HTTPS method"); + perf_opt_usage(); + exit(1); +} + +void perf_net_doh_set_max_concurrent_streams(size_t max_concurr) +{ + doh_max_concurr = max_concurr; +} + +static void perf__doh_connect(struct perf_net_socket* sock) +{ + int ret; + + nghttp2_session_del(self->http2.session); + ret = nghttp2_session_client_new2(&self->http2.session, _nghttp2_callbacks, sock, _nghttp2_option); + if (ret < 0) { + perf_log_fatal("Failed to initialize http2 session: %s", nghttp2_strerror(ret)); + } + + int fd = socket(self->server.sa.sa.sa_family, SOCK_STREAM, 0); + if (fd == -1) { + char __s[256]; + perf_log_fatal("socket: %s", perf_strerror_r(errno, __s, sizeof(__s))); + } + ck_pr_store_int(&sock->fd, fd); + + if (self->ssl) { + SSL_free(self->ssl); + } + if (!(self->ssl = SSL_new(ssl_ctx))) { + perf_log_fatal("SSL_new(): %s", ERR_error_string(ERR_get_error(), 0)); + } + if (!(ret = SSL_set_fd(self->ssl, sock->fd))) { + perf_log_fatal("SSL_set_fd(): %s", ERR_error_string(SSL_get_error(self->ssl, ret), 0)); + } + + if (self->server.sa.sa.sa_family == AF_INET6) { + int on = 1; + + if (setsockopt(sock->fd, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)) == -1) { + perf_log_warning("setsockopt(IPV6_V6ONLY) failed"); + } + } + + if (bind(sock->fd, &self->local.sa.sa, self->local.length) == -1) { + char __s[256]; + perf_log_fatal("bind: %s", perf_strerror_r(errno, __s, sizeof(__s))); + } + + if (self->bufsize > 0) { + ret = setsockopt(sock->fd, SOL_SOCKET, SO_RCVBUF, + &self->bufsize, sizeof(self->bufsize)); + if (ret < 0) + perf_log_warning("setsockbuf(SO_RCVBUF) failed"); + + ret = setsockopt(sock->fd, SOL_SOCKET, SO_SNDBUF, + &self->bufsize, sizeof(self->bufsize)); + if (ret < 0) + perf_log_warning("setsockbuf(SO_SNDBUF) failed"); + } + + int flags = fcntl(sock->fd, F_GETFL, 0); + if (flags < 0) + perf_log_fatal("fcntl(F_GETFL)"); + ret = fcntl(sock->fd, F_SETFL, flags | O_NONBLOCK); + if (ret < 0) + perf_log_fatal("fcntl(F_SETFL)"); + + self->conn_ts = perf_get_time(); + if (sock->event) { + sock->event(sock, self->conning_event, self->conn_ts); + self->conning_event = perf_socket_event_reconnecting; + } + if (connect(sock->fd, &self->server.sa.sa, self->server.length)) { + if (errno == EINPROGRESS) { + return; + } else { + char __s[256]; + perf_log_fatal("connect() failed: %s", perf_strerror_r(errno, __s, sizeof(__s))); + } + } + + self->is_conn_ready = true; +} + +static void perf__doh_reconnect(struct perf_net_socket* sock) +{ + close(sock->fd); + // self->have_more = false; TODO + + self->http2.settings_sent = false; + self->is_ready = false; + self->is_conn_ready = false; + self->is_ssl_ready = false; + self->is_sending = false; + self->is_post_sending = false; + + self->http2.dnsmsg_at = 0; + self->http2.dnsmsg_completed = false; + self->http2_code = 0; + self->http2_is_dns = false; + + perf__doh_connect(sock); +} + +// static ssize_t _recv_callback(nghttp2_session *session, uint8_t *buf, size_t length, int flags, void *sock) +// { +// if (self->http2.dnsmsg_completed) { +// return NGHTTP2_ERR_WOULDBLOCK; +// } +// +// ssize_t n = SSL_read(self->ssl, buf, length); +// if (!n) { +// perf__doh_reconnect(sock); +// return NGHTTP2_ERR_WOULDBLOCK; +// } +// if (n < 0) { +// int err = SSL_get_error(self->ssl, n); +// switch (err) { +// case SSL_ERROR_WANT_READ: +// return NGHTTP2_ERR_WOULDBLOCK; +// case SSL_ERROR_SYSCALL: +// switch (errno) { +// case ECONNREFUSED: +// case ECONNRESET: +// case ENOTCONN: +// perf__doh_reconnect(sock); +// return NGHTTP2_ERR_WOULDBLOCK; +// default: +// break; +// } +// break; +// default: +// break; +// } +// return NGHTTP2_ERR_CALLBACK_FAILURE; +// } +// return n; +// } + +static ssize_t perf__doh_recv(struct perf_net_socket* sock, void* buf, size_t len, int flags) +{ + // read TLS data here instead of nghttp2_recv_callback + PERF_LOCK(&self->lock); + if (!self->is_ready) { + PERF_UNLOCK(&self->lock); + errno = EAGAIN; + return -1; + } + + uint8_t recvbuf[TCP_RECV_BUF_SIZE]; + ssize_t n = SSL_read(self->ssl, recvbuf, TCP_RECV_BUF_SIZE); + if (!n) { + perf__doh_reconnect(sock); + PERF_UNLOCK(&self->lock); + errno = EAGAIN; + return -1; + } + if (n < 0) { + int err = SSL_get_error(self->ssl, n); + switch (err) { + case SSL_ERROR_WANT_READ: + errno = EAGAIN; + break; + case SSL_ERROR_SYSCALL: + switch (errno) { + case ECONNREFUSED: + case ECONNRESET: + case ENOTCONN: + perf__doh_reconnect(sock); + errno = EAGAIN; + break; + default: + break; + } + break; + default: + errno = EBADF; + break; + } + PERF_UNLOCK(&self->lock); + return -1; + } + + // this will be processed by nghttp2 callbacks + int ret = nghttp2_session_mem_recv(self->http2.session, recvbuf, n); + if (ret < 0) { + perf_log_warning("nghttp2_session_mem_recv failed: %s", nghttp2_strerror(ret)); + PERF_UNLOCK(&self->lock); + return -1; + } + // TODO: handle partial mem_recv + if (ret != n) { + perf_log_fatal("perf__doh_recv() mem_recv did not take all"); + } + + // TODO: is this faster then mem_recv? + // int ret = nghttp2_session_recv(self->http2.session); + // if (ret < 0) { + // perf_log_warning("nghttp2_session_recv failed: %s", nghttp2_strerror(ret)); + // PERF_UNLOCK(&self->lock); + // return -1; + // } + + if (self->http2.dnsmsg_completed) { + if (self->http2_code > 99 && self->http2_code < 600) { + self->stats.code[self->http2_code - 100]++; + } else { + self->stats.unknown_code++; + } + if (!self->http2_is_dns) { + // TODO: store non-dns for stats + self->http2.dnsmsg_completed = false; + self->http2.dnsmsg_at = 0; + self->http2_code = 0; + self->http2_is_dns = false; + PERF_UNLOCK(&self->lock); + errno = EAGAIN; + return -1; + } + if (self->http2_code < 200 || self->http2_code > 299) { + // TODO: store return code for stats + self->http2.dnsmsg_completed = false; + self->http2.dnsmsg_at = 0; + self->http2_code = 0; + PERF_UNLOCK(&self->lock); + errno = EAGAIN; + return -1; + } + if (self->http2.dnsmsg_at < len) { + len = self->http2.dnsmsg_at; + } + memcpy(buf, self->http2.dnsmsg, len); + self->http2.dnsmsg_completed = false; + self->http2.dnsmsg_at = 0; + + // self->have_more = false; TODO + PERF_UNLOCK(&self->lock); + return len; + } + + // self->have_more = true; TODO + PERF_UNLOCK(&self->lock); + errno = EAGAIN; + return -1; +} + +static void _submit_dns_query_get(struct perf_net_socket* sock, const void* buf, size_t len) +{ + const size_t path_len = doh_uri.pathlen + + sizeof(DNS_GET_REQUEST_VAR) - 1 + + (4 * ((len + 2) / 3)) + 1; + char full_path[path_len]; + char* p = &full_path[0]; + + memcpy(p, doh_uri.path, doh_uri.pathlen); + p += doh_uri.pathlen; + + memcpy(p, DNS_GET_REQUEST_VAR, sizeof(DNS_GET_REQUEST_VAR) - 1); + p += sizeof(DNS_GET_REQUEST_VAR) - 1; + + EVP_EncodeBlock((unsigned char*)p, buf, len); + // RFC8484 requires base64url (RFC4648) + // and Padding characters (=) for base64url MUST NOT be included. + // base64url alphabet is the same as base64 except + is - and / is _ + while (*p) { + switch (*p) { + case '+': + *p++ = '-'; + break; + case '/': + *p++ = '_'; + break; + case '=': + *p = 0; + break; + default: + p++; + } + } + + const nghttp2_nv hdrs[] = { + MAKE_NV(":method", "GET"), + MAKE_NV(":scheme", "https"), + MAKE_NV_LEN(":authority", doh_uri.hostport, doh_uri.hostportlen), + MAKE_NV_LEN(":path", full_path, p - full_path), + MAKE_NV("accept", "application/dns-message"), + MAKE_NV("user-agent", "dnsperf/" PACKAGE_VERSION " (nghttp2/" NGHTTP2_VERSION ")") + }; + + int32_t stream_id = nghttp2_submit_request(self->http2.session, + NULL, + hdrs, + sizeof(hdrs) / sizeof(hdrs[0]), + NULL, + sock); + if (stream_id < 0) { + perf_log_fatal("Failed to submit HTTP2 request: %s", nghttp2_strerror(stream_id)); + } +} + +static ssize_t _payload_read_cb(nghttp2_session* session, + int32_t stream_id, uint8_t* buf, + size_t length, uint32_t* data_flags, + nghttp2_data_source* source, + void* sock) +{ + http2_data_provider_t* payload = source->ptr; + + ssize_t payload_size = length < payload->len ? length : payload->len; + + memcpy(buf, payload->bufp, payload_size); + payload->bufp += payload_size; + payload->len -= payload_size; + // check for EOF + if (payload->len == 0) { + *data_flags |= NGHTTP2_DATA_FLAG_EOF; + self->is_post_sending = false; + } + + return payload_size; +} + +static void _submit_dns_query_post(struct perf_net_socket* sock, const void* buf, size_t len) +{ + // POST requires DATA flow-controlled payload that local endpoint + // can send across without issuing WINDOW_UPDATE + // we need to check for this and bounce back the request if the + // payload > remote window size + // TODO: are below needed? can they be checked on connect? + // int remote_window_size = nghttp2_session_get_remote_window_size(self->http2.session); + // if (remote_window_size < 0) { + // perf_log_fatal("failed to get http2 session remote window size"); + // } + // if (len > remote_window_size) { + // perf_log_fatal("remote window size is too small for POST payload"); + // } + + // compose content-length + char payload_size[20]; + int payload_size_len = snprintf(payload_size, sizeof(payload_size), "%zu", len); + // TODO: check snprintf() + + const nghttp2_nv hdrs[] = { + MAKE_NV(":method", "POST"), + MAKE_NV(":scheme", "https"), + MAKE_NV_LEN(":authority", doh_uri.hostport, doh_uri.hostportlen), + MAKE_NV_LEN(":path", doh_uri.path, doh_uri.pathlen), + MAKE_NV("accept", "application/dns-message"), + MAKE_NV("content-type", "application/dns-message"), + MAKE_NV_LEN("content-length", payload_size, payload_size_len), + MAKE_NV("user-agent", "dnsperf/" PACKAGE_VERSION " (nghttp2/" NGHTTP2_VERSION ")") + }; + + if (len > self->http2.payload.buf_len) { + self->http2.payload.buf_len = ((len / MAX_EDNS_PACKET) + 1) * MAX_EDNS_PACKET; + if (!(self->http2.payload.buf = realloc(self->http2.payload.buf, self->http2.payload.buf_len))) { + perf_log_fatal("perf_net_doh: out of memory"); + } + } + if (self && self->http2.payload.buf && buf) { // fix clang scan-build + memcpy(self->http2.payload.buf, buf, len); + } else { + perf_log_fatal("_submit_dns_query_post(): payload.buf is null"); + } + self->http2.payload.bufp = self->http2.payload.buf; + self->http2.payload.len = len; + self->is_post_sending = true; + + // we need data provider to pass to submit() + + nghttp2_data_provider data_provider = { + .source.ptr = &self->http2.payload, + .read_callback = _payload_read_cb + }; + int32_t stream_id = nghttp2_submit_request(self->http2.session, + NULL, + hdrs, + sizeof(hdrs) / sizeof(hdrs[0]), + &data_provider, + sock); + if (stream_id < 0) { + perf_log_fatal("Failed to submit HTTP2 request: %s", nghttp2_strerror(stream_id)); + } +} + +static ssize_t perf__doh_sendto(struct perf_net_socket* sock, uint16_t qid, const void* buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen) +{ + PERF_LOCK(&self->lock); + + if (!self->is_ready) { + // TODO: query will be lost here + PERF_UNLOCK(&self->lock); + errno = EINPROGRESS; + return -1; + } + + if (self->is_sending) { + perf_log_fatal("called when sending"); + } + + self->qid = qid; + + switch (doh_method) { + case doh_method_get: + _submit_dns_query_get(sock, buf, len); + break; + case doh_method_post: + _submit_dns_query_post(sock, buf, len); + break; + } + + int ret = nghttp2_session_send(self->http2.session); + if (ret < 0) { + // TODO: handle error better, reconnect when needed + perf_log_warning("nghttp2_session_send failed: %s", nghttp2_strerror(ret)); + self->do_reconnect = true; + PERF_UNLOCK(&self->lock); + errno = EINPROGRESS; + return -1; + } + + if (self->is_post_sending || nghttp2_session_get_outbound_queue_size(self->http2.session) > 0 || nghttp2_session_want_write(self->http2.session)) { + self->is_sending = true; + PERF_UNLOCK(&self->lock); + errno = EINPROGRESS; + return -1; + } + PERF_UNLOCK(&self->lock); + + return len; +} + +static int perf__doh_close(struct perf_net_socket* sock) +{ + // TODO + return close(sock->fd); +} + +static int perf__doh_sockeq(struct perf_net_socket* sock_a, struct perf_net_socket* sock_b) +{ + return sock_a->fd == sock_b->fd; +} + +static int perf__doh_sockready(struct perf_net_socket* sock, int pipe_fd, int64_t timeout) +{ + PERF_LOCK(&self->lock); + + if (self->do_reconnect) { + perf__doh_reconnect(sock); + self->do_reconnect = false; + } + + if (self->is_ready) { + // do nghttp2 I/O send to flush outstanding frames + int ret = nghttp2_session_send(self->http2.session); + if (ret != 0) { + // TODO: handle error better, reconnect when needed + perf_log_warning("nghttp2_session_send failed: %s", nghttp2_strerror(ret)); + self->do_reconnect = true; + PERF_UNLOCK(&self->lock); + return 0; + } + + bool sent = false; + if (self->is_sending) { + if (self->is_post_sending || nghttp2_session_get_outbound_queue_size(self->http2.session) > 0 || nghttp2_session_want_write(self->http2.session)) { + PERF_UNLOCK(&self->lock); + return 0; + } + self->is_sending = false; + sent = true; + } + PERF_UNLOCK(&self->lock); + if (sent && sock->sent) { + sock->sent(sock, self->qid); + } + return 1; + } + + if (!self->is_conn_ready) { + switch (perf_os_waituntilanywritable(&sock, 1, pipe_fd, timeout)) { + case PERF_R_TIMEDOUT: + PERF_UNLOCK(&self->lock); + return -1; + case PERF_R_SUCCESS: { + int error = 0; + socklen_t len = (socklen_t)sizeof(error); + + getsockopt(sock->fd, SOL_SOCKET, SO_ERROR, (void*)&error, &len); + if (error != 0) { + if (error == EINPROGRESS +#if EWOULDBLOCK != EAGAIN + || error == EWOULDBLOCK +#endif + || error == EAGAIN) { + PERF_UNLOCK(&self->lock); + return 0; + } + // unrecoverable error, reconnect + self->do_reconnect = true; + PERF_UNLOCK(&self->lock); + return 0; + } + break; + } + default: + PERF_UNLOCK(&self->lock); + return -1; + } + self->is_conn_ready = true; + } + + if (!self->is_ssl_ready) { + int ret = SSL_connect(self->ssl); + if (!ret) { + // unrecoverable error, reconnect + self->do_reconnect = true; + PERF_UNLOCK(&self->lock); + return 0; + } + if (ret < 0) { + switch (SSL_get_error(self->ssl, ret)) { + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + break; + default: + // unrecoverable error, reconnect + self->do_reconnect = true; + } + PERF_UNLOCK(&self->lock); + return 0; + } + + const uint8_t* alpn = 0; + uint32_t alpn_len = 0; +#ifndef OPENSSL_NO_NEXTPROTONEG + SSL_get0_next_proto_negotiated(self->ssl, &alpn, &alpn_len); +#endif /* !OPENSSL_NO_NEXTPROTONEG */ +#if OPENSSL_VERSION_NUMBER >= 0x10002000L + if (!alpn) { + SSL_get0_alpn_selected(self->ssl, &alpn, &alpn_len); + } +#endif /* OPENSSL_VERSION_NUMBER >= 0x10002000L */ +#if defined(OPENSSL_NO_NEXTPROTONEG) && OPENSSL_VERSION_NUMBER < 0x10002000L +#error "OpenSSL has no support for getting alpn" +#endif + if (!alpn || alpn_len != 2 || memcmp("h2", alpn, 2) != 0) { + perf_log_warning("Unable to get ALPN or not \"h2\", reconnecting"); + self->do_reconnect = true; + PERF_UNLOCK(&self->lock); + return 0; + } + self->is_ssl_ready = true; + } + + if (!self->http2.settings_sent) { + // send settings + // TODO: does sending settings need session_send()? + nghttp2_settings_entry iv[] = { + { NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, doh_max_concurr }, + { NGHTTP2_SETTINGS_ENABLE_PUSH, 0 }, + { NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE, 65535 } + }; + int ret = nghttp2_submit_settings(self->http2.session, NGHTTP2_FLAG_NONE, iv, + sizeof(iv) / sizeof(*iv)); + if (ret != 0) { + perf_log_warning("Could not submit https2 SETTINGS: %s", nghttp2_strerror(ret)); + self->do_reconnect = true; + PERF_UNLOCK(&self->lock); + return 0; + } + self->http2.settings_sent = true; + } + + self->is_ready = true; + PERF_UNLOCK(&self->lock); + + if (sock->event) { + sock->event(sock, self->conn_event, perf_get_time() - self->conn_ts); + self->conn_event = perf_socket_event_reconnected; + } + + return 1; +} + +static bool perf__doh_have_more(struct perf_net_socket* sock) +{ + // return self->have_more; TODO + return false; +} + +/* nghttp2 callbacks */ + +static ssize_t _http2_send_cb(nghttp2_session* session, + const uint8_t* data, + size_t length, + int flags, + void* sock) +{ + // TODO: remove once non-experimental + if (!PERF_TRYLOCK(&self->lock)) { + perf_log_fatal("_http2_send_cb called without lock"); + } + + if (!self->is_ready) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + + ssize_t n = SSL_write(self->ssl, data, length); + if (n < 1) { + switch (SSL_get_error(self->ssl, n)) { + case SSL_ERROR_SYSCALL: + switch (errno) { + case ECONNREFUSED: + case ECONNRESET: + case ENOTCONN: + case EPIPE: + perf__doh_reconnect(sock); + return NGHTTP2_ERR_CALLBACK_FAILURE; + default: + break; + } + break; + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + return NGHTTP2_ERR_WOULDBLOCK; + default: + break; + } + perf_log_warning("SSL_write(): %s", ERR_error_string(SSL_get_error(self->ssl, n), 0)); + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + + return n; +} + +static int _http2_frame_recv_cb(nghttp2_session* session, const nghttp2_frame* frame, void* sock) +{ + // TODO: remove once non-experimental + if (!PERF_TRYLOCK(&self->lock)) { + perf_log_fatal("_http2_frame_recv_cb called without lock"); + } + + switch (frame->hd.type) { + case NGHTTP2_DATA: + // we are interested in DATA frame which will carry the DNS response + // NGHTTP2_FLAG_END_STREAM indicates that we have the data in full + if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { + // TODO: what's the point of below code? if dnsmsg_at > max size then it will already done a buffer overflow + // if (self->http2.dnsmsg_at > DNS_MSG_MAX_SIZE) { + // perf_log_warning("DNS response > DNS message maximum size"); + // return NGHTTP2_ERR_CALLBACK_FAILURE; + // } + + // TODO: need to be able to receive multiple responses at the same time + if (self->http2.dnsmsg_completed) { + perf_log_fatal("_http2_frame_recv_cb: frame received when already having a dns msg"); + } + + self->http2.dnsmsg_completed = true; + // self->have_more = false; TODO + } + break; + case NGHTTP2_RST_STREAM: + case NGHTTP2_GOAWAY: + perf__doh_reconnect(sock); + break; + default: + break; + } + + return 0; +} + +static int _http2_on_header_callback(nghttp2_session* session, const nghttp2_frame* frame, const uint8_t* name, size_t namelen, const uint8_t* value, size_t valuelen, uint8_t flags, void* sock) +{ + switch (frame->hd.type) { + case NGHTTP2_HEADERS: { + if (!value) { + return 0; + } + if (!strncasecmp(":status:", (const char*)name, namelen)) { + self->http2_code = atoi((const char*)value); + } else if (!strncasecmp("content-type:", (const char*)name, namelen)) { + if (!strncasecmp("application/dns-message", (const char*)value, valuelen)) { + self->http2_is_dns = true; + } + } + break; + } + default: + break; + } + return 0; +} + +static int _http2_data_chunk_recv_cb(nghttp2_session* session, + uint8_t flags, + int32_t stream_id, + const uint8_t* data, + size_t len, void* sock) +{ + // TODO: remove once non-experimental + if (!PERF_TRYLOCK(&self->lock)) { + perf_log_fatal("_http2_data_chunk_recv_cb called without lock"); + } + + if (self->http2.dnsmsg_completed) { + perf_log_fatal("_http2_data_chunk_recv_cb: chunk received when already having a dns msg"); + } + + // TODO: point of nghttp2_session_get_stream_user_data() code? + // if (nghttp2_session_get_stream_user_data(session, stream_id)) { + if (self->http2.dnsmsg_at + len > DNS_MSG_MAX_SIZE) { + perf_log_warning("http2 chunk data exceeds DNS message max size"); + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + memcpy(self->http2.dnsmsg + self->http2.dnsmsg_at, data, len); + self->http2.dnsmsg_at += len; + // } + + return 0; +} + +#ifndef OPENSSL_NO_NEXTPROTONEG +/* NPN TLS extension check */ +static int select_next_proto_cb(SSL* ssl, unsigned char** out, + unsigned char* outlen, const unsigned char* in, + unsigned int inlen, void* arg) +{ + if (nghttp2_select_next_protocol(out, outlen, in, inlen) <= 0) { + perf_log_warning("Server did not advertise %u", NGHTTP2_PROTO_VERSION_ID); + return SSL_TLSEXT_ERR_ALERT_WARNING; + } + + return SSL_TLSEXT_ERR_OK; +} +#endif /* !OPENSSL_NO_NEXTPROTONEG */ + +struct perf_net_socket* perf_net_doh_opensocket(const perf_sockaddr_t* server, const perf_sockaddr_t* local, size_t bufsize, void* data, perf_net_sent_cb_t sent, perf_net_event_cb_t event) +{ + struct perf__doh_socket* tmp = calloc(1, sizeof(struct perf__doh_socket)); // clang scan-build + struct perf_net_socket* sock = (struct perf_net_socket*)tmp; + + if (!sock) { + perf_log_fatal("perf_net_doh_opensocket(): out of memory"); + return 0; // needed for clang scan build + } + + sock->recv = perf__doh_recv; + sock->sendto = perf__doh_sendto; + sock->close = perf__doh_close; + sock->sockeq = perf__doh_sockeq; + sock->sockready = perf__doh_sockready; + sock->have_more = perf__doh_have_more; + + sock->data = data; + sock->sent = sent; + sock->event = event; + + self->server = *server; + self->local = *local; + self->bufsize = bufsize; + if (self->bufsize > 0) { + self->bufsize *= 1024; + } + self->conning_event = perf_socket_event_connecting; + self->conn_event = perf_socket_event_connected; + PERF_MUTEX_INIT(&self->lock); + + if (!(self->http2.payload.buf = malloc(MAX_EDNS_PACKET))) { + perf_log_fatal("perf_net_doh_opensocket(): out of memory"); + } + self->http2.payload.buf_len = MAX_EDNS_PACKET; + + if (!ssl_ctx) { +#ifdef HAVE_TLS_METHOD + if (!(ssl_ctx = SSL_CTX_new(TLS_method()))) { + perf_log_fatal("SSL_CTX_new(): %s", ERR_error_string(ERR_get_error(), 0)); + } + if (!SSL_CTX_set_min_proto_version(ssl_ctx, TLS1_2_VERSION)) { + perf_log_fatal("SSL_CTX_set_min_proto_version(TLS1_2_VERSION): %s", ERR_error_string(ERR_get_error(), 0)); + } +#else + if (!(ssl_ctx = SSL_CTX_new(SSLv23_client_method()))) { + perf_log_fatal("SSL_CTX_new(): %s", ERR_error_string(ERR_get_error(), 0)); + } +#endif + SSL_CTX_set_mode(ssl_ctx, SSL_MODE_ENABLE_PARTIAL_WRITE); +#ifndef OPENSSL_NO_NEXTPROTONEG + SSL_CTX_set_next_proto_select_cb(ssl_ctx, select_next_proto_cb, NULL); +#endif /* !OPENSSL_NO_NEXTPROTONEG */ +#if OPENSSL_VERSION_NUMBER >= 0x10002000L + SSL_CTX_set_alpn_protos(ssl_ctx, (const unsigned char*)"\x02h2", 3); +#endif // OPENSSL_VERSION_NUMBER >= 0x10002000L + } + + /* setup HTTP/2 callbacks */ + if (!_nghttp2_callbacks || !_nghttp2_option) { + PERF_LOCK(&_nghttp2_lock); + if (!_nghttp2_callbacks) { + if (nghttp2_session_callbacks_new(&_nghttp2_callbacks)) { + perf_log_fatal("Unable to create nghttp2 callbacks: out of memory"); + } + nghttp2_session_callbacks_set_send_callback(_nghttp2_callbacks, _http2_send_cb); + nghttp2_session_callbacks_set_on_data_chunk_recv_callback(_nghttp2_callbacks, _http2_data_chunk_recv_cb); + nghttp2_session_callbacks_set_on_frame_recv_callback(_nghttp2_callbacks, _http2_frame_recv_cb); + nghttp2_session_callbacks_set_on_header_callback(_nghttp2_callbacks, _http2_on_header_callback); + + // nghttp2_session_callbacks_set_recv_callback(_nghttp2_callbacks, _recv_callback); + } + + /* setup HTTP/2 options */ + if (!_nghttp2_option) { + if (nghttp2_option_new(&_nghttp2_option)) { + perf_log_fatal("Unable to create nghttp2 options: out of memory"); + } + nghttp2_option_set_peer_max_concurrent_streams(_nghttp2_option, doh_max_concurr); + } + PERF_UNLOCK(&_nghttp2_lock); + } + + perf__doh_connect(sock); + + return sock; +} + +static struct _doh_stats doh_stats; + +void perf_net_doh_stats_init() +{ + memset(&doh_stats, 0, sizeof(doh_stats)); +} + +void perf_net_doh_stats_compile(struct perf_net_socket* sock) +{ + int i; + for (i = 0; i < (sizeof(doh_stats.code) / sizeof(doh_stats.code[0])); i++) { + doh_stats.code[i] += self->stats.code[i]; + } + doh_stats.unknown_code += self->stats.unknown_code; +} + +void perf_net_doh_stats_print() +{ + printf("DNS-over-HTTPS statistics:\n\n"); + + printf(" HTTP/2 return codes: "); + bool first_code = true, no_codes = true; + int i; + for (i = 0; i < (sizeof(doh_stats.code) / sizeof(doh_stats.code[0])); i++) { + if (doh_stats.code[i]) { + if (first_code) + first_code = false; + else + printf(", "); + printf("%d: %zu", i + 100, doh_stats.code[i]); + no_codes = false; + } + } + if (doh_stats.unknown_code) { + if (!first_code) + printf(", "); + printf("unknown: %zu", doh_stats.unknown_code); + no_codes = false; + } + if (no_codes) { + printf("none"); + } + printf("\n"); + + printf("\n"); +} diff --git a/src/net_dot.c b/src/net_dot.c index a662f7d..6b53e35 100644 --- a/src/net_dot.c +++ b/src/net_dot.c @@ -61,8 +61,6 @@ static void perf__dot_connect(struct perf_net_socket* sock) { int ret; - self->is_ready = true; - int fd = socket(self->server.sa.sa.sa_family, SOCK_STREAM, 0); if (fd == -1) { char __s[256]; @@ -119,12 +117,14 @@ static void perf__dot_connect(struct perf_net_socket* sock) } if (connect(sock->fd, &self->server.sa.sa, self->server.length)) { if (errno == EINPROGRESS) { - self->is_ready = false; + return; } else { char __s[256]; perf_log_fatal("connect() failed: %s", perf_strerror_r(errno, __s, sizeof(__s))); } } + + self->is_conn_ready = true; } static void perf__dot_reconnect(struct perf_net_socket* sock) @@ -136,6 +136,7 @@ static void perf__dot_reconnect(struct perf_net_socket* sock) self->sending = 0; self->is_sending = false; } + self->is_ready = false; self->is_conn_ready = false; perf__dot_connect(sock); } @@ -416,7 +417,7 @@ static bool perf__dot_have_more(struct perf_net_socket* sock) return self->have_more; } -struct perf_net_socket* perf_net_dot_opensocket(const perf_sockaddr_t* server, const perf_sockaddr_t* local, size_t bufsize) +struct perf_net_socket* perf_net_dot_opensocket(const perf_sockaddr_t* server, const perf_sockaddr_t* local, size_t bufsize, void* data, perf_net_sent_cb_t sent, perf_net_event_cb_t event) { struct perf__dot_socket* tmp = calloc(1, sizeof(struct perf__dot_socket)); // clang scan-build struct perf_net_socket* sock = (struct perf_net_socket*)tmp; @@ -433,6 +434,10 @@ struct perf_net_socket* perf_net_dot_opensocket(const perf_sockaddr_t* server, c sock->sockready = perf__dot_sockready; sock->have_more = perf__dot_have_more; + sock->data = data; + sock->sent = sent; + sock->event = event; + self->server = *server; self->local = *local; self->bufsize = bufsize; diff --git a/src/net_tcp.c b/src/net_tcp.c index c04f25d..97d1f65 100644 --- a/src/net_tcp.c +++ b/src/net_tcp.c @@ -367,7 +367,7 @@ static bool perf__tcp_have_more(struct perf_net_socket* sock) return self->have_more; } -struct perf_net_socket* perf_net_tcp_opensocket(const perf_sockaddr_t* server, const perf_sockaddr_t* local, size_t bufsize) +struct perf_net_socket* perf_net_tcp_opensocket(const perf_sockaddr_t* server, const perf_sockaddr_t* local, size_t bufsize, void* data, perf_net_sent_cb_t sent, perf_net_event_cb_t event) { struct perf__tcp_socket* tmp = calloc(1, sizeof(struct perf__tcp_socket)); // clang scan-build struct perf_net_socket* sock = (struct perf_net_socket*)tmp; @@ -384,6 +384,10 @@ struct perf_net_socket* perf_net_tcp_opensocket(const perf_sockaddr_t* server, c sock->sockready = perf__tcp_sockready; sock->have_more = perf__tcp_have_more; + sock->data = data; + sock->sent = sent; + sock->event = event; + self->server = *server; self->local = *local; self->bufsize = bufsize; diff --git a/src/net_udp.c b/src/net_udp.c index 03e0ae1..685e609 100644 --- a/src/net_udp.c +++ b/src/net_udp.c @@ -61,7 +61,7 @@ static int perf__udp_sockready(struct perf_net_socket* sock, int pipe_fd, int64_ return 1; } -struct perf_net_socket* perf_net_udp_opensocket(const perf_sockaddr_t* server, const perf_sockaddr_t* local, size_t bufsize) +struct perf_net_socket* perf_net_udp_opensocket(const perf_sockaddr_t* server, const perf_sockaddr_t* local, size_t bufsize, void* data, perf_net_sent_cb_t sent, perf_net_event_cb_t event) { struct perf__udp_socket* tmp = calloc(1, sizeof(struct perf__udp_socket)); // clang scan-build struct perf_net_socket* sock = (struct perf_net_socket*)tmp; @@ -79,6 +79,10 @@ struct perf_net_socket* perf_net_udp_opensocket(const perf_sockaddr_t* server, c sock->sockeq = perf__udp_sockeq; sock->sockready = perf__udp_sockready; + sock->data = data; + sock->sent = sent; + sock->event = event; + sock->fd = socket(server->sa.sa.sa_family, SOCK_DGRAM, 0); if (sock->fd == -1) { char __s[256]; @@ -44,7 +44,7 @@ typedef struct { const char* desc; const char* help; const char* defval; - char defvalbuf[32]; + char defvalbuf[512]; union { void* valp; char** stringp; @@ -56,7 +56,28 @@ typedef struct { } u; } opt_t; +typedef struct long_opt long_opt_t; +struct long_opt { + long_opt_t* next; + const char* name; + perf_opttype_t type; + const char* desc; + const char* help; + const char* defval; + char defvalbuf[512]; + union { + void* valp; + char** stringp; + bool* boolp; + unsigned int* uintp; + uint64_t* uint64p; + double* doublep; + in_port_t* portp; + } u; +}; + static opt_t opts[MAX_OPTS]; +static long_opt_t* longopts = 0; static unsigned int nopts; static char optstr[MAX_OPTS * 2 + 2 + 1] = { 0 }; extern const char* progname; @@ -76,13 +97,13 @@ void perf_opt_add(char c, perf_opttype_t type, const char* desc, const char* hel opt->desc = desc; opt->help = help; if (defval != NULL) { - opt->defvalbuf[sizeof(opt->defvalbuf) - 1] = 0; - strncpy(opt->defvalbuf, defval, sizeof(opt->defvalbuf)); - if (opt->defvalbuf[sizeof(opt->defvalbuf) - 1]) { + if (strlen(defval) > sizeof(opt->defvalbuf) - 1) { perf_log_fatal("perf_opt_add(): defval too large"); return; } - opt->defval = opt->defvalbuf; + strncpy(opt->defvalbuf, defval, sizeof(opt->defvalbuf)); + opt->defvalbuf[sizeof(opt->defvalbuf) - 1] = 0; + opt->defval = opt->defvalbuf; } else { opt->defval = NULL; } @@ -94,6 +115,37 @@ void perf_opt_add(char c, perf_opttype_t type, const char* desc, const char* hel optstr[sizeof(optstr) - 1] = 0; } +void perf_long_opt_add(const char* name, perf_opttype_t type, const char* desc, const char* help, const char* defval, void* valp) +{ + long_opt_t* opt = calloc(1, sizeof(long_opt_t)); + + if (!opt) { + perf_log_fatal("perf_long_opt_add(): out of memory"); + return; + } + + opt->name = name; + opt->type = type; + opt->desc = desc; + opt->help = help; + if (defval != NULL) { + if (strlen(defval) > sizeof(opt->defvalbuf) - 1) { + perf_log_fatal("perf_opt_add(): defval too large"); + free(opt); // fix clang scan-build + return; + } + strncpy(opt->defvalbuf, defval, sizeof(opt->defvalbuf)); + opt->defvalbuf[sizeof(opt->defvalbuf) - 1] = 0; + opt->defval = opt->defvalbuf; + } else { + opt->defval = NULL; + } + opt->u.valp = valp; + + opt->next = longopts; + longopts = opt; +} + void perf_opt_usage(void) { unsigned int prefix_len, position, arg_len, i, j; @@ -181,6 +233,70 @@ parse_timeval(const char* desc, const char* str) return MILLION * parse_double(desc, str); } +static int perf_opt_long_parse(char* optarg) +{ + ssize_t optlen; + char* arg; + + // TODO: Allow boolean not to have =/value + if (!(arg = strchr(optarg, '='))) { + return -1; + } + optlen = arg - optarg; + if (optlen < 1) { + return -1; + } + arg++; + + long_opt_t* opt = longopts; + while (opt) { + if (!strncmp(optarg, opt->name, optlen)) { + switch (opt->type) { + case perf_opt_string: + *opt->u.stringp = arg; + break; + case perf_opt_boolean: + *opt->u.boolp = true; + break; + case perf_opt_uint: + *opt->u.uintp = parse_uint(opt->desc, arg, 1, 0xFFFFFFFF); + break; + case perf_opt_zpint: + *opt->u.uintp = parse_uint(opt->desc, arg, 0, 0xFFFFFFFF); + break; + case perf_opt_timeval: + *opt->u.uint64p = parse_timeval(opt->desc, arg); + break; + case perf_opt_double: + *opt->u.doublep = parse_double(opt->desc, arg); + break; + case perf_opt_port: + *opt->u.portp = parse_uint(opt->desc, arg, 0, 0xFFFF); + break; + } + return 0; + } + opt = opt->next; + } + + return -1; +} + +void perf_long_opt_usage(void) +{ + fprintf(stderr, "Usage: %s ... -O <name>=<value> ...\n\nAvailable long options:\n", progname); + long_opt_t* opt = longopts; + while (opt) { + fprintf(stderr, " %s: %s", opt->name, opt->help); + if (opt->defval) { + fprintf(stderr, " (default: %s)", opt->defval); + } + fprintf(stderr, "\n"); + + opt = opt->next; + } +} + void perf_opt_parse(int argc, char** argv) { int c; @@ -188,6 +304,8 @@ void perf_opt_parse(int argc, char** argv) unsigned int i; perf_opt_add('h', perf_opt_boolean, NULL, "print this help", NULL, NULL); + perf_opt_add('H', perf_opt_boolean, NULL, "print long options help", NULL, NULL); + perf_opt_add('O', perf_opt_string, NULL, "set long options: <name>=<value>", NULL, NULL); while ((c = getopt(argc, argv, optstr)) != -1) { for (i = 0; i < nopts; i++) { @@ -202,6 +320,18 @@ void perf_opt_parse(int argc, char** argv) perf_opt_usage(); exit(0); } + if (c == 'H') { + perf_long_opt_usage(); + exit(0); + } + if (c == 'O') { + if (perf_opt_long_parse(optarg)) { + fprintf(stderr, "invalid long option: %s\n", optarg); + perf_opt_usage(); + exit(1); + } + continue; + } opt = &opts[i]; switch (opt->type) { case perf_opt_string: @@ -30,10 +30,11 @@ typedef enum { perf_opt_port, } perf_opttype_t; -void perf_opt_add(char c, perf_opttype_t type, const char* desc, const char* help, - const char* defval, void* valp); +void perf_opt_add(char c, perf_opttype_t type, const char* desc, const char* help, const char* defval, void* valp); +void perf_long_opt_add(const char* name, perf_opttype_t type, const char* desc, const char* help, const char* defval, void* valp); void perf_opt_usage(void); +void perf_long_opt_usage(void); void perf_opt_parse(int argc, char** argv); diff --git a/src/parse_uri.c b/src/parse_uri.c new file mode 100644 index 0000000..fa74c85 --- /dev/null +++ b/src/parse_uri.c @@ -0,0 +1,112 @@ +#include "parse_uri.h" + +#include <string.h> + +// From: https://github.com/nghttp2/nghttp2/blob/master/examples/client.c +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2013 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +int parse_uri(struct URI* res, const char* uri) +{ + /* We only interested in https */ + size_t len, i, offset; + int ipv6addr = 0; + memset(res, 0, sizeof(struct URI)); + len = strlen(uri); + if (len < 9 || memcmp("https://", uri, 8) != 0) { + return -1; + } + offset = 8; + res->host = res->hostport = &uri[offset]; + res->hostlen = 0; + if (uri[offset] == '[') { + /* IPv6 literal address */ + ++offset; + ++res->host; + ipv6addr = 1; + for (i = offset; i < len; ++i) { + if (uri[i] == ']') { + res->hostlen = i - offset; + offset = i + 1; + break; + } + } + } else { + const char delims[] = ":/?#"; + for (i = offset; i < len; ++i) { + if (strchr(delims, uri[i]) != NULL) { + break; + } + } + res->hostlen = i - offset; + offset = i; + } + if (res->hostlen == 0) { + return -1; + } + /* Assuming https */ + res->port = 443; + if (offset < len) { + if (uri[offset] == ':') { + /* port */ + const char delims[] = "/?#"; + int port = 0; + ++offset; + for (i = offset; i < len; ++i) { + if (strchr(delims, uri[i]) != NULL) { + break; + } + if ('0' <= uri[i] && uri[i] <= '9') { + port *= 10; + port += uri[i] - '0'; + if (port > 65535) { + return -1; + } + } else { + return -1; + } + } + if (port == 0) { + return -1; + } + offset = i; + res->port = (uint16_t)port; + } + } + res->hostportlen = (size_t)(uri + offset + ipv6addr - res->host); + for (i = offset; i < len; ++i) { + if (uri[i] == '#') { + break; + } + } + if (i - offset == 0) { + res->path = "/"; + res->pathlen = 1; + } else { + res->path = &uri[offset]; + res->pathlen = i - offset; + } + return 0; +} diff --git a/src/parse_uri.h b/src/parse_uri.h new file mode 100644 index 0000000..bb69e79 --- /dev/null +++ b/src/parse_uri.h @@ -0,0 +1,41 @@ +// From: https://github.com/nghttp2/nghttp2/blob/master/examples/client.c +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2013 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include <inttypes.h> +#include <stdlib.h> + +struct URI { + const char* host; + /* In this program, path contains query component as well. */ + const char* path; + size_t pathlen; + const char* hostport; + size_t hostlen; + size_t hostportlen; + uint16_t port; +}; + +int parse_uri(struct URI* res, const char* uri); diff --git a/src/resperf.1.in b/src/resperf.1.in index ea077f2..8a37457 100644 --- a/src/resperf.1.in +++ b/src/resperf.1.in @@ -44,6 +44,7 @@ resperf \- test the resolution performance of a caching DNS server [\fB\-F\ \fIfall_behind\fR] [\fB\-v\fR] [\fB\-W\fR] +[\fB\-O\ \fIoption=value\fR] .ad .hy .hy 0 @@ -74,6 +75,7 @@ resperf \- test the resolution performance of a caching DNS server [\fB\-F\ \fIfall_behind\fR] [\fB\-v\fR] [\fB\-W\fR] +[\fB\-O\ \fIoption=value\fR] .ad .hy .SH DESCRIPTION @@ -413,6 +415,11 @@ itself and the server under test can sustain. Otherwise, the test is likely to be cut short as a result of either running out of query IDs (because of large numbers of dropped queries) or of \fBresperf\fR falling behind its transmission schedule. +.SS "Using DNS-over-HTTPS" +When using DNS-over-HTTPS you must set the \fB-O doh\-uri=...\fR to something +that works with the server you're sending to. +Also note that the value for maximum outstanding queries will be used to +control the maximum concurrent streams within the HTTP/2 connection. .SH OPTIONS Because the \fBresperf\-report\fR script passes its command line options directly to the \fBresperf\fR programs, they both accept the same set of @@ -437,7 +444,7 @@ This allows for long running tests on very small and simple query datafile. \fB-M \fImode\fR .br .RS -Specifies the transport mode to use, "udp", "tcp" or "dot". +Specifies the transport mode to use, "udp", "tcp", "dot" or "doh". Default is "udp". .RE @@ -452,7 +459,7 @@ The default is the loopback address, 127.0.0.1. .br .RS Sets the port on which the DNS packets are sent. -If not specified, the standard DNS port (udp/tcp 53, DoT 853) is used. +If not specified, the standard DNS port (udp/tcp 53, DoT 853, DoH 443) is used. .RE \fB-a \fIlocal_addr\fR @@ -634,6 +641,14 @@ Enables verbose mode to report about network readiness and congestion. Log warnings and errors to standard output instead of standard error making it easier for script, test and automation to capture all output. .RE + +\fB-O \fIoption=value\fR +.br +.RS +Set an extended long option for various things to control different aspects +of testing or protocol modules, see EXTENDED OPTIONS in \fBdnsperf\fR(1) for +list of available options. +.RE .SH "THE PLOT DATA FILE" The plot data file is written by the \fBresperf\fR program and contains the data to be plotted using \fBgnuplot\fR. diff --git a/src/resperf.c b/src/resperf.c index f7038e9..2a93bf1 100644 --- a/src/resperf.c +++ b/src/resperf.c @@ -53,7 +53,8 @@ #define DEFAULT_SERVER_NAME "127.0.0.1" #define DEFAULT_SERVER_PORT 53 #define DEFAULT_SERVER_DOT_PORT 853 -#define DEFAULT_SERVER_PORTS "udp/tcp 53 or DoT 853" +#define DEFAULT_SERVER_DOH_PORT 443 +#define DEFAULT_SERVER_PORTS "udp/tcp 53, DoT 853 or DoH 443" #define DEFAULT_LOCAL_PORT 0 #define DEFAULT_SOCKET_BUFFER 32 #define DEFAULT_TIMEOUT 45 @@ -218,8 +219,23 @@ stringify(double value, int precision) static void perf__net_event(struct perf_net_socket* sock, perf_socket_event_t event, uint64_t elapsed_time); static void perf__net_sent(struct perf_net_socket* sock, uint16_t qid); -static void -setup(int argc, char** argv) +static ramp_bucket* init_buckets(int n) +{ + ramp_bucket* p; + int i; + + if (!(p = calloc(n, sizeof(*p)))) { + perf_log_fatal("out of memory"); + return 0; // fix clang scan-build + } + for (i = 0; i < n; i++) { + p[i].queries = p[i].responses = p[i].failures = 0; + p[i].latency_sum = 0.0; + } + return p; +} + +static void setup(int argc, char** argv) { const char* family = NULL; const char* server_name = DEFAULT_SERVER_NAME; @@ -233,6 +249,8 @@ setup(int argc, char** argv) unsigned int i; const char* _mode = 0; const char* edns_option_str = NULL; + const char* doh_uri = DEFAULT_DOH_URI; + const char* doh_method = DEFAULT_DOH_METHOD; sock_family = AF_UNSPEC; server_port = 0; @@ -251,7 +269,7 @@ setup(int argc, char** argv) perf_opt_add('f', perf_opt_string, "family", "address family of DNS transport, inet or inet6", "any", &family); - perf_opt_add('M', perf_opt_string, "mode", "set transport mode: udp, tcp or dot", "udp", &_mode); + perf_opt_add('M', perf_opt_string, "mode", "set transport mode: udp, tcp, dot or doh", "udp", &_mode); perf_opt_add('s', perf_opt_string, "server_addr", "the server to query", DEFAULT_SERVER_NAME, &server_name); perf_opt_add('p', perf_opt_port, "port", @@ -310,6 +328,10 @@ setup(int argc, char** argv) perf_opt_add('R', perf_opt_boolean, NULL, "reopen datafile on end, allow for infinit use of it", NULL, &reopen_datafile); perf_opt_add('F', perf_opt_zpint, "fall_behind", "the maximum number of queries that is allowed to fall behind, zero to disable", stringify(DEFAULT_MAX_FALL_BEHIND, 0), &max_fall_behind); + perf_long_opt_add("doh-uri", perf_opt_string, "doh_uri", + "the URI to use for DNS-over-HTTPS", DEFAULT_DOH_URI, &doh_uri); + perf_long_opt_add("doh-method", perf_opt_string, "doh_method", + "the HTTP method to use for DNS-over-HTTPS: GET or POST", DEFAULT_DOH_METHOD, &doh_method); perf_opt_parse(argc, argv); @@ -321,8 +343,26 @@ setup(int argc, char** argv) mode = perf_net_parsemode(_mode); if (!server_port) { - server_port = mode == sock_dot ? DEFAULT_SERVER_DOT_PORT : DEFAULT_SERVER_PORT; + switch (mode) { + case sock_doh: + server_port = DEFAULT_SERVER_DOH_PORT; + break; + case sock_dot: + server_port = DEFAULT_SERVER_DOT_PORT; + break; + default: + server_port = DEFAULT_SERVER_PORT; + break; + } + } + + if (doh_uri) { + perf_net_doh_parse_uri(doh_uri); + } + if (doh_method) { + perf_net_doh_parse_method(doh_method); } + perf_net_doh_set_max_concurrent_streams(max_outstanding); if (max_outstanding > nsocks * DEFAULT_MAX_OUTSTANDING) perf_log_fatal("number of outstanding packets (%u) must not " @@ -364,17 +404,23 @@ setup(int argc, char** argv) if (edns_option_str != NULL) edns_option = perf_edns_parseoption(edns_option_str); + traffic_time = ramp_time + sustain_time; + end_time = traffic_time + wait_time; + + n_buckets = (traffic_time + bucket_interval - 1) / bucket_interval; + buckets = init_buckets(n_buckets); + + time_now = perf_get_time(); + time_of_program_start = time_now; + if (!(socks = calloc(nsocks, sizeof(*socks)))) { perf_log_fatal("out of memory"); } for (i = 0; i < nsocks; i++) { - socks[i] = perf_net_opensocket(mode, &server_addr, &local_addr, i, bufsize); + socks[i] = perf_net_opensocket(mode, &server_addr, &local_addr, i, bufsize, (void*)(intptr_t)i, perf__net_sent, perf__net_event); if (!socks[i]) { perf_log_fatal("perf_net_opensocket(): no socket returned, out of memory?"); } - socks[i]->data = (void*)(intptr_t)i; - socks[i]->sent = perf__net_sent; - socks[i]->event = perf__net_event; } } @@ -384,8 +430,10 @@ cleanup(void) unsigned int i; perf_datafile_close(&input); - for (i = 0; i < nsocks; i++) + for (i = 0; i < nsocks; i++) { + perf_net_stats_compile(mode, socks[i]); (void)perf_net_close(socks[i]); + } close(dummypipe[0]); close(dummypipe[1]); @@ -499,23 +547,7 @@ print_statistics(void) } printf(" Maximum throughput: %.6lf qps\n", max_throughput); printf(" Lost at that point: %.2f%%\n", loss_at_max_throughput); -} - -static ramp_bucket* -init_buckets(int n) -{ - ramp_bucket* p; - int i; - - if (!(p = calloc(n, sizeof(*p)))) { - perf_log_fatal("out of memory"); - return 0; // fix clang scan-build - } - for (i = 0; i < n; i++) { - p[i].queries = p[i].responses = p[i].failures = 0; - p[i].latency_sum = 0.0; - } - return p; + printf("\n"); } /* @@ -776,6 +808,7 @@ int main(int argc, char** argv) switch (mode) { case sock_tcp: case sock_dot: + case sock_doh: // block SIGPIPE for TCP/DOT mode, if connection is closed it will generate a signal perf_os_blocksignal(SIGPIPE, true); break; @@ -788,15 +821,6 @@ int main(int argc, char** argv) max_packet_size = edns ? MAX_EDNS_PACKET : MAX_UDP_PACKET; perf_buffer_init(&msg, outpacket_buffer, max_packet_size); - traffic_time = ramp_time + sustain_time; - end_time = traffic_time + wait_time; - - n_buckets = (traffic_time + bucket_interval - 1) / bucket_interval; - buckets = init_buckets(n_buckets); - - time_now = perf_get_time(); - time_of_program_start = time_now; - printf("[Status] Command line: %s", progname); for (i = 1; i < argc; i++) { printf(" %s", argv[i]); @@ -896,7 +920,9 @@ end_loop: fclose(plotf); print_statistics(); + perf_net_stats_init(mode); cleanup(); + perf_net_stats_print(mode); #if OPENSSL_VERSION_NUMBER < 0x10100000L ERR_free_strings(); #endif diff --git a/src/test/Makefile.in b/src/test/Makefile.in index 4c89ac9..fa6167e 100644 --- a/src/test/Makefile.in +++ b/src/test/Makefile.in @@ -441,6 +441,8 @@ libdir = @libdir@ libexecdir = @libexecdir@ libldns_CFLAGS = @libldns_CFLAGS@ libldns_LIBS = @libldns_LIBS@ +libnghttp2_CFLAGS = @libnghttp2_CFLAGS@ +libnghttp2_LIBS = @libnghttp2_LIBS@ libssl_CFLAGS = @libssl_CFLAGS@ libssl_LIBS = @libssl_LIBS@ localedir = @localedir@ diff --git a/src/test/test2.sh b/src/test/test2.sh index e42ef49..a5e72ad 100755 --- a/src/test/test2.sh +++ b/src/test/test2.sh @@ -100,6 +100,9 @@ echo " A" | ../dnsperf -W -s 1.1.1.1 \ echo "toooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooolongname" \ | ../dnsperf -W -s 1.1.1.1 -u \ | grep "Unable to parse domain name" +echo "tooooooooooooooooooooooooooooo.oooooooooooooooooooooooo.ooooooooooooooooooooooooooooo.ooooooooooooooooooooooooooooooo.oooooooooooooooooooooooooooooooooooooo.oooooooooooooooooooooooooooo.ooooooooooooooooooooooooo.ooooooooooooooooooooooooooooooooo.oooooooooooooooooooooooooooo.oooooooooooooooooooooooooooooooo.ooooooooooooooooooo.longname" \ + | ../dnsperf -W -s 1.1.1.1 -u \ + | grep "Unable to parse domain name" echo -e "test\ndelete toooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooolongname" \ | ../dnsperf -W -s 1.1.1.1 -u \ | grep "invalid update command, domain name too large" diff --git a/src/test/test5.sh b/src/test/test5.sh index 40c9b31..515a5ff 100755 --- a/src/test/test5.sh +++ b/src/test/test5.sh @@ -5,13 +5,17 @@ test "$TEST_DNSPERF_WITH_NETWORK" = "1" || exit 0 dumdumd=`which dumdumd` if [ -n "$dumdumd" ]; then + pkill -9 dumdumd || true + $dumdumd 127.0.0.1 5353 -r -D 100 & pid="$!" + sleep 2 ../dnsperf -s 127.0.0.1 -p 5353 -d "$srcdir/datafile" -t 2 -l 2 -Q 10 -m tcp kill "$pid" $dumdumd 127.0.0.1 5353 -r -D 10 & pid="$!" + sleep 2 ../dnsperf -s 127.0.0.1 -p 5353 -d "$srcdir/datafile" -t 2 -l 10 -Q 100 -m tcp kill "$pid" @@ -20,11 +24,13 @@ if [ -n "$dumdumd" ]; then $dumdumd 127.0.0.1 5353 -r -T -D 100 & pid="$!" + sleep 2 ../dnsperf -s 127.0.0.1 -p 5353 -d "$srcdir/datafile" -t 2 -l 2 -Q 10 -m dot kill "$pid" $dumdumd 127.0.0.1 5353 -r -T -D 10 & pid="$!" + sleep 2 ../dnsperf -s 127.0.0.1 -p 5353 -d "$srcdir/datafile" -t 2 -l 10 -Q 100 -m dot kill "$pid" @@ -28,6 +28,7 @@ #include <stdbool.h> #include <string.h> #include <sys/time.h> +#include <errno.h> #define MILLION ((uint64_t)1000000) @@ -76,6 +77,19 @@ } \ } while (0) +static inline int PERF_TRYLOCK(pthread_mutex_t* mutex) +{ + int __n = pthread_mutex_trylock(mutex); + if (__n == EBUSY) { + return 1; + } + if (__n != 0) { + char __s[256]; + perf_log_fatal("pthread_mutex_lock failed: %s", perf_strerror_r(__n, __s, sizeof(__s))); + } + return 0; +} + #define PERF_UNLOCK(mutex) \ do { \ int __n = pthread_mutex_unlock((mutex)); \ |