diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index b62051e..caea824 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -22,7 +22,7 @@ jobs: run: sudo apt-get update - name: Setup Dependencies - run: sudo apt-get install cmake libc-ares-dev libcurl4-openssl-dev libev-dev build-essential clang-tidy dnsutils python3-pip python3-venv valgrind ${{ matrix.compiler }} + run: sudo apt-get install cmake libc-ares-dev libcurl4-openssl-dev libev-dev libsystemd-dev build-essential clang-tidy dnsutils python3-pip python3-venv valgrind ${{ matrix.compiler }} - name: Setup Python Virtual Environment run: python3 -m venv ${{github.workspace}}/venv diff --git a/.gitignore b/.gitignore index 49ddd59..967c5bd 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ output.xml report.html custom_curl/ valgrind-*.log +tests/robot/__pycache__ diff --git a/CMakeLists.txt b/CMakeLists.txt index 55152ec..5c6fe60 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,8 @@ cmake_minimum_required(VERSION 3.7) project(HttpsDnsProxy C) +include(CheckIncludeFile) + # FUNCTIONS # source: https://stackoverflow.com/a/27990434 @@ -25,15 +27,19 @@ if (NOT CMAKE_INSTALL_BINDIR) set(CMAKE_INSTALL_BINDIR bin) endif() -set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra --pedantic -Wno-strict-aliasing -Wno-variadic-macros") +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Wpedantic -Wstrict-aliasing -Wformat=2 -Wunused -Wno-variadic-macros -Wnull-dereference -Wshadow -Wconversion -Wsign-conversion -Wfloat-conversion -Wimplicit-fallthrough") set(CMAKE_C_FLAGS_DEBUG "-gdwarf-4 -DDEBUG") set(CMAKE_C_FLAGS_RELEASE "-O2") -if ((CMAKE_C_COMPILER_ID MATCHES GNU AND CMAKE_C_COMPILER_VERSION VERSION_GREATER_EQUAL 9) OR - (CMAKE_C_COMPILER_ID MATCHES Clang AND CMAKE_C_COMPILER_VERSION VERSION_GREATER_EQUAL 10)) +if (((CMAKE_C_COMPILER_ID MATCHES GNU AND CMAKE_C_COMPILER_VERSION VERSION_GREATER_EQUAL 9) AND + (CMAKE_C_COMPILER_ID MATCHES GNU AND CMAKE_C_COMPILER_VERSION VERSION_LESS 14)) OR + ( CMAKE_C_COMPILER_ID MATCHES Clang AND CMAKE_C_COMPILER_VERSION VERSION_GREATER_EQUAL 10)) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-gnu-zero-variadic-macro-arguments -Wno-gnu-folding-constant") endif() +set(SERVICE_EXTRA_OPTIONS "") +set(SERVICE_TYPE "simple") + # VERSION # It is possible to define external default value like: cmake -DSW_VERSION=1.2-custom @@ -81,6 +87,15 @@ include_directories( ${LIBCARES_INCLUDE_DIR} ${LIBCURL_INCLUDE_DIR} ${LIBEV_INCLUDE_DIR} src) +check_include_file("systemd/sd-daemon.h" HAVE_SD_DAEMON_H) + +if(HAVE_SD_DAEMON_H) + message(STATUS "Using libsystemd") + add_definitions(-DHAS_LIBSYSTEMD=1) + set(LIBS ${LIBS} systemd) + set(SERVICE_TYPE "notify") +endif() + # CLANG TIDY option(USE_CLANG_TIDY "Use clang-tidy during compilation" ON) @@ -95,7 +110,7 @@ if(USE_CLANG_TIDY) message(STATUS "clang-tidy not found.") else() message(STATUS "clang-tidy found: ${CLANG_TIDY_EXE}") - set(DO_CLANG_TIDY "${CLANG_TIDY_EXE}" "-fix" "-fix-errors" "-checks=*,-cert-err34-c,-readability-identifier-length,-altera-unroll-loops,-bugprone-easily-swappable-parameters,-concurrency-mt-unsafe,-*magic-numbers,-hicpp-signed-bitwise,-readability-function-cognitive-complexity,-altera-id-dependent-backward-branch,-google-readability-todo,-misc-include-cleaner") + set(DO_CLANG_TIDY "${CLANG_TIDY_EXE}" "-fix" "-fix-errors" "-checks=*,-cert-err34-c,-readability-identifier-length,-altera-unroll-loops,-bugprone-easily-swappable-parameters,-concurrency-mt-unsafe,-*magic-numbers,-hicpp-signed-bitwise,-readability-function-cognitive-complexity,-altera-id-dependent-backward-branch,-google-readability-todo,-misc-include-cleaner,-cast-align") endif() else() message(STATUS "Not using clang-tidy.") @@ -132,7 +147,6 @@ endif() install(TARGETS ${TARGET_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR}) -set(SERVICE_EXTRA_OPTIONS "") if(IS_DIRECTORY "/etc/munin/plugins" AND IS_DIRECTORY "/etc/munin/plugin-conf.d") set(SERVICE_EXTRA_OPTIONS "-s 300") diff --git a/README.md b/README.md index 14313c8..08573af 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ `https_dns_proxy` is a light-weight DNS<-->HTTPS, non-caching translation proxy for the [RFC 8484][rfc-8484] DNS-over-HTTPS standard. It receives -regular (UDP) DNS requests and issues them via DoH. +regular (UDP or TCP) DNS requests and issues them via DoH. [Google's DNS-over-HTTPS][google-doh] service is default, but [Cloudflare's service][cloudflare-doh] also works with trivial commandline flag @@ -48,6 +48,7 @@ Depends on `c-ares (>=1.11.0)`, `libcurl (>=7.66.0)`, `libev (>=4.25)`. On Debian-derived systems those are libc-ares-dev, libcurl4-{openssl,nss,gnutls}-dev and libev-dev respectively. On Redhat-derived systems those are c-ares-devel, libcurl-devel and libev-devel. +On systems with systemd it is recommended to have libsystemd development package installed. On MacOS, you may run into issues with curl headers. Others have had success when first installing curl with brew. ``` @@ -57,7 +58,7 @@ brew link curl --force On Ubuntu ``` -apt-get install cmake libc-ares-dev libcurl4-openssl-dev libev-dev build-essential +apt-get install cmake libc-ares-dev libcurl4-openssl-dev libev-dev libsystemd-dev build-essential ``` If all pre-requisites are met, you should be able to build with: @@ -158,7 +159,7 @@ docker run --name "https-dns-proxy" -p 5053:5053/udp \ Just run it as a daemon and point traffic at it. Commandline flags are: ``` -Usage: ./https_dns_proxy [-a ] [-p ] +Usage: ./https_dns_proxy [-a ] [-p ] [-T ] [-b ] [-i ] [-4] [-r ] [-t ] [-x] [-q] [-C ] [-c ] [-d] [-u ] [-g ] @@ -167,6 +168,8 @@ Usage: ./https_dns_proxy [-a ] [-p ] DNS server -a listen_addr Local IPv4/v6 address to bind to. (Default: 127.0.0.1) -p listen_port Local port to bind to. (Default: 5053) + -T tcp_client_limit Number of TCP clients to serve. + (Default: 20, Disabled: 0, Min: 1, Max: 200) DNS client -b dns_servers Comma-separated IPv4/v6 addresses and ports (addr:port) @@ -189,6 +192,9 @@ Usage: ./https_dns_proxy [-a ] [-p ] -q Use HTTP/3 (QUIC) only. -m max_idle_time Maximum idle time in seconds allowed for reusing a HTTPS connection. (Default: 118, Min: 0, Max: 3600) + -L conn_loss_time Time in seconds to tolerate connection timeouts of reused connections. + This option mitigates half-open TCP connection issue (e.g. WAN IP change). + (Default: 15, Min: 5, Max: 60) -C ca_path Optional file containing CA certificates. -c dscp_codepoint Optional DSCP codepoint to set on upstream HTTPS server connections. (Min: 0, Max: 63) diff --git a/https_dns_proxy.service.in b/https_dns_proxy.service.in index cab76b6..bec7753 100644 --- a/https_dns_proxy.service.in +++ b/https_dns_proxy.service.in @@ -6,11 +6,13 @@ Before=nss-lookup.target After=network.target [Service] -Type=simple +Type=${SERVICE_TYPE} DynamicUser=yes -Restart=on-failure ExecStart=${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}/https_dns_proxy \ -v -v ${SERVICE_EXTRA_OPTIONS} +Restart=on-failure +RestartSec=5 +TimeoutStartSec=20 TimeoutStopSec=10 [Install] diff --git a/munin/https_dns_proxy.plugin b/munin/https_dns_proxy.plugin index 654450e..6f4663f 100755 --- a/munin/https_dns_proxy.plugin +++ b/munin/https_dns_proxy.plugin @@ -11,6 +11,8 @@ graph_scale no graph_args --base 1000 --lower-limit 0 requests.label Requests responses.label Responses +tcprequests.label TcpRequests +tcpresponses.label TcpResponses multigraph https_dns_proxy_latency graph_title HTTPS DNS proxy - latency @@ -19,6 +21,7 @@ graph_category network graph_scale no graph_args --base 1000 --lower-limit 0 latency.label Latency +tcplatency.label TcpLatency multigraph https_dns_proxy_connections graph_title HTTPS DNS proxy - connections @@ -40,7 +43,7 @@ EOM esac log_lines=$(journalctl --unit https_dns_proxy.service --output cat --since '6 minutes ago') -pattern='stat\.c:[0-9]+ ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+)$' +pattern='stat\.c:[0-9]+ ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+)$' # match log lines with pattern (last match will be used) IFS=' @@ -53,18 +56,26 @@ for line in $log_lines; do fi done -latency='U' if [ -n "${stat[3]}" ] && \ [ -n "${stat[2]}" ] && \ [ "${stat[2]}" -gt "0" ]; then latency=$((${stat[3]} / ${stat[2]})) fi +if [ -n "${stat[11]}" ] && \ + [ -n "${stat[10]}" ] && \ + [ "${stat[10]}" -gt "0" ]; then + tcplatency=$((${stat[11]} / ${stat[10]})) +fi + echo "multigraph https_dns_proxy_count" echo "requests.value ${stat[1]:-U}" echo "responses.value ${stat[2]:-U}" +echo "tcprequests.value ${stat[9]:-U}" +echo "tcpresponses.value ${stat[10]:-U}" echo "multigraph https_dns_proxy_latency" -echo "latency.value ${latency}" +echo "latency.value ${latency:-0}" +echo "tcplatency.value ${tcplatency:-0}" echo "multigraph https_dns_proxy_connections" echo "opened.value ${stat[6]:-U}" echo "closed.value ${stat[7]:-U}" diff --git a/src/dns_poller.c b/src/dns_poller.c index ce5cdd2..2ff30d8 100644 --- a/src/dns_poller.c +++ b/src/dns_poller.c @@ -7,12 +7,12 @@ static void sock_cb(struct ev_loop __attribute__((unused)) *loop, ev_io *w, int revents) { dns_poller_t *d = (dns_poller_t *)w->data; - ares_process_fd(d->ares, (revents & EV_READ) ? w->fd : ARES_SOCKET_BAD, - (revents & EV_WRITE) ? w->fd : ARES_SOCKET_BAD); + ares_process_fd(d->ares, (revents & EV_READ) ? w->fd : ARES_SOCKET_BAD, + (revents & EV_WRITE) ? w->fd : ARES_SOCKET_BAD); } static struct ev_io * get_io_event(dns_poller_t *d, int sock) { - for (int i = 0; i < d->io_events_count; i++) { + for (unsigned i = 0; i < d->io_events_count; i++) { if (d->io_events[i].fd == sock) { return &d->io_events[i]; } @@ -35,7 +35,7 @@ static void sock_state_cb(void *data, int fd, int read, int write) { // reserve and start new event on unused slot io_event_ptr = get_io_event(d, 0); if (!io_event_ptr) { - FLOG("c-ares needed more IO event handler, than the number of provided nameservers: %d", d->io_events_count); + FLOG("c-ares needed more IO event handler, than the number of provided nameservers: %u", d->io_events_count); } DLOG("Reserved new io event: %p", io_event_ptr); // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) @@ -44,21 +44,43 @@ static void sock_state_cb(void *data, int fd, int read, int write) { ev_io_start(d->loop, io_event_ptr); } -static char *get_addr_listing(char** addr_list, const int af) { +static char *get_addr_listing(struct ares_addrinfo_node * nodes) { char *list = (char *)calloc(1, POLLER_ADDR_LIST_SIZE); - char *pos = list; if (list == NULL) { FLOG("Out of mem"); } - for (int i = 0; addr_list[i]; i++) { - const char *res = ares_inet_ntop(af, addr_list[i], pos, - list + POLLER_ADDR_LIST_SIZE - 1 - pos); + char *pos = list; + unsigned ipv4 = 0; + unsigned ipv6 = 0; + + for (struct ares_addrinfo_node *node = nodes; node != NULL; node = node->ai_next) { + const char *res = NULL; + + if (node->ai_family == AF_INET) { + res = ares_inet_ntop(AF_INET, (const void *)&((struct sockaddr_in *)node->ai_addr)->sin_addr, + pos, (ares_socklen_t)(list + POLLER_ADDR_LIST_SIZE - 1 - pos)); + ipv4++; + } else if (node->ai_family == AF_INET6) { + res = ares_inet_ntop(AF_INET6, (const void *)&((struct sockaddr_in6 *)node->ai_addr)->sin6_addr, + pos, (ares_socklen_t)(list + POLLER_ADDR_LIST_SIZE - 1 - pos)); + ipv6++; + } else { + WLOG("Unhandled address family: %d", node->ai_family); + continue; + } + if (res != NULL) { pos += strlen(pos); *pos = ','; pos++; + } else { + DLOG("Not enough space left for further IP addresses"); // test with POLLER_ADDR_LIST_SIZE = 10 value + break; } } + + DLOG("Received %u IPv4 and %u IPv6 addresses", ipv4, ipv6); + if (pos == list) { free((void*)list); list = NULL; @@ -69,19 +91,20 @@ static char *get_addr_listing(char** addr_list, const int af) { } static void ares_cb(void *arg, int status, int __attribute__((unused)) timeouts, - struct hostent *h) { + struct ares_addrinfo *result) { dns_poller_t *d = (dns_poller_t *)arg; d->request_ongoing = 0; ev_tstamp interval = 5; // retry by default after some time if (status != ARES_SUCCESS) { WLOG("DNS lookup of '%s' failed: %s", d->hostname, ares_strerror(status)); - } else if (!h || h->h_length < 1) { + } else if (!result || result->nodes == NULL) { WLOG("No hosts found for '%s'", d->hostname); } else { interval = d->polling_interval; - d->cb(d->hostname, d->cb_data, get_addr_listing(h->h_addr_list, h->h_addrtype)); + d->cb(d->hostname, d->cb_data, get_addr_listing(result->nodes)); } + ares_freeaddrinfo(result); if (status != ARES_EDESTRUCTION) { DLOG("DNS poll interval changed to: %.0lf", interval); @@ -97,8 +120,8 @@ static ev_tstamp get_timeout(dns_poller_t *d) struct timeval tv; struct timeval *tvp = ares_timeout(d->ares, &max_tv, &tv); // NOLINTNEXTLINE(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) - ev_tstamp after = tvp->tv_sec + tvp->tv_usec * 1e-6; - return after ? after : 0.1; + ev_tstamp after = (double)tvp->tv_sec + (double)tvp->tv_usec * 1e-6; + return after > 0.1 ? after : 0.1; } static void timer_cb(struct ev_loop __attribute__((unused)) *loop, @@ -106,9 +129,14 @@ static void timer_cb(struct ev_loop __attribute__((unused)) *loop, dns_poller_t *d = (dns_poller_t *)w->data; if (d->request_ongoing) { - // process query timeouts - DLOG("Processing DNS queries"); + DLOG("Processing DNS query timeouts"); +#if ARES_VERSION_MAJOR >= 1 && ARES_VERSION_MINOR >= 34 + ares_process_fds(d->ares, NULL, 0, ARES_PROCESS_FLAG_NONE); +#elif ARES_VERSION_MAJOR >= 1 && ARES_VERSION_MINOR >= 28 + ares_process_fd(d->ares, ARES_SOCKET_BAD, ARES_SOCKET_BAD); +#else ares_process(d->ares, NULL, NULL); +#endif } else { DLOG("Starting DNS query"); // Cancel any pending queries before making new ones. c-ares can't be depended on to @@ -117,7 +145,14 @@ static void timer_cb(struct ev_loop __attribute__((unused)) *loop, // free memory tied up by any "zombie" queries. ares_cancel(d->ares); d->request_ongoing = 1; - ares_gethostbyname(d->ares, d->hostname, d->family, ares_cb, d); + + struct ares_addrinfo_hints hints; + memset(&hints, 0, sizeof(hints)); // NOLINT(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) + hints.ai_flags = ARES_AI_CANONNAME; + hints.ai_family = d->family; + hints.ai_socktype = SOCK_STREAM; + + ares_getaddrinfo(d->ares, d->hostname, "https", &hints, ares_cb, d); } if (d->request_ongoing) { // need to re-check, it might change! @@ -170,8 +205,8 @@ void dns_poller_init(dns_poller_t *d, struct ev_loop *loop, d->timer.data = d; ev_timer_start(d->loop, &d->timer); - int nameservers = 1; - for (int i = 0; bootstrap_dns[i]; i++) { + unsigned nameservers = 1; + for (unsigned i = 0; bootstrap_dns[i]; i++) { if (bootstrap_dns[i] == ',') { nameservers++; } @@ -181,7 +216,7 @@ void dns_poller_init(dns_poller_t *d, struct ev_loop *loop, if (!d->io_events) { FLOG("Out of mem"); } - for (int i = 0; i < nameservers; i++) { + for (unsigned i = 0; i < nameservers; i++) { d->io_events[i].data = d; } d->io_events_count = nameservers; diff --git a/src/dns_poller.h b/src/dns_poller.h index e4f63fd..e257220 100644 --- a/src/dns_poller.h +++ b/src/dns_poller.h @@ -30,7 +30,7 @@ typedef struct { ev_timer timer; ev_io *io_events; - int io_events_count; + unsigned io_events_count; } dns_poller_t; // Initializes c-ares and starts a timer for periodic DNS resolution on the diff --git a/src/dns_server.c b/src/dns_server.c index 9f150e8..2fc8f19 100644 --- a/src/dns_server.c +++ b/src/dns_server.c @@ -1,60 +1,41 @@ -#include // NOLINT(llvmlibc-restrict-system-libc-headers) -#include // NOLINT(llvmlibc-restrict-system-libc-headers) -#include // NOLINT(llvmlibc-restrict-system-libc-headers) -#include // NOLINT(llvmlibc-restrict-system-libc-headers) +#include // NOLINT(llvmlibc-restrict-system-libc-headers) +#include // NOLINT(llvmlibc-restrict-system-libc-headers) +#include // NOLINT(llvmlibc-restrict-system-libc-headers) #include -#include // NOLINT(llvmlibc-restrict-system-libc-headers) -#include // NOLINT(llvmlibc-restrict-system-libc-headers) -#include // NOLINT(llvmlibc-restrict-system-libc-headers) +#include // NOLINT(llvmlibc-restrict-system-libc-headers) +#include // NOLINT(llvmlibc-restrict-system-libc-headers) #include "dns_server.h" #include "logging.h" -enum { -REQUEST_MAX = 1500 // A default MTU. We don't do TCP so any bigger is likely a waste -}; - - // Creates and bind a listening UDP socket for incoming requests. -static int get_listen_sock(const char *listen_addr, int listen_port, - unsigned int *addrlen) { - struct addrinfo *ai = NULL; - struct addrinfo hints; - // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) - memset(&hints, 0, sizeof(struct addrinfo)); - /* prevent DNS lookups if leakage is our worry */ - hints.ai_flags = AI_NUMERICHOST; - - int res = getaddrinfo(listen_addr, NULL, &hints, &ai); - if(res != 0) { - FLOG("Error parsing listen address %s:%d (getaddrinfo): %s", listen_addr, listen_port, - gai_strerror(res)); - if(ai) { - freeaddrinfo(ai); - } - return -1; +static int get_listen_sock(struct addrinfo *listen_addrinfo) { + int sock = socket(listen_addrinfo->ai_family, SOCK_DGRAM, 0); + if (sock < 0) { + FLOG("Error creating socket: %s (%d)", strerror(errno), errno); } - struct sockaddr_in *saddr = (struct sockaddr_in*) ai->ai_addr; - - *addrlen = ai->ai_addrlen; - saddr->sin_port = htons(listen_port); - - int sock = socket(ai->ai_family, SOCK_DGRAM, 0); - if (sock < 0) { - FLOG("Error creating socket"); + uint16_t port = 0; + char ipstr[INET6_ADDRSTRLEN]; + if (listen_addrinfo->ai_family == AF_INET) { + port = ntohs(((struct sockaddr_in*) listen_addrinfo->ai_addr)->sin_port); + inet_ntop(AF_INET, &((struct sockaddr_in *)listen_addrinfo->ai_addr)->sin_addr, ipstr, sizeof(ipstr)); + } else if (listen_addrinfo->ai_family == AF_INET6) { + port = ntohs(((struct sockaddr_in6*) listen_addrinfo->ai_addr)->sin6_port); + inet_ntop(AF_INET6, &((struct sockaddr_in6 *)listen_addrinfo->ai_addr)->sin6_addr, ipstr, sizeof(ipstr)); + } else { + FLOG("Unknown address family: %d", listen_addrinfo->ai_family); } - res = bind(sock, ai->ai_addr, ai->ai_addrlen); + int res = bind(sock, listen_addrinfo->ai_addr, listen_addrinfo->ai_addrlen); if (res < 0) { - FLOG("Error binding %s:%d: %s (%d)", listen_addr, listen_port, - strerror(errno), res); + FLOG("Error binding on %s:%d UDP: %s (%d)", ipstr, port, + strerror(errno), errno); } - freeaddrinfo(ai); + ILOG("Listening on %s:%d UDP", ipstr, port); - ILOG("Listening on %s:%d", listen_addr, listen_port); return sock; } @@ -62,34 +43,41 @@ static void watcher_cb(struct ev_loop __attribute__((unused)) *loop, ev_io *w, int __attribute__((unused)) revents) { dns_server_t *d = (dns_server_t *)w->data; - char *buf = (char *)calloc(1, REQUEST_MAX + 1); - if (buf == NULL) { - FLOG("Out of mem"); - } - struct sockaddr_storage raddr; - /* recvfrom can write to addrlen */ - socklen_t tmp_addrlen = d->addrlen; - ssize_t len = recvfrom(w->fd, buf, REQUEST_MAX, 0, (struct sockaddr*)&raddr, - &tmp_addrlen); + char tmp_buf[DNS_REQUEST_BUFFER_SIZE]; + struct sockaddr_storage tmp_raddr; + socklen_t tmp_addrlen = d->addrlen; // recvfrom can write to addrlen + ssize_t len = recvfrom(w->fd, tmp_buf, DNS_REQUEST_BUFFER_SIZE, MSG_TRUNC, + (struct sockaddr*)&tmp_raddr, &tmp_addrlen); if (len < 0) { ELOG("recvfrom failed: %s", strerror(errno)); return; } + if (len > DNS_REQUEST_BUFFER_SIZE) { + WLOG("Unsupported request received, too large: %d. Limit is: %d", + len, DNS_REQUEST_BUFFER_SIZE); + return; + } - if (len < (int)sizeof(uint16_t)) { - WLOG("Malformed request received (too short)."); + if (len < DNS_HEADER_LENGTH) { + WLOG("Malformed request received, too short: %d", len); return; } - uint16_t tx_id = ntohs(*((uint16_t*)buf)); - d->cb(d, d->cb_data, (struct sockaddr*)&raddr, tx_id, buf, len); + char *dns_req = (char *)malloc((size_t)len); // To free buffer after https request is complete. + if (dns_req == NULL) { + FLOG("Out of mem"); + } + memcpy(dns_req, tmp_buf, (size_t)len); // NOLINT(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) + + d->cb(d, 0, d->cb_data, (struct sockaddr*)&tmp_raddr, dns_req, (size_t)len); } void dns_server_init(dns_server_t *d, struct ev_loop *loop, - const char *listen_addr, int listen_port, + struct addrinfo *listen_addrinfo, dns_req_received_cb cb, void *data) { d->loop = loop; - d->sock = get_listen_sock(listen_addr, listen_port, &d->addrlen); + d->sock = get_listen_sock(listen_addrinfo); + d->addrlen = listen_addrinfo->ai_addrlen; d->cb = cb; d->cb_data = data; @@ -99,9 +87,127 @@ void dns_server_init(dns_server_t *d, struct ev_loop *loop, ev_io_start(d->loop, &d->watcher); } -void dns_server_respond(dns_server_t *d, struct sockaddr *raddr, char *buf, - size_t blen) { - ssize_t len = sendto(d->sock, buf, blen, 0, raddr, d->addrlen); +static uint16_t get_edns_udp_size(const char *dns_req, const size_t dns_req_len) { + ares_dns_record_t *dnsrec = NULL; + ares_status_t parse_status = ares_dns_parse((const unsigned char *)dns_req, dns_req_len, 0, &dnsrec); + if (parse_status != ARES_SUCCESS) { + WLOG("Failed to parse DNS request: %s", ares_strerror((int)parse_status)); + return DNS_SIZE_LIMIT; + } + const uint16_t tx_id = ares_dns_record_get_id(dnsrec); + uint16_t udp_size = 0; + const size_t record_count = ares_dns_record_rr_cnt(dnsrec, ARES_SECTION_ADDITIONAL); + for (size_t i = 0; i < record_count; ++i) { + const ares_dns_rr_t *rr = ares_dns_record_rr_get(dnsrec, ARES_SECTION_ADDITIONAL, i); + if (ares_dns_rr_get_type(rr) == ARES_REC_TYPE_OPT) { + udp_size = ares_dns_rr_get_u16(rr, ARES_RR_OPT_UDP_SIZE); + if (udp_size > 0) { + DLOG("%04hX: Found EDNS0 UDP buffer size: %u", tx_id, udp_size); + } + break; + } + } + ares_dns_record_destroy(dnsrec); + if (udp_size < DNS_SIZE_LIMIT) { + DLOG("%04hX: EDNS0 UDP buffer size %u overruled to %d", tx_id, udp_size, DNS_SIZE_LIMIT); + return DNS_SIZE_LIMIT; // RFC6891 4.3 "Values lower than 512 MUST be treated as equal to 512." + } + return udp_size; +} + +static void truncate_dns_response(char *buf, size_t *buflen, const uint16_t size_limit) { + const size_t old_size = *buflen; + buf[2] |= 0x02; // anyway: set truncation flag + + ares_dns_record_t *dnsrec = NULL; + ares_status_t status = ares_dns_parse((const unsigned char *)buf, *buflen, 0, &dnsrec); + if (status != ARES_SUCCESS) { + WLOG("Failed to parse DNS response: %s", ares_strerror((int)status)); + return; + } + const uint16_t tx_id = ares_dns_record_get_id(dnsrec); + + // NOTE: according to current c-ares implementation, removing first or last elements are the fastest! + + // remove every additional and authority record + while (ares_dns_record_rr_cnt(dnsrec, ARES_SECTION_ADDITIONAL) > 0) { + status = ares_dns_record_rr_del(dnsrec, ARES_SECTION_ADDITIONAL, 0); + if (status != ARES_SUCCESS) { + WLOG("%04hX: Could not remove additional record: %s", tx_id, ares_strerror((int)status)); + } + } + while (ares_dns_record_rr_cnt(dnsrec, ARES_SECTION_AUTHORITY) > 0) { + status = ares_dns_record_rr_del(dnsrec, ARES_SECTION_AUTHORITY, 0); + if (status != ARES_SUCCESS) { + WLOG("%04hX: Could not remove authority record: %s", tx_id, ares_strerror((int)status)); + } + } + + // rough estimate to reach size limit + size_t answers = ares_dns_record_rr_cnt(dnsrec, ARES_SECTION_ANSWER); + size_t answers_to_keep = (size_limit - DNS_HEADER_LENGTH) / (old_size / answers); + answers_to_keep = answers_to_keep > 0 ? answers_to_keep : 1; // try to keep 1 answer + + // remove answer records until fit size limit or running out of answers + unsigned char *new_resp = NULL; + size_t new_resp_len = 0; + for (uint8_t g = 0; g < UINT8_MAX; ++g) { // endless loop guard + status = ares_dns_write(dnsrec, &new_resp, &new_resp_len); + if (status != ARES_SUCCESS) { + WLOG("%04hX: Failed to create truncated DNS response: %s", tx_id, ares_strerror((int)status)); + new_resp = NULL; // just to be sure + break; + } + if (new_resp_len < size_limit || answers == 0) { + break; + } + if (new_resp_len >= old_size) { + WLOG("%04hX: Truncated DNS response size larger or equal to original: %u >= %u", + tx_id, new_resp_len, old_size); // impossible? + } + ares_free_string(new_resp); + new_resp = NULL; + + DLOG("%04hX: DNS response size truncated from %u to %u but to keep %u limit reducing answers from %u to %u", + tx_id, old_size, new_resp_len, size_limit, answers, answers_to_keep); + + while (answers > answers_to_keep) { + status = ares_dns_record_rr_del(dnsrec, ARES_SECTION_ANSWER, answers - 1); + if (status != ARES_SUCCESS) { + WLOG("%04hX: Could not remove answer record: %s", tx_id, ares_strerror((int)status)); + break; + } + --answers; + } + answers = ares_dns_record_rr_cnt(dnsrec, ARES_SECTION_ANSWER); // update to be sure! + answers_to_keep /= 2; + } + ares_dns_record_destroy(dnsrec); + + if (new_resp != NULL && new_resp_len < old_size) { + memcpy(buf, new_resp, new_resp_len); // NOLINT(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) + *buflen = new_resp_len; + buf[2] |= 0x02; // set truncation flag + ILOG("%04hX: DNS response size truncated from %u to %u to keep %u limit", + tx_id, old_size, new_resp_len, size_limit); + ares_free_string(new_resp); + } +} + +void dns_server_respond(dns_server_t *d, struct sockaddr *raddr, + const char *dns_req, const size_t dns_req_len, char *dns_resp, size_t dns_resp_len) { + if (dns_resp_len > DNS_SIZE_LIMIT) { + const uint16_t udp_size = get_edns_udp_size(dns_req, dns_req_len); + if (dns_resp_len > udp_size) { + truncate_dns_response(dns_resp, &dns_resp_len, udp_size); + } else { + uint16_t tx_id = ntohs(*((uint16_t*)dns_req)); + DLOG("%04hX: DNS response size %u larger than %d but EDNS0 UDP buffer size %u allows it", + tx_id, dns_resp_len, DNS_SIZE_LIMIT, udp_size); + } + } + + ssize_t len = sendto(d->sock, dns_resp, dns_resp_len, 0, raddr, d->addrlen); if(len == -1) { DLOG("sendto failed: %s", strerror(errno)); } diff --git a/src/dns_server.h b/src/dns_server.h index 9982285..0d87165 100644 --- a/src/dns_server.h +++ b/src/dns_server.h @@ -1,15 +1,23 @@ #ifndef _DNS_SERVER_H_ #define _DNS_SERVER_H_ +#include +#include +#include #include #include #include +enum { + DNS_HEADER_LENGTH = 12, // RFC1035 4.1.1 header size + DNS_SIZE_LIMIT = 512, + DNS_REQUEST_BUFFER_SIZE = 4096 // EDNS default before DNS Flag Day 2020 +}; + struct dns_server_s; -typedef void (*dns_req_received_cb)(struct dns_server_s *dns_server, void *data, - struct sockaddr* addr, uint16_t tx_id, - char *dns_req, size_t dns_req_len); +typedef void (*dns_req_received_cb)(void *dns_server, uint8_t is_tcp, void *data, + struct sockaddr* addr, char *dns_req, size_t dns_req_len); typedef struct dns_server_s { struct ev_loop *loop; @@ -21,12 +29,12 @@ typedef struct dns_server_s { } dns_server_t; void dns_server_init(dns_server_t *d, struct ev_loop *loop, - const char *listen_addr, int listen_port, + struct addrinfo *listen_addrinfo, dns_req_received_cb cb, void *data); // Sends a DNS response 'buf' of length 'blen' to 'raddr'. -void dns_server_respond(dns_server_t *d, struct sockaddr *raddr, char *buf, - size_t blen); +void dns_server_respond(dns_server_t *d, struct sockaddr *raddr, + const char *dns_req, const size_t dns_req_len, char *dns_resp, size_t dns_resp_len); void dns_server_stop(dns_server_t *d); diff --git a/src/dns_server_tcp.c b/src/dns_server_tcp.c new file mode 100644 index 0000000..97d03e4 --- /dev/null +++ b/src/dns_server_tcp.c @@ -0,0 +1,371 @@ +//NOLINTNEXTLINE(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp) +#define _GNU_SOURCE // needed for having accept4() + +#include // NOLINT(llvmlibc-restrict-system-libc-headers) +#include // NOLINT(llvmlibc-restrict-system-libc-headers) +#include // NOLINT(llvmlibc-restrict-system-libc-headers) + +#include "dns_server_tcp.h" +#include "logging.h" + +// the following macros require to have client pointer to tcp_client_s structure +// else: compilation failure will occur +#define LOG_CLIENT(level, format, args...) LOG(level, "C-%u: " format, client->id, ## args) +#define DLOG_CLIENT(format, args...) DLOG("C-%u: " format, client->id, ## args) +#define ILOG_CLIENT(format, args...) ILOG("C-%u: " format, client->id, ## args) +#define WLOG_CLIENT(format, args...) WLOG("C-%u: " format, client->id, ## args) +#define ELOG_CLIENT(format, args...) ELOG("C-%u: " format, client->id, ## args) +#define FLOG_CLIENT(format, args...) FLOG("C-%u: " format, client->id, ## args) + +enum { + LISTEN_BACKLOG = 5, + IDLE_TIMEOUT_S = 120, // "two minutes" according to RFC1035 4.2.2 + RESEND_DELAY_US = 500, // 0.0005 sec +}; + +struct tcp_client_s { + struct dns_server_tcp_s * d; + + uint64_t id; + int sock; + + struct sockaddr_storage raddr; + socklen_t addr_len; + + char * input_buffer; + uint32_t input_buffer_size; + uint32_t input_buffer_used; + + ev_io read_watcher; + ev_timer timer_watcher; + + struct tcp_client_s * next; +} __attribute__((packed)) __attribute__((aligned(128))); + +struct dns_server_tcp_s { + struct ev_loop *loop; + + dns_req_received_cb cb; + void *cb_data; + + int sock; + socklen_t addrlen; + ev_io accept_watcher; + + uint64_t client_id; + uint16_t client_count; + uint16_t client_limit; + struct tcp_client_s * clients; +} __attribute__((packed)) __attribute__((aligned(128))); + + +static void remove_client(struct tcp_client_s * client) { + dns_server_tcp_t *d = client->d; + + DLOG_CLIENT("Removing client, socket %d", client->sock); + + if (d->client_count == d->client_limit) { + ev_io_start(d->loop, &d->accept_watcher); // continue accepting new client connections + } + d->client_count--; + + ev_io_stop(d->loop, &client->read_watcher); + ev_timer_stop(d->loop, &client->timer_watcher); + + free(client->input_buffer); + + close(client->sock); + + if (d->clients == client) { + d->clients = client->next; + } + else { + for (struct tcp_client_s * cur = d->clients; cur != NULL; cur = cur->next) { + if (cur->next == client) { + cur->next = client->next; + break; + } + } + } + + free(client); +} + +static int get_dns_request(struct tcp_client_s *client, + char ** dns_req, uint16_t * req_size) { + // check if whole request is available + *req_size = ntohs(*((uint16_t*)client->input_buffer)); + uint16_t data_size = sizeof(uint16_t) + *req_size; + if (data_size > client->input_buffer_used) { + return 0; // Partial request + } + // copy whole request + *dns_req = (char *)malloc(*req_size); // To free buffer after https request is complete. + if (*dns_req == NULL) { + FLOG_CLIENT("Out of mem"); + } + memcpy(*dns_req, client->input_buffer + sizeof(uint16_t), *req_size); // NOLINT(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) + // move down data of next request(s) if any + client->input_buffer_used -= data_size; + memmove(client->input_buffer, client->input_buffer + data_size, client->input_buffer_used); // NOLINT(clang-diagnostic-format-nonliteral,clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) + return 1; +} + +static void read_cb(struct ev_loop __attribute__((unused)) *loop, + ev_io *w, int __attribute__((unused)) revents) { + struct tcp_client_s *client = (struct tcp_client_s *)w->data; + dns_server_tcp_t *d = client->d; + + // Receive data + char buf[DNS_REQUEST_BUFFER_SIZE]; // if there would be more data, callback will be called again + ssize_t len = recv(w->fd, buf, DNS_REQUEST_BUFFER_SIZE, 0); + if (len <= 0) { + if (len == 0 || errno == ECONNRESET) { + DLOG_CLIENT("Connection closed"); + } else if (errno == EAGAIN || errno == EWOULDBLOCK) { + return; + } else { + WLOG_CLIENT("Read error: %s", strerror(errno)); + } + remove_client(client); + return; + } + + // Append data into input buffer + const uint32_t free_space = client->input_buffer_size - client->input_buffer_used; + const uint32_t needed_space = client->input_buffer_used + (uint32_t)len; + DLOG_CLIENT("Received %d byte, free: %u", len, free_space); + if (free_space < len) { + for (client->input_buffer_size = 64; // lower value does not make much sense + client->input_buffer_size < needed_space; + client->input_buffer_size *= 2) { + if (client->input_buffer_size > 2*UINT16_MAX) { + FLOG_CLIENT("Unrealistic input buffer size: %u", client->input_buffer_size); + } + } + DLOG_CLIENT("Resize input buffer to %u", client->input_buffer_size); + client->input_buffer = (char *) realloc((void*) client->input_buffer, // NOLINT(bugprone-suspicious-realloc-usage) if realloc fails, program stops + client->input_buffer_size); + if (client->input_buffer == NULL) { + FLOG_CLIENT("Out of mem"); + } + } + memcpy(client->input_buffer + client->input_buffer_used, buf, (size_t)len); // NOLINT(clang-diagnostic-format-nonliteral,clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) + client->input_buffer_used = needed_space; + + // Split requests + char *dns_req = NULL; + uint16_t req_size = 0; + uint8_t request_received = 0; + while (get_dns_request(client, &dns_req, &req_size)) { + if (req_size < DNS_HEADER_LENGTH) { + WLOG_CLIENT("Malformed request received, too short: %u", req_size); + free(dns_req); + remove_client(client); + return; + } + + d->cb(d, 1, d->cb_data, (struct sockaddr*)&client->raddr, dns_req, req_size); + request_received = 1; + } + + if (request_received) { + ev_timer_again(d->loop, &client->timer_watcher); + } +} + +static void timer_cb(struct ev_loop __attribute__((unused)) *loop, + ev_timer *w, int __attribute__((unused)) revents) { + struct tcp_client_s *client = (struct tcp_client_s *)w->data; + DLOG_CLIENT("TCP client timeouted"); + remove_client(client); +} + +static void accept_cb(struct ev_loop __attribute__((unused)) *loop, + ev_io *w, int __attribute__((unused)) revents) { + dns_server_tcp_t *d = (dns_server_tcp_t *)w->data; + + struct sockaddr_storage client_addr; + socklen_t client_addr_len = sizeof(client_addr); + + int client_sock = accept4(w->fd, (struct sockaddr *)&client_addr, + &client_addr_len, SOCK_NONBLOCK); + if (client_sock == -1 && errno != EAGAIN && errno != EWOULDBLOCK) { + ELOG("Failed to accept TCP client: %s", strerror(errno)); + return; + } + + d->client_id++; + d->client_count++; + if (d->client_count == d->client_limit) { + ev_io_stop(d->loop, &d->accept_watcher); // suspend accepting new client connections + } + + struct tcp_client_s *client = (struct tcp_client_s *)calloc(1, sizeof(struct tcp_client_s)); + if (client == NULL) { + FLOG("Out of mem"); + } + client->d = d; + client->id = d->client_id; + client->sock = client_sock; + memcpy(&client->raddr, &client_addr, client_addr_len); // NOLINT(clang-diagnostic-format-nonliteral,clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) + client->addr_len = client_addr_len; + client->input_buffer = NULL; + client->next = d->clients; + d->clients = client; + + // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) + ev_io_init(&client->read_watcher, read_cb, client->sock, EV_READ); + client->read_watcher.data = client; + ev_io_start(d->loop, &client->read_watcher); + + ev_init(&client->timer_watcher, timer_cb); + client->timer_watcher.repeat = IDLE_TIMEOUT_S; + client->timer_watcher.data = client; + ev_timer_again(d->loop, &client->timer_watcher); + + DLOG_CLIENT("Accepted client %u of %u, socket %d", d->client_count, d->client_limit, client->sock); +} + +// Creates and bind a listening non-blocking TCP socket for incoming requests. +static int get_tcp_listen_sock(struct addrinfo *listen_addrinfo) { + int sock = socket(listen_addrinfo->ai_family, SOCK_STREAM, 0); + if (sock < 0) { + FLOG("Error creating TCP socket: %s (%d)", strerror(errno), errno); + } + + int yes = 1; + if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) == -1) { + ELOG("Reuse address failed: %s (%d)", strerror(errno), errno); + } + + uint16_t port = 0; + char ipstr[INET6_ADDRSTRLEN]; + if (listen_addrinfo->ai_family == AF_INET) { + port = ntohs(((struct sockaddr_in*) listen_addrinfo->ai_addr)->sin_port); + inet_ntop(AF_INET, &((struct sockaddr_in *)listen_addrinfo->ai_addr)->sin_addr, ipstr, sizeof(ipstr)); + } else if (listen_addrinfo->ai_family == AF_INET6) { + port = ntohs(((struct sockaddr_in6*) listen_addrinfo->ai_addr)->sin6_port); + inet_ntop(AF_INET6, &((struct sockaddr_in6 *)listen_addrinfo->ai_addr)->sin6_addr, ipstr, sizeof(ipstr)); + } else { + FLOG("Unknown address family: %d", listen_addrinfo->ai_family); + } + + int res = bind(sock, listen_addrinfo->ai_addr, listen_addrinfo->ai_addrlen); + if (res < 0) { + FLOG("Error binding on %s:%d TCP: %s (%d)", ipstr, port, + strerror(errno), errno); + } + + if (listen(sock, LISTEN_BACKLOG) == -1) { + FLOG("Error listaning on %s:%d TCP: %s (%d)", ipstr, port, + strerror(errno), errno); + } + + int flags = fcntl(sock, F_GETFL, 0); + if (flags == -1) { + FLOG("Error getting TCP socket flags: %s (%d)", ipstr, port, + strerror(errno), errno); + } + if (fcntl(sock, F_SETFL, flags | O_NONBLOCK) == -1) { + FLOG("Error setting TCP socket to non-blocking: %s (%d)", ipstr, port, + strerror(errno), errno); + } + + ILOG("Listening on %s:%d TCP", ipstr, port); + + return sock; +} + +dns_server_tcp_t * dns_server_tcp_create( + struct ev_loop *loop, struct addrinfo *listen_addrinfo, + dns_req_received_cb cb, void *data, uint16_t tcp_client_limit) { + dns_server_tcp_t * d = (dns_server_tcp_t *) malloc(sizeof(dns_server_tcp_t)); + if (d == NULL) { + FLOG("Out of mem"); + } + d->loop = loop; + d->cb = cb; + d->cb_data = data; + d->sock = get_tcp_listen_sock(listen_addrinfo); + d->addrlen = listen_addrinfo->ai_addrlen; + d->client_id = 0; + d->client_count = 0; + d->client_limit = tcp_client_limit; + d->clients = NULL; + + // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) + ev_io_init(&d->accept_watcher, accept_cb, d->sock, EV_READ); + d->accept_watcher.data = d; + ev_io_start(d->loop, &d->accept_watcher); + + return d; +} + +void dns_server_tcp_respond(dns_server_tcp_t *d, + struct sockaddr *raddr, char *resp, size_t resp_len) +{ + if (resp_len < DNS_HEADER_LENGTH || resp_len > UINT16_MAX) { + WLOG("Malformed response received, invalid length: %u", resp_len); + return; + } + + // find client data + struct tcp_client_s *client = NULL; + for (struct tcp_client_s * cur = d->clients; cur != NULL; cur = cur->next) { + if (memcmp(raddr, &(cur->raddr), cur->addr_len) == 0) { + client = cur; + break; + } + } + if (client == NULL) { + uint16_t response_id = ntohs(*((uint16_t*)resp)); + WLOG("Could not find client, can not send DNS response: %04hX", response_id); + return; + } + + DLOG_CLIENT("Sending %u bytes", resp_len); + + // send length of response + uint16_t resp_size = htons((uint16_t)resp_len); + ssize_t len = send(client->sock, &resp_size, sizeof(uint16_t), MSG_MORE | MSG_NOSIGNAL); + if (len != sizeof(uint16_t)) { + WLOG_CLIENT("Send error: %s, len: %d", strerror(errno), len); + remove_client(client); + return; + } + + // send the response + ssize_t sent = 0; + for (uint8_t i = 0; i < UINT8_MAX; ++i) // endless loop guard + { + len = send(client->sock, resp + sent, resp_len - (size_t)sent, MSG_NOSIGNAL); + if (len < 0) { + if (errno != EAGAIN && errno != EWOULDBLOCK) { + WLOG_CLIENT("Send error: %s", strerror(errno)); + remove_client(client); + return; + } + } + sent += len; + + if (sent == (ssize_t)resp_len) { + break; + } + + usleep(RESEND_DELAY_US); + } + + ev_timer_again(d->loop, &client->timer_watcher); +} + +void dns_server_tcp_stop(dns_server_tcp_t *d) { + while (d->clients) { + remove_client(d->clients); //NOLINT(clang-analyzer-unix.Malloc) false use after free detection + } + ev_io_stop(d->loop, &d->accept_watcher); +} + +void dns_server_tcp_cleanup(dns_server_tcp_t *d) { + close(d->sock); +} diff --git a/src/dns_server_tcp.h b/src/dns_server_tcp.h new file mode 100755 index 0000000..3fb32a9 --- /dev/null +++ b/src/dns_server_tcp.h @@ -0,0 +1,19 @@ +#ifndef _DNS_SERVER_TCP_H_ +#define _DNS_SERVER_TCP_H_ + +#include "dns_server.h" + +typedef struct dns_server_tcp_s dns_server_tcp_t; + +dns_server_tcp_t * dns_server_tcp_create( + struct ev_loop *loop, struct addrinfo *listen_addrinfo, + dns_req_received_cb cb, void *data, uint16_t tcp_client_limit); + +void dns_server_tcp_respond(dns_server_tcp_t *d, + struct sockaddr *raddr, char *resp, size_t resp_len); + +void dns_server_tcp_stop(dns_server_tcp_t *d); + +void dns_server_tcp_cleanup(dns_server_tcp_t *d); + +#endif // _DNS_SERVER_H_ diff --git a/src/https_client.c b/src/https_client.c index 7b21227..b26ac0f 100644 --- a/src/https_client.c +++ b/src/https_client.c @@ -131,6 +131,11 @@ static int closesocket_callback(void __attribute__((unused)) *clientp, curl_sock DLOG("curl closed socket: %d", sock); client->connections--; + if (client->connections <= 0 && ev_is_active(&client->reset_timer)) { + ILOG("Client reset timer cancelled, since all connection closed"); + ev_timer_stop(client->loop, &client->reset_timer); + } + if (client->stat) { stat_connection_closed(client->stat); } @@ -138,7 +143,7 @@ static int closesocket_callback(void __attribute__((unused)) *clientp, curl_sock return 0; } -static void https_log_data(enum LogSeverity level, struct https_fetch_ctx *ctx, +static void https_log_data(int level, struct https_fetch_ctx *ctx, const char * prefix, char *ptr, size_t size) { const size_t width = 0x10; @@ -156,14 +161,14 @@ static void https_log_data(enum LogSeverity level, struct https_fetch_ctx *ctx, for (size_t c = 0; c < width; c++) { if (i+c < size) { // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) - hex_off += snprintf(hex + hex_off, sizeof(hex) - hex_off, - "%02x ", (unsigned char)ptr[i+c]); + hex_off += (size_t)snprintf(hex + hex_off, sizeof(hex) - hex_off, + "%02x ", (unsigned char)ptr[i+c]); // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) - str_off += snprintf(str + str_off, sizeof(str) - str_off, - "%c", isprint(ptr[i+c]) ? ptr[i+c] : '.'); + str_off += (size_t)snprintf(str + str_off, sizeof(str) - str_off, + "%c", isprint(ptr[i+c]) ? ptr[i+c] : '.'); } else { // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) - hex_off += snprintf(hex + hex_off, sizeof(hex) - hex_off, " "); + hex_off += (size_t)snprintf(hex + hex_off, sizeof(hex) - hex_off, " "); } } @@ -249,7 +254,7 @@ static void https_set_request_version(https_client_t *client, switch (client->opt->use_http_version) { case 1: http_version_int = CURL_HTTP_VERSION_1_1; - // fallthrough + __attribute__((fallthrough)); case 2: break; case 3: @@ -310,7 +315,7 @@ static void https_fetch_ctx_init(https_client_t *client, ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_WRITEDATA, ctx); ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_MAXAGE_CONN, client->opt->max_idle_time); ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_PIPEWAIT, client->opt->use_http_version > 1); - ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_USERAGENT, "https_dns_proxy/0.3"); + ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_USERAGENT, "https_dns_proxy/0.4"); ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_FOLLOWLOCATION, 0); ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_NOSIGNAL, 0); ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_TIMEOUT, client->connections > 0 ? 5 : 10 /* seconds */); @@ -338,7 +343,7 @@ static void https_fetch_ctx_init(https_client_t *client, static int https_fetch_ctx_process_response(https_client_t *client, struct https_fetch_ctx *ctx, - int curl_result_code) + CURLcode curl_result_code) { CURLcode res = 0; long long_resp = 0; @@ -353,6 +358,12 @@ static int https_fetch_ctx_process_response(https_client_t *client, case CURLE_WRITE_ERROR: WLOG_REQ("curl request failed with write error (probably response content was too large)"); break; + case CURLE_OPERATION_TIMEDOUT: + if (!ev_is_active(&client->reset_timer)) { + ILOG_REQ("Client reset timer started"); + ev_timer_start(client->loop, &client->reset_timer); + } + __attribute__((fallthrough)); default: WLOG_REQ("curl request failed with %d: %s", curl_result_code, curl_easy_strerror(curl_result_code)); if (ctx->curl_errbuf[0] != 0) { @@ -419,15 +430,15 @@ static int https_fetch_ctx_process_response(https_client_t *client, res = curl_easy_getinfo(ctx->curl, CURLINFO_SSL_VERIFYRESULT, &long_resp); if (res != CURLE_OK) { ELOG_REQ("CURLINFO_SSL_VERIFYRESULT: %s", curl_easy_strerror(res)); - } else if (long_resp != CURLE_OK) { - WLOG_REQ("CURLINFO_SSL_VERIFYRESULT: %s", curl_easy_strerror(long_resp)); + } else if (long_resp != 0) { + WLOG_REQ("CURLINFO_SSL_VERIFYRESULT: certificate verification failure %d", long_resp); } res = curl_easy_getinfo(ctx->curl, CURLINFO_OS_ERRNO, &long_resp); if (res != CURLE_OK) { ELOG_REQ("CURLINFO_OS_ERRNO: %s", curl_easy_strerror(res)); } else if (long_resp != 0) { - WLOG_REQ("CURLINFO_OS_ERRNO: %d %s", long_resp, strerror(long_resp)); + WLOG_REQ("CURLINFO_OS_ERRNO: %d %s", long_resp, strerror((int)long_resp)); if (long_resp == ENETUNREACH && !client->opt->ipv4) { // this can't be fixed here with option overwrite because of dns_poller WLOG("Try to run application with -4 argument!"); @@ -465,7 +476,7 @@ static int https_fetch_ctx_process_response(https_client_t *client, res = curl_easy_getinfo(ctx->curl, CURLINFO_SCHEME, &str_resp); if (res != CURLE_OK) { ELOG_REQ("CURLINFO_SCHEME: %s", curl_easy_strerror(res)); - } else if (strcasecmp(str_resp, "https") != 0) { + } else if (str_resp != NULL && strcasecmp(str_resp, "https") != 0) { DLOG_REQ("CURLINFO_SCHEME: %s", str_resp); } @@ -510,7 +521,7 @@ static void https_fetch_ctx_cleanup(https_client_t *client, if (curl_result_code < 0) { WLOG_REQ("Request was aborted"); drop_reply = 1; - } else if (https_fetch_ctx_process_response(client, ctx, curl_result_code) != 0) { + } else if (https_fetch_ctx_process_response(client, ctx, (CURLcode)curl_result_code) != 0) { ILOG_REQ("Response was faulty, skipping DNS reply"); drop_reply = 1; } @@ -540,7 +551,7 @@ static void check_multi_info(https_client_t *c) { struct https_fetch_ctx *cur = c->fetches; while (cur) { if (cur->curl == msg->easy_handle) { - https_fetch_ctx_cleanup(c, prev, cur, msg->data.result); + https_fetch_ctx_cleanup(c, prev, cur, (int)msg->data.result); break; } prev = cur; @@ -642,21 +653,37 @@ static int multi_timer_cb(CURLM __attribute__((unused)) *multi, ev_timer_stop(c->loop, &c->timer); if (timeout_ms >= 0) { // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) - ev_timer_init(&c->timer, timer_cb, timeout_ms / 1000.0, 0); + ev_timer_init(&c->timer, timer_cb, (double)timeout_ms / 1000.0, 0); ev_timer_start(c->loop, &c->timer); } return 0; } +static void https_client_multi_init(https_client_t *c, struct curl_slist *header_list) { + c->curlm = curl_multi_init(); // if fails, first setopt will fail + c->header_list = header_list; + + ASSERT_CURL_MULTI_SETOPT(c->curlm, CURLMOPT_PIPELINING, CURLPIPE_HTTP1 | CURLPIPE_MULTIPLEX); + ASSERT_CURL_MULTI_SETOPT(c->curlm, CURLMOPT_MAX_TOTAL_CONNECTIONS, HTTPS_CONNECTION_LIMIT); + ASSERT_CURL_MULTI_SETOPT(c->curlm, CURLMOPT_MAX_HOST_CONNECTIONS, HTTPS_CONNECTION_LIMIT); + ASSERT_CURL_MULTI_SETOPT(c->curlm, CURLMOPT_SOCKETDATA, c); + ASSERT_CURL_MULTI_SETOPT(c->curlm, CURLMOPT_SOCKETFUNCTION, multi_sock_cb); + ASSERT_CURL_MULTI_SETOPT(c->curlm, CURLMOPT_TIMERDATA, c); + ASSERT_CURL_MULTI_SETOPT(c->curlm, CURLMOPT_TIMERFUNCTION, multi_timer_cb); +} + +static void reset_timer_cb(struct ev_loop __attribute__((unused)) *loop, + ev_timer *w, int __attribute__((unused)) revents) { + GET_PTR(https_client_t, c, w->data); + ILOG("Client reset timer timeouted"); + https_client_reset(c); +} + void https_client_init(https_client_t *c, options_t *opt, stat_t *stat, struct ev_loop *loop) { // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) memset(c, 0, sizeof(*c)); c->loop = loop; - c->curlm = curl_multi_init(); // if fails, first setopt will fail - c->header_list = curl_slist_append(curl_slist_append(NULL, - "Accept: " DOH_CONTENT_TYPE), - "Content-Type: " DOH_CONTENT_TYPE); c->fetches = NULL; c->timer.data = c; for (int i = 0; i < HTTPS_SOCKET_LIMIT; i++) { @@ -665,13 +692,13 @@ void https_client_init(https_client_t *c, options_t *opt, c->opt = opt; c->stat = stat; - ASSERT_CURL_MULTI_SETOPT(c->curlm, CURLMOPT_PIPELINING, CURLPIPE_HTTP1 | CURLPIPE_MULTIPLEX); - ASSERT_CURL_MULTI_SETOPT(c->curlm, CURLMOPT_MAX_TOTAL_CONNECTIONS, HTTPS_CONNECTION_LIMIT); - ASSERT_CURL_MULTI_SETOPT(c->curlm, CURLMOPT_MAX_HOST_CONNECTIONS, HTTPS_CONNECTION_LIMIT); - ASSERT_CURL_MULTI_SETOPT(c->curlm, CURLMOPT_SOCKETDATA, c); - ASSERT_CURL_MULTI_SETOPT(c->curlm, CURLMOPT_SOCKETFUNCTION, multi_sock_cb); - ASSERT_CURL_MULTI_SETOPT(c->curlm, CURLMOPT_TIMERDATA, c); - ASSERT_CURL_MULTI_SETOPT(c->curlm, CURLMOPT_TIMERFUNCTION, multi_timer_cb); + ev_timer_init(&c->reset_timer, reset_timer_cb, (double)opt->conn_loss_time, 0); + c->reset_timer.data = c; + + struct curl_slist *header_list = curl_slist_append(curl_slist_append(NULL, + "Accept: " DOH_CONTENT_TYPE), + "Content-Type: " DOH_CONTENT_TYPE); + https_client_multi_init(c, header_list); } void https_client_fetch(https_client_t *c, const char *url, @@ -687,11 +714,10 @@ void https_client_fetch(https_client_t *c, const char *url, } void https_client_reset(https_client_t *c) { - options_t *opt = c->opt; - stat_t *stat = c->stat; - struct ev_loop *loop = c->loop; + struct curl_slist *header_list = c->header_list; + c->header_list = NULL; https_client_cleanup(c); - https_client_init(c, opt, stat, loop); + https_client_multi_init(c, header_list); } void https_client_cleanup(https_client_t *c) { @@ -700,4 +726,5 @@ void https_client_cleanup(https_client_t *c) { } curl_slist_free_all(c->header_list); curl_multi_cleanup(c->curlm); + ev_timer_stop(c->loop, &c->reset_timer); } diff --git a/src/https_client.h b/src/https_client.h index 7c5ea08..de60bbb 100644 --- a/src/https_client.h +++ b/src/https_client.h @@ -43,6 +43,8 @@ typedef struct { options_t *opt; stat_t *stat; + + ev_timer reset_timer; } https_client_t; void https_client_init(https_client_t *c, options_t *opt, diff --git a/src/logging.c b/src/logging.c index a85a45f..944c96b 100644 --- a/src/logging.c +++ b/src/logging.c @@ -121,18 +121,18 @@ void _log(const char *file, int line, int severity, const char *fmt, ...) { if (chars < 0 || chars >= LOG_LINE_SIZE/2) { abort(); // must be impossible } - buff_pos += chars; + buff_pos += (uint32_t)chars; va_list args; va_start(args, fmt); - // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) + // NOLINTNEXTLINE(clang-diagnostic-format-nonliteral,clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) chars = vsnprintf(buff + buff_pos, LOG_LINE_SIZE - buff_pos, fmt, args); va_end(args); if (chars < 0) { abort(); // must be impossible } - buff_pos += chars; + buff_pos += (uint32_t)chars; if (buff_pos >= LOG_LINE_SIZE) { buff_pos = LOG_LINE_SIZE - 1; buff[buff_pos - 1] = '$'; // indicate truncation diff --git a/src/main.c b/src/main.c index 2e9f5f2..c81d5a5 100644 --- a/src/main.c +++ b/src/main.c @@ -9,8 +9,13 @@ #include // NOLINT(llvmlibc-restrict-system-libc-headers) #include // NOLINT(llvmlibc-restrict-system-libc-headers) +#if HAS_LIBSYSTEMD == 1 +#include // NOLINT(llvmlibc-restrict-system-libc-headers) +#endif + #include "dns_poller.h" #include "dns_server.h" +#include "dns_server_tcp.h" #include "https_client.h" #include "logging.h" #include "options.h" @@ -24,12 +29,15 @@ typedef struct { const char *resolver_url; stat_t *stat; uint8_t using_dns_poller; + socklen_t addrlen; } app_state_t; // NOLINTNEXTLINE(altera-struct-pack-align) typedef struct { - dns_server_t *dns_server; + void *dns_server; + uint8_t is_tcp; char* dns_req; + size_t dns_req_len; stat_t *stat; ev_tstamp start_tstamp; uint16_t tx_id; @@ -84,31 +92,37 @@ static void https_resp_cb(void *data, char *buf, size_t buflen) { if (req == NULL) { FLOG("%04hX: data NULL", req->tx_id); } - free((void*)req->dns_req); if (buf != NULL) { // May be NULL for timeout, DNS failure, or something similar. - if (buflen < (int)sizeof(uint16_t)) { - WLOG("%04hX: Malformed response received (too short)", req->tx_id); + if (buflen < DNS_HEADER_LENGTH) { + WLOG("%04hX: Malformed response received, too short: %u", req->tx_id, buflen); } else { - uint16_t response_id = ntohs(*((uint16_t*)buf)); + const uint16_t response_id = ntohs(*((uint16_t*)buf)); if (req->tx_id != response_id) { WLOG("DNS request and response IDs are not matching: %hX != %hX", req->tx_id, response_id); } else { - dns_server_respond(req->dns_server, (struct sockaddr*)&req->raddr, buf, buflen); + if (req->is_tcp) { + dns_server_tcp_respond((dns_server_tcp_t *)req->dns_server, (struct sockaddr*)&req->raddr, buf, buflen); + } else { + dns_server_respond((dns_server_t *)req->dns_server, (struct sockaddr*)&req->raddr, + req->dns_req, req->dns_req_len, buf, buflen); + } if (req->stat) { - stat_request_end(req->stat, buflen, ev_now(req->dns_server->loop) - req->start_tstamp); + stat_request_end(req->stat, buflen, ev_now(req->stat->loop) - req->start_tstamp, req->is_tcp); } } } } + free((void*)req->dns_req); free(req); } -static void dns_server_cb(dns_server_t *dns_server, void *data, - struct sockaddr* addr, uint16_t tx_id, +static void dns_server_cb(void *dns_server, uint8_t is_tcp, void *data, + struct sockaddr* tmp_remote_addr, char *dns_req, size_t dns_req_len) { app_state_t *app = (app_state_t *)data; + uint16_t tx_id = ntohs(*((uint16_t*)dns_req)); DLOG("Received request for id: %hX, len: %d", tx_id, dns_req_len); // If we're not yet bootstrapped, don't answer. libcurl will fall back to @@ -125,17 +139,40 @@ static void dns_server_cb(dns_server_t *dns_server, void *data, FLOG("%04hX: Out of mem", tx_id); } req->tx_id = tx_id; - memcpy(&req->raddr, addr, dns_server->addrlen); // NOLINT(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) + memcpy(&req->raddr, tmp_remote_addr, app->addrlen); // NOLINT(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) req->dns_server = dns_server; - req->dns_req = dns_req; // To free buffer after https request is complete. - req->start_tstamp = ev_now(dns_server->loop); + req->is_tcp = is_tcp; + req->dns_req = dns_req; // To free buffer after https request is complete. + req->dns_req_len = dns_req_len; req->stat = app->stat; if (req->stat) { - stat_request_begin(app->stat, dns_req_len); + req->start_tstamp = ev_now(app->stat->loop); + stat_request_begin(app->stat, dns_req_len, is_tcp); } https_client_fetch(app->https_client, app->resolver_url, - dns_req, dns_req_len, app->resolv, req->tx_id, https_resp_cb, req); + req->dns_req, dns_req_len, app->resolv, req->tx_id, https_resp_cb, req); +} + +static void systemd_notify_ready(void) { +#if HAS_LIBSYSTEMD == 1 + static uint8_t called_once = 0; + if (called_once != 0) { + DLOG("Systemd notify already called once!"); + return; + } + called_once = 1; + const int result = sd_notify(0, "READY=1"); + if (result > 0) { + DLOG("Systemd notify succeeded, service is ready!"); + } else if (result == 0) { + WLOG("Systemd notify called, but NOTIFY_SOCKET not set. Running manually?"); + } else { + ELOG("Systemd notify failed with: %s", strerror(result)); + } +#else + DLOG("Systemd notify skipped, not compiled with libsystemd!"); +#endif } static int addr_list_reduced(const char* full_list, const char* list) { @@ -144,7 +181,7 @@ static int addr_list_reduced(const char* full_list, const char* list) { while (pos < end) { char current[50]; const char *comma = strchr(pos, ','); - size_t ip_len = comma ? comma - pos : end - pos; + size_t ip_len = (size_t)(comma ? comma - pos : end - pos); strncpy(current, pos, ip_len); // NOLINT(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) current[ip_len] = '\0'; @@ -168,7 +205,13 @@ static void dns_poll_cb(const char* hostname, void *data, memset(buf, 0, sizeof(buf)); // NOLINT(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) if (strlen(hostname) > 254) { FLOG("Hostname too long."); } int ip_start = snprintf(buf, sizeof(buf) - 1, "%s:443:", hostname); // NOLINT(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) - (void)snprintf(buf + ip_start, sizeof(buf) - 1 - ip_start, "%s", addr_list); // NOLINT(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) + if (ip_start < 0) { + abort(); // must be impossible + } + (void)snprintf(buf + ip_start, sizeof(buf) - 1 - (uint32_t)ip_start, "%s", addr_list); // NOLINT(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) + if (app->resolv == NULL) { + systemd_notify_ready(); + } if (app->resolv && app->resolv->data) { char * old_addr_list = strstr(app->resolv->data, ":443:"); if (old_addr_list) { @@ -205,11 +248,28 @@ static int proxy_supports_name_resolution(const char *proxy) return 0; } +static struct addrinfo * get_listen_address(const char *listen_addr) { + struct addrinfo *ai = NULL; + struct addrinfo hints; + // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) + memset(&hints, 0, sizeof(struct addrinfo)); + /* prevent DNS lookups if leakage is our worry */ + hints.ai_flags = AI_NUMERICHOST; + + int res = getaddrinfo(listen_addr, NULL, &hints, &ai); + if (res != 0) { + FLOG("Error parsing listen address %s, getaddrinfo error: %s", + listen_addr, gai_strerror(res)); + } + + return ai; +} + static const char * sw_version(void) { #ifdef SW_VERSION return SW_VERSION; #else - return "2025.5.10-atLeast"; // update date sometimes, like 1-2 times a year + return "2025.8.26-atLeast"; // update date sometimes, like 1-2 times a year #endif } @@ -243,7 +303,7 @@ int main(int argc, char *argv[]) { } case OPR_PARSING_ERROR: printf("Failed to parse options!\n"); - // fallthrough + __attribute__((fallthrough)); case OPR_OPTION_ERROR: printf("\n"); options_show_usage(argc, argv); @@ -252,7 +312,7 @@ int main(int argc, char *argv[]) { abort(); // must not happen } - logging_init(opt.logfd, opt.loglevel, opt.flight_recorder_size); + logging_init(opt.logfd, opt.loglevel, (uint32_t)opt.flight_recorder_size); ILOG("Version: %s", sw_version()); ILOG("Built: " __DATE__ " " __TIME__); @@ -293,16 +353,32 @@ int main(int argc, char *argv[]) { https_client_t https_client; https_client_init(&https_client, &opt, (opt.stats_interval ? &stat : NULL), loop); + struct addrinfo *listen_addrinfo = get_listen_address(opt.listen_addr); + + if (listen_addrinfo->ai_family == AF_INET) { + ((struct sockaddr_in*) listen_addrinfo->ai_addr)->sin_port = htons((uint16_t)opt.listen_port); + } else if (listen_addrinfo->ai_family == AF_INET6) { + ((struct sockaddr_in6*) listen_addrinfo->ai_addr)->sin6_port = htons((uint16_t)opt.listen_port); + } + app_state_t app; app.https_client = &https_client; app.resolv = NULL; app.resolver_url = opt.resolver_url; app.using_dns_poller = 0; app.stat = (opt.stats_interval ? &stat : NULL); + app.addrlen = listen_addrinfo->ai_addrlen; dns_server_t dns_server; - dns_server_init(&dns_server, loop, opt.listen_addr, opt.listen_port, - dns_server_cb, &app); + dns_server_init(&dns_server, loop, listen_addrinfo, dns_server_cb, &app); + + dns_server_tcp_t * dns_server_tcp = NULL; + if (opt.tcp_client_limit > 0) { + dns_server_tcp = dns_server_tcp_create(loop, listen_addrinfo, dns_server_cb, &app, (uint16_t)opt.tcp_client_limit); + } + + freeaddrinfo(listen_addrinfo); + listen_addrinfo = NULL; if (opt.gid != (uid_t)-1 && setgroups(1, &opt.gid)) { FLOG("Failed to set groups"); @@ -351,6 +427,8 @@ int main(int argc, char *argv[]) { } else { ILOG("Resolver prefix '%s' doesn't appear to contain a " "hostname. DNS polling disabled.", opt.resolver_url); + + systemd_notify_ready(); } } @@ -367,6 +445,9 @@ int main(int argc, char *argv[]) { ev_signal_stop(loop, &sigint); ev_signal_stop(loop, &sigpipe); dns_server_stop(&dns_server); + if (dns_server_tcp != NULL) { + dns_server_tcp_stop(dns_server_tcp); + } stat_stop(&stat); DLOG("re-entering loop"); @@ -374,6 +455,11 @@ int main(int argc, char *argv[]) { DLOG("loop finished all events"); dns_server_cleanup(&dns_server); + if (dns_server_tcp != NULL) { + dns_server_tcp_cleanup(dns_server_tcp); + free(dns_server_tcp); + dns_server_tcp = NULL; + } https_client_cleanup(&https_client); stat_cleanup(&stat); diff --git a/src/options.c b/src/options.c index e432e93..51f8193 100644 --- a/src/options.c +++ b/src/options.c @@ -16,12 +16,14 @@ #endif enum { -DEFAULT_HTTP_VERSION = 2 +DEFAULT_HTTP_VERSION = 2, +MAX_TCP_CLIENTS = 200 }; void options_init(struct Options *opt) { opt->listen_addr = "127.0.0.1"; opt->listen_port = 5053; + opt->tcp_client_limit = 20; opt->logfile = "-"; opt->logfd = STDOUT_FILENO; opt->loglevel = LOG_ERROR; @@ -39,6 +41,7 @@ void options_init(struct Options *opt) { opt->curl_proxy = NULL; opt->use_http_version = DEFAULT_HTTP_VERSION; opt->max_idle_time = 118; + opt->conn_loss_time = 15; opt->stats_interval = 0; opt->ca_info = NULL; opt->flight_recorder_size = 0; @@ -46,7 +49,7 @@ void options_init(struct Options *opt) { enum OptionsParseResult options_parse_args(struct Options *opt, int argc, char **argv) { int c = 0; - while ((c = getopt(argc, argv, "a:c:p:du:g:b:i:4r:e:t:l:vxqm:s:C:F:hV")) != -1) { + while ((c = getopt(argc, argv, "a:c:p:T:du:g:b:i:4r:e:t:l:vxqm:L:s:C:F:hV")) != -1) { switch (c) { case 'a': // listen_addr opt->listen_addr = optarg; @@ -57,6 +60,9 @@ enum OptionsParseResult options_parse_args(struct Options *opt, int argc, char * case 'p': // listen_port opt->listen_port = atoi(optarg); break; + case 'T': // tcp_client_limit + opt->tcp_client_limit = atoi(optarg); + break; case 'd': // daemonize opt->daemonize = 1; break; @@ -102,6 +108,9 @@ enum OptionsParseResult options_parse_args(struct Options *opt, int argc, char * case 'm': opt->max_idle_time = atoi(optarg); break; + case 'L': + opt->conn_loss_time = atoi(optarg); + break; case 's': // stats interval opt->stats_interval = atoi(optarg); break; @@ -174,6 +183,11 @@ enum OptionsParseResult options_parse_args(struct Options *opt, int argc, char * printf("Maximum idle time must be between 0 and 3600.\n"); return OPR_OPTION_ERROR; } + if (opt->conn_loss_time < 5 || + opt->conn_loss_time > 60) { + printf("Connection loss time must be between 5 and 60.\n"); + return OPR_OPTION_ERROR; + } if (opt->stats_interval < 0 || opt->stats_interval > 3600) { printf("Statistic interval must be between 0 and 3600.\n"); return OPR_OPTION_ERROR; @@ -183,13 +197,21 @@ enum OptionsParseResult options_parse_args(struct Options *opt, int argc, char * printf("Flight recorder limit must be between 100 and 100000.\n"); return OPR_OPTION_ERROR; } + if (opt->listen_port < 0 || opt->listen_port > UINT16_MAX) { + printf("Listen port must be between 0 and %u.\n", UINT16_MAX); + return OPR_OPTION_ERROR; + } + if (opt->tcp_client_limit < 0 || opt->tcp_client_limit > MAX_TCP_CLIENTS) { + printf("TCP client limit must be between 0 and %u.\n", MAX_TCP_CLIENTS); + return OPR_OPTION_ERROR; + } return OPR_SUCCESS; } void options_show_usage(int __attribute__((unused)) argc, char **argv) { struct Options defaults; options_init(&defaults); - printf("Usage: %s [-a ] [-p ]\n", argv[0]); + printf("Usage: %s [-a ] [-p ] [-T ]\n", argv[0]); printf(" [-b ] [-i ] [-4]\n"); printf(" [-r ] [-t ] [-x] [-q] [-C ] [-c ]\n"); printf(" [-d] [-u ] [-g ] \n"); @@ -199,6 +221,8 @@ void options_show_usage(int __attribute__((unused)) argc, char **argv) { defaults.listen_addr); printf(" -p listen_port Local port to bind to. (Default: %d)\n", defaults.listen_port); + printf(" -T tcp_client_limit Number of TCP clients to serve. (Default: %d, Disabled: 0, Min: 1, Max: %d)\n", + defaults.tcp_client_limit, MAX_TCP_CLIENTS); printf("\n DNS client\n"); printf(" -b dns_servers Comma-separated IPv4/v6 addresses and ports (addr:port)\n"); printf(" of DNS servers to resolve resolver host (e.g. dns.google).\n"\ @@ -223,6 +247,10 @@ void options_show_usage(int __attribute__((unused)) argc, char **argv) { printf(" -m max_idle_time Maximum idle time in seconds allowed for reusing a HTTPS connection.\n"\ " (Default: %d, Min: 0, Max: 3600)\n", defaults.max_idle_time); + printf(" -L conn_loss_time Time in seconds to tolerate connection timeouts of reused connections.\n"\ + " This option mitigates half-open TCP connection issue (e.g. WAN IP change).\n"\ + " (Default: %d, Min: 5, Max: 60)\n", + defaults.conn_loss_time); printf(" -C ca_path Optional file containing CA certificates.\n"); printf(" -c dscp_codepoint Optional DSCP codepoint to set on upstream HTTPS server\n"); printf(" connections. (Min: 0, Max: 63)\n"); diff --git a/src/options.h b/src/options.h index b303ac0..9320b50 100644 --- a/src/options.h +++ b/src/options.h @@ -6,7 +6,9 @@ struct Options { const char *listen_addr; - uint16_t listen_port; + int listen_port; + + int tcp_client_limit; // Logfile. const char *logfile; @@ -48,6 +50,8 @@ struct Options { int max_idle_time; + int conn_loss_time; + // Print statistic interval int stats_interval; @@ -55,7 +59,7 @@ struct Options { const char *ca_info; // Number of logs to be kept by flight recorder - uint32_t flight_recorder_size; + int flight_recorder_size; } __attribute__((aligned(128))); typedef struct Options options_t; diff --git a/src/stat.c b/src/stat.c index af3c921..bddf758 100644 --- a/src/stat.c +++ b/src/stat.c @@ -7,17 +7,26 @@ static void reset_counters(stat_t *s) { s->requests = 0; s->responses = 0; s->query_times_sum = 0; + s->connections_opened = 0; s->connections_closed = 0; s->connections_reused = 0; + + s->tcp_requests_size = 0; + s->tcp_responses_size = 0; + s->tcp_requests = 0; + s->tcp_responses = 0; + s->tcp_query_times_sum = 0; } static void stat_print(stat_t *s) { - SLOG("%llu %llu %llu %zu %zu %llu %llu %llu", + SLOG("%llu %llu %llu %zu %zu %llu %llu %llu %llu %llu %llu %zu %zu", s->requests, s->responses, s->query_times_sum, s->requests_size, s->responses_size, s->connections_opened, s->connections_closed, - s->connections_reused); + s->connections_reused, + s->tcp_requests, s->tcp_responses, s->tcp_query_times_sum, + s->tcp_requests_size, s->tcp_responses_size); reset_counters(s); } @@ -40,23 +49,36 @@ void stat_init(stat_t *s, struct ev_loop *loop, int stats_interval) { ev_timer_start(loop, &s->stats_timer); SLOG("RequestsCount ResponsesCount LatencyMilisecondsSummary " "RequestsSize ResponsesSize ConnectionsOpened ConnectionsClosed " - "ConnectionsReused"); + "ConnectionsReused TcpRequestsCount TcpResponsesCount " + "TcpLatencyMilisecondsSummary TcpRequestsSize TcpResponsesSize"); } } -void stat_request_begin(stat_t *s, size_t req_len) +void stat_request_begin(stat_t *s, size_t req_len, uint8_t is_tcp) { + if (is_tcp) { + s->tcp_requests_size += req_len; + s->tcp_requests++; + } else { s->requests_size += req_len; s->requests++; + } } -void stat_request_end(stat_t *s, size_t resp_len, ev_tstamp latency) +void stat_request_end(stat_t *s, size_t resp_len, ev_tstamp latency, uint8_t is_tcp) { if (resp_len) { - s->responses_size += resp_len; - s->responses++; - // NOLINTNEXTLINE(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) - s->query_times_sum += (latency * 1000); + if (is_tcp) { + s->tcp_responses_size += resp_len; + s->tcp_responses++; + // NOLINTNEXTLINE(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) + s->tcp_query_times_sum += (uint64_t)(latency * 1000); + } else { + s->responses_size += resp_len; + s->responses++; + // NOLINTNEXTLINE(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) + s->query_times_sum += (uint64_t)(latency * 1000); + } } } diff --git a/src/stat.h b/src/stat.h index f96674c..9c1f4bc 100644 --- a/src/stat.h +++ b/src/stat.h @@ -19,22 +19,30 @@ typedef struct { struct ev_loop *loop; int stats_interval; + ev_timer stats_timer; + size_t requests_size; size_t responses_size; uint64_t requests; uint64_t responses; uint64_t query_times_sum; + uint64_t connections_opened; uint64_t connections_closed; uint64_t connections_reused; - ev_timer stats_timer; + + size_t tcp_requests_size; + size_t tcp_responses_size; + uint64_t tcp_requests; + uint64_t tcp_responses; + uint64_t tcp_query_times_sum; } stat_t; void stat_init(stat_t *s, struct ev_loop *loop, int stats_interval); -void stat_request_begin(stat_t *s, size_t req_len); +void stat_request_begin(stat_t *s, size_t req_len, uint8_t is_tcp); -void stat_request_end(stat_t *s, size_t resp_len, ev_tstamp latency); +void stat_request_end(stat_t *s, size_t resp_len, ev_tstamp latency, uint8_t is_tcp); void stat_connection_opened(stat_t *s); diff --git a/tests/robot/DnsTcpClient.py b/tests/robot/DnsTcpClient.py new file mode 100644 index 0000000..35ba0b2 --- /dev/null +++ b/tests/robot/DnsTcpClient.py @@ -0,0 +1,65 @@ +import socket + + +class DnsTcpClient: + + ROBOT_LIBRARY_SCOPE = 'GLOBAL' + + google_dns_request_byte_parts = [ + b'\x00\x1C', # 2 byte: DNS request length: 28 byte + b'\x00\x01\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00', # 12 byte + b'\x06google\x03com\x00', # 12 byte + b'\x00\x01\x00\x01' # 4 byte + ] + + def __init__(self): + self.client_socket = None + + def open_tcp_client_connection(self, host, port): + try: + self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.client_socket.settimeout(10) + self.client_socket.connect((host, int(port))) + print(f"Successfully connected to {host}:{port}") + except Exception as e: + raise Exception(f"Failed to open TCP connection: {e}") + + def send_tcp_request_parts(self, *parts): + if not self.client_socket: + raise Exception("No TCP connection open. Call 'Open Tcp Client Connection' first.") + try: + msg = b'' + for part in parts: + msg += DnsTcpClient.google_dns_request_byte_parts[int(part) - 1] + self.client_socket.sendall(msg) + print(f"Sent parts {' '.join(parts)}, bytes: {len(msg)}") + except Exception as e: + raise Exception(f"Failed to send message: {e}") + + def receive_tcp_response(self): + if not self.client_socket: + raise Exception("No TCP connection open. Call 'Open Tcp Client Connection' first.") + msg = b'' + run = True + dnslen = 0 + while run: + try: + data = self.client_socket.recv(1024) + if not data: + raise Exception(f"Connection closed!") + print(f"Received {len(data)} bytes") + msg += data + if not dnslen: + dnslen = msg[0] * 256 + msg[1] + print(f"DNS response length: {dnslen} bytes") + if len(msg) == dnslen + 2: + run = False + except Exception as e: + raise Exception(f"Failed to receive message: {e}") from e + return msg[2:] + + def close_tcp_client_connection(self): + if self.client_socket: + self.client_socket.close() + self.client_socket = None + print("TCP connection closed.") diff --git a/tests/robot/functional_tests.robot b/tests/robot/functional_tests.robot index 7d11f3b..7241f5c 100644 --- a/tests/robot/functional_tests.robot +++ b/tests/robot/functional_tests.robot @@ -3,6 +3,7 @@ Documentation Simple functional tests for https_dns_proxy Library OperatingSystem Library Process Library Collections +Library DnsTcpClient.py *** Variables *** @@ -18,6 +19,7 @@ Test Teardown Stop Proxy Common Test Setup Set Test Variable &{expected_logs} loop destroyed=1 # last log line Set Test Variable @{error_logs} [F] # any fatal error + Set Test Variable @{dig_options} +notcp # UDP only Start Proxy [Arguments] @{args} @@ -29,6 +31,7 @@ Start Proxy Set Test Variable ${dig_timeout} 2 Set Test Variable ${dig_retry} 0 Sleep 0.5 + Is Process Running ${proxy} Common Test Setup Start Proxy With Valgrind @@ -36,7 +39,7 @@ Start Proxy With Valgrind @{default_args} = Create List --track-fds=yes --time-stamp=yes --log-file=valgrind-%p.log --suppressions=valgrind.supp ... --gen-suppressions=all --tool=memcheck --leak-check=full --leak-resolution=high ... --show-leak-kinds=all --track-origins=yes --keep-stacktraces=alloc-and-free - ... ${BINARY_PATH} -v -v -v -F 100 -4 -p ${PORT} # using flight recorder with smallest possible buffer size to test memory leak + ... ${BINARY_PATH} -v -v -v -F 100 -4 -p ${PORT} # using flight recorder with smallest possible buffer size to test memory leak @{proces_args} = Combine Lists ${default_args} ${args} ${proxy} = Start Process valgrind @{proces_args} ... stderr=STDOUT alias=proxy @@ -44,11 +47,15 @@ Start Proxy With Valgrind Set Test Variable ${dig_timeout} 10 Set Test Variable ${dig_retry} 2 Sleep 6 # wait for valgrind to fire up the proxy + Is Process Running ${proxy} Common Test Setup Stop Proxy Send Signal To Process SIGINT ${proxy} ${result} = Wait For Process ${proxy} timeout=15 secs + IF $result is None + ${result} = Terminate Process ${proxy} kill=true + END Log ${result.rc} Log ${result.stdout} Log ${result.stderr} @@ -64,21 +71,24 @@ Stop Proxy Start Dig [Arguments] ${domain}=google.com - ${handle} = Start Process dig +timeout\=${dig_timeout} +retry\=${dig_retry} @127.0.0.1 -p ${PORT} ${domain} + ${handle} = Start Process dig +timeout\=${dig_timeout} +retry\=${dig_retry} @{dig_options} @127.0.0.1 -p ${PORT} ${domain} ... stderr=STDOUT alias=dig RETURN ${handle} Stop Dig - [Arguments] ${handle} - ${result} = Wait For Process ${handle} timeout=15 secs + [Arguments] ${handle} ${expect}=${None} + ${result} = Wait For Process ${handle} timeout=20 secs Log ${result.stdout} Should Be Equal As Integers ${result.rc} 0 - Should Contain ${result.stdout} ANSWER SECTION + ${expect}= Set Variable If $expect is None ANSWER SECTION ${expect} + Should Contain ${result.stdout} ${expect} + RETURN ${result.stdout} Run Dig - [Arguments] ${domain}=google.com + [Arguments] ${domain}=google.com ${expect}=${None} ${handle} = Start Dig ${domain} - Stop Dig ${handle} + ${dig_output} = Stop Dig ${handle} ${expect} + RETURN ${dig_output} Run Dig Parallel ${dig_handles} = Create List @@ -91,6 +101,25 @@ Run Dig Parallel END +Large Response Test + [Documentation] https://dnscheck.tools/#more + # use large buffer not to fragment UDP response, and ask for TXT response + Set Test Variable @{dig_options} @{dig_options} +bufsize=5000 -t txt + ${dig_output} = Run Dig txtfill4096.test.dnscheck.tools + Should Match Regexp ${dig_output} MSG SIZE\\s+rcvd: 4\\d{3}$ # expecting more than 4k large response + +Verify Truncation + [Arguments] ${domain} ${udp_buffer_size} ${result_bytes_min} ${result_bytes_max} ${expect}=${None} + # ask for TXT response + Set Test Variable @{dig_options} +notcp +ignore +bufsize=${udp_buffer_size} -t txt + ${dig_output} = Run Dig ${domain} ${expect} + Should Contain ${dig_output} flags: qr tc + # expecting response to be ${result_bytes_min} byte (could be flaky) + @{res} = Should Match Regexp ${dig_output} MSG SIZE\\s+rcvd: (\\d+)$ # expecting more than 4k large response + Should Be True ${res}[1] >= ${result_bytes_min} + Should Be True ${res}[1] <= ${result_bytes_max} + + *** Test Cases *** Handle Unbound Server Does Not Support HTTP/1.1 Start Proxy -x -r https://doh.mullvad.net/dns-query # resolver uses Unbound @@ -107,4 +136,68 @@ Reuse HTTP/2 Connection Valgrind Resource Leak Check Start Proxy With Valgrind Run Dig Parallel - \ No newline at end of file + +Valgrind Resource Leak Check TCP + Start Proxy With Valgrind + Set Test Variable @{dig_options} +tcp # TCP only + Run Dig Parallel + +Large Response UDP + Start Proxy + Large Response Test + +Large Response TCP + Start Proxy + Set Test Variable @{dig_options} +tcp # TCP only + Large Response Test + +Send TCP Requests Fragmented + [Documentation] Check manually the debug logs of dns_server_tcp.c file! + Start Proxy + Open Tcp Client Connection 127.0.0.1 ${PORT} + + # send 1st request and start 2nd one + Send Tcp Request Parts 1 + Sleep 0.01 + Send Tcp Request Parts 2 + Sleep 0.01 + Send Tcp Request Parts 3 + Sleep 0.01 + Send Tcp Request Parts 4 1 + ${dns_reply} = Receive Tcp Response + Log ${dns_reply} + Should Contain ${dns_reply} google + + # send 2nd request and start 3rd one + Send Tcp Request Parts 2 + Sleep 0.01 + Send Tcp Request Parts 3 4 1 2 + ${dns_reply} = Receive Tcp Response + Log ${dns_reply} + Should Contain ${dns_reply} google + + # finish 3rd request + Send Tcp Request Parts 3 4 + ${dns_reply} = Receive Tcp Response + Log ${dns_reply} + Should Contain ${dns_reply} google + + Close Tcp Client Connection + +Truncate UDP Small + Start Proxy + Wait Until Keyword Succeeds 5x 200ms + # too small buffer will be overridden to 512, so expecting more than 300 bytes + ... Verify Truncation microsoft.com 256 300 512 + +Truncate UDP Large + Start Proxy + Wait Until Keyword Succeeds 5x 200ms + # expecting more than 1500 byte large response + ... Verify Truncation microsoft.com 2000 1500 2000 + +Truncate UDP Impossible + Start Proxy + Wait Until Keyword Succeeds 5x 200ms + # the only TXT answer record has to be dropped to met limit + ... Verify Truncation txtfill4096.test.dnscheck.tools 4096 12 100 ANSWER: 0