From c5b5e941fb6f2f1625bb29bf09c7d41d17f76679 Mon Sep 17 00:00:00 2001 From: Aaron Drew Date: Tue, 19 Aug 2025 13:19:36 +0000 Subject: [PATCH] TCP support added. --- src/dns_server.h | 35 ------ src/dns_server_tcp.c | 165 +++++++++++++++++++++++++ src/dns_server_tcp.h | 31 +++++ src/{dns_server.c => dns_server_udp.c} | 20 ++- src/dns_server_udp.h | 35 ++++++ src/main.c | 95 +++++++++++--- tests/robot/functional_tests.robot | 42 +++++-- 7 files changed, 347 insertions(+), 76 deletions(-) delete mode 100644 src/dns_server.h create mode 100644 src/dns_server_tcp.c create mode 100644 src/dns_server_tcp.h rename src/{dns_server.c => dns_server_udp.c} (85%) create mode 100644 src/dns_server_udp.h diff --git a/src/dns_server.h b/src/dns_server.h deleted file mode 100644 index 9982285..0000000 --- a/src/dns_server.h +++ /dev/null @@ -1,35 +0,0 @@ -#ifndef _DNS_SERVER_H_ -#define _DNS_SERVER_H_ - -#include -#include -#include - -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 struct dns_server_s { - struct ev_loop *loop; - void *cb_data; - dns_req_received_cb cb; - int sock; - socklen_t addrlen; - ev_io watcher; -} dns_server_t; - -void dns_server_init(dns_server_t *d, struct ev_loop *loop, - const char *listen_addr, int listen_port, - 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_stop(dns_server_t *d); - -void dns_server_cleanup(dns_server_t *d); - -#endif // _DNS_SERVER_H_ diff --git a/src/dns_server_tcp.c b/src/dns_server_tcp.c new file mode 100644 index 0000000..cec1ed8 --- /dev/null +++ b/src/dns_server_tcp.c @@ -0,0 +1,165 @@ +#include "dns_server_tcp.h" +#include "logging.h" +#include +#include +#include +#include +#include +#include + +#define REQUEST_MAX 4096 + +typedef struct { + int fd; + ev_io io; + ev_timer timer; + dns_server_tcp_t *server; +} tcp_client_t; + +#define TCP_IDLE_TIMEOUT 90.0 + +static int get_listen_sock_tcp(const char *listen_addr, int listen_port) { + struct addrinfo hints, *ai; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_PASSIVE; + + char port_str[16]; + snprintf(port_str, sizeof(port_str), "%d", listen_port); + int res = getaddrinfo(listen_addr, port_str, &hints, &ai); + if (res != 0) { + FLOG("getaddrinfo failed: %s", gai_strerror(res)); + return -1; + } + + int sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); + if (sock < 0) { + FLOG("socket failed: %s", strerror(errno)); + freeaddrinfo(ai); + return -1; + } + + int optval = 1; + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)); + + if (bind(sock, ai->ai_addr, ai->ai_addrlen) < 0) { + FLOG("bind failed: %s", strerror(errno)); + close(sock); + freeaddrinfo(ai); + return -1; + } + + if (listen(sock, 128) < 0) { + FLOG("listen failed: %s", strerror(errno)); + close(sock); + freeaddrinfo(ai); + return -1; + } + + freeaddrinfo(ai); + ILOG("TCP Listening on %s:%d", listen_addr, listen_port); + return sock; +} + +static void tcp_client_close(struct ev_loop *loop, tcp_client_t *client) { + ev_io_stop(loop, &client->io); + ev_timer_stop(loop, &client->timer); + close(client->fd); + free(client); +} + +static void tcp_client_timeout_cb(struct ev_loop *loop, ev_timer *w, int __attribute__((unused)) revents) { + tcp_client_t *client = (tcp_client_t *)w->data; + DLOG("TCP client fd %d timed out after %ds inactivity", client->fd, (int)TCP_IDLE_TIMEOUT); + tcp_client_close(loop, client); +} + +static void tcp_client_cb(struct ev_loop *loop, ev_io *w, int __attribute__((unused)) revents) { + tcp_client_t *client = (tcp_client_t *)w->data; + int client_fd = client->fd; + uint8_t lenbuf[2]; + ssize_t n = recv(client_fd, lenbuf, 2, MSG_PEEK); + if (n < 2) { + tcp_client_close(loop, client); + return; + } + recv(client_fd, lenbuf, 2, 0); + uint16_t msglen = (lenbuf[0] << 8) | lenbuf[1]; + if (msglen > REQUEST_MAX) { + WLOG("TCP DNS request too large"); + tcp_client_close(loop, client); + return; + } + char *buf = (char *)calloc(1, msglen + 1); + if (!buf) { + FLOG("Out of mem"); + tcp_client_close(loop, client); + return; + } + ssize_t rlen = recv(client_fd, buf, msglen, 0); + if (rlen < msglen) { + WLOG("Short TCP DNS read"); + free(buf); + tcp_client_close(loop, client); + return; + } + uint16_t tx_id = ntohs(*((uint16_t*)buf)); + dns_server_tcp_t *d = client->server; + d->cb(d, d->cb_data, client_fd, tx_id, buf, msglen); + // Reset inactivity timer + ev_timer_stop(loop, &client->timer); + ev_timer_set(&client->timer, TCP_IDLE_TIMEOUT, 0.0); + ev_timer_start(loop, &client->timer); +} + +static void tcp_accept_cb(struct ev_loop *loop, ev_io *w, int __attribute__((unused)) revents) { + dns_server_tcp_t *d = (dns_server_tcp_t *)w->data; + int client_fd = accept(w->fd, NULL, NULL); + if (client_fd < 0) { + ELOG("accept failed: %s", strerror(errno)); + return; + } + tcp_client_t *client = (tcp_client_t *)calloc(1, sizeof(tcp_client_t)); + if (!client) { + FLOG("Out of mem"); + close(client_fd); + return; + } + client->fd = client_fd; + client->server = d; + client->io.data = client; + client->timer.data = client; + ev_io_init(&client->io, tcp_client_cb, client_fd, EV_READ); + ev_timer_init(&client->timer, tcp_client_timeout_cb, TCP_IDLE_TIMEOUT, 0.0); + ev_io_start(loop, &client->io); + ev_timer_start(loop, &client->timer); +} + +void dns_server_tcp_init(dns_server_tcp_t *d, struct ev_loop *loop, + const char *listen_addr, int listen_port, + dns_tcp_req_received_cb cb, void *data) { + d->loop = loop; + d->sock = get_listen_sock_tcp(listen_addr, listen_port); + d->cb = cb; + d->cb_data = data; + ev_io_init(&d->watcher, tcp_accept_cb, d->sock, EV_READ); + d->watcher.data = d; + ev_io_start(d->loop, &d->watcher); +} + +void dns_server_tcp_respond(int client_fd, char *buf, size_t blen) { + uint8_t lenbuf[2]; + lenbuf[0] = (blen >> 8) & 0xff; + lenbuf[1] = blen & 0xff; + send(client_fd, lenbuf, 2, 0); + send(client_fd, buf, blen, 0); +} + +void dns_server_tcp_stop(dns_server_tcp_t *d) { + ev_io_stop(d->loop, &d->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 100644 index 0000000..3f3c386 --- /dev/null +++ b/src/dns_server_tcp.h @@ -0,0 +1,31 @@ +#ifndef _DNS_SERVER_TCP_H_ +#define _DNS_SERVER_TCP_H_ + +#include +#include + +struct dns_server_tcp_s; + +typedef void (*dns_tcp_req_received_cb)(struct dns_server_tcp_s *dns_server, void *data, + int client_fd, uint16_t tx_id, + char *dns_req, size_t dns_req_len); + +typedef struct dns_server_tcp_s { + struct ev_loop *loop; + void *cb_data; + dns_tcp_req_received_cb cb; + int sock; + ev_io watcher; +} dns_server_tcp_t; + +void dns_server_tcp_init(dns_server_tcp_t *d, struct ev_loop *loop, + const char *listen_addr, int listen_port, + dns_tcp_req_received_cb cb, void *data); + +void dns_server_tcp_respond(int client_fd, char *buf, size_t blen); + +void dns_server_tcp_stop(dns_server_tcp_t *d); + +void dns_server_tcp_cleanup(dns_server_tcp_t *d); + +#endif // _DNS_SERVER_TCP_H_ diff --git a/src/dns_server.c b/src/dns_server_udp.c similarity index 85% rename from src/dns_server.c rename to src/dns_server_udp.c index 9f150e8..9a71d10 100644 --- a/src/dns_server.c +++ b/src/dns_server_udp.c @@ -7,7 +7,7 @@ #include // NOLINT(llvmlibc-restrict-system-libc-headers) #include // NOLINT(llvmlibc-restrict-system-libc-headers) -#include "dns_server.h" +#include "dns_server_udp.h" #include "logging.h" @@ -60,7 +60,7 @@ static int get_listen_sock(const char *listen_addr, int listen_port, 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; + dns_server_udp_t *d = (dns_server_udp_t *)w->data; char *buf = (char *)calloc(1, REQUEST_MAX + 1); if (buf == NULL) { @@ -85,32 +85,30 @@ static void watcher_cb(struct ev_loop __attribute__((unused)) *loop, d->cb(d, d->cb_data, (struct sockaddr*)&raddr, tx_id, buf, len); } -void dns_server_init(dns_server_t *d, struct ev_loop *loop, - const char *listen_addr, int listen_port, - dns_req_received_cb cb, void *data) { +void dns_server_udp_init(dns_server_udp_t *d, struct ev_loop *loop, + const char *listen_addr, int listen_port, + dns_udp_req_received_cb cb, void *data) { d->loop = loop; d->sock = get_listen_sock(listen_addr, listen_port, &d->addrlen); d->cb = cb; d->cb_data = data; - - // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) ev_io_init(&d->watcher, watcher_cb, d->sock, EV_READ); d->watcher.data = d; ev_io_start(d->loop, &d->watcher); } -void dns_server_respond(dns_server_t *d, struct sockaddr *raddr, char *buf, - size_t blen) { +void dns_server_udp_respond(dns_server_udp_t *d, struct sockaddr *raddr, char *buf, + size_t blen) { ssize_t len = sendto(d->sock, buf, blen, 0, raddr, d->addrlen); if(len == -1) { DLOG("sendto failed: %s", strerror(errno)); } } -void dns_server_stop(dns_server_t *d) { +void dns_server_udp_stop(dns_server_udp_t *d) { ev_io_stop(d->loop, &d->watcher); } -void dns_server_cleanup(dns_server_t *d) { +void dns_server_udp_cleanup(dns_server_udp_t *d) { close(d->sock); } diff --git a/src/dns_server_udp.h b/src/dns_server_udp.h new file mode 100644 index 0000000..bc814e7 --- /dev/null +++ b/src/dns_server_udp.h @@ -0,0 +1,35 @@ +#ifndef _DNS_SERVER_UDP_H_ +#define _DNS_SERVER_UDP_H_ + +#include +#include +#include + +struct dns_server_udp_s; + +typedef void (*dns_udp_req_received_cb)(struct dns_server_udp_s *dns_server, void *data, + struct sockaddr* addr, uint16_t tx_id, + char *dns_req, size_t dns_req_len); + +typedef struct dns_server_udp_s { + struct ev_loop *loop; + void *cb_data; + dns_udp_req_received_cb cb; + int sock; + socklen_t addrlen; + ev_io watcher; +} dns_server_udp_t; + +void dns_server_udp_init(dns_server_udp_t *d, struct ev_loop *loop, + const char *listen_addr, int listen_port, + dns_udp_req_received_cb cb, void *data); + +// Sends a DNS response 'buf' of length 'blen' to 'raddr'. +void dns_server_udp_respond(dns_server_udp_t *d, struct sockaddr *raddr, char *buf, + size_t blen); + +void dns_server_udp_stop(dns_server_udp_t *d); + +void dns_server_udp_cleanup(dns_server_udp_t *d); + +#endif // _DNS_SERVER_UDP_H_ diff --git a/src/main.c b/src/main.c index 2e9f5f2..e3c41f4 100644 --- a/src/main.c +++ b/src/main.c @@ -10,7 +10,8 @@ #include // NOLINT(llvmlibc-restrict-system-libc-headers) #include "dns_poller.h" -#include "dns_server.h" +#include "dns_server_udp.h" +#include "dns_server_tcp.h" #include "https_client.h" #include "logging.h" #include "options.h" @@ -28,7 +29,7 @@ typedef struct { // NOLINTNEXTLINE(altera-struct-pack-align) typedef struct { - dns_server_t *dns_server; + dns_server_udp_t *dns_server; char* dns_req; stat_t *stat; ev_tstamp start_tstamp; @@ -36,6 +37,16 @@ typedef struct { struct sockaddr_storage raddr; } request_t; +// TCP DNS request context +typedef struct { + dns_server_tcp_t *dns_server_tcp; + char* dns_req; + stat_t *stat; + ev_tstamp start_tstamp; + uint16_t tx_id; + int client_fd; +} tcp_request_t; + static int is_ipv4_address(char *str) { struct in6_addr addr; return inet_pton(AF_INET, str, &addr) == 1; @@ -94,7 +105,7 @@ static void https_resp_cb(void *data, char *buf, size_t buflen) { 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); + dns_server_udp_respond(req->dns_server, (struct sockaddr*)&req->raddr, buf, buflen); if (req->stat) { stat_request_end(req->stat, buflen, ev_now(req->dns_server->loop) - req->start_tstamp); } @@ -104,33 +115,48 @@ static void https_resp_cb(void *data, char *buf, size_t buflen) { free(req); } -static void dns_server_cb(dns_server_t *dns_server, void *data, +static void https_resp_tcp_cb(void *data, char *buf, size_t buflen) { + tcp_request_t *req = (tcp_request_t *)data; + DLOG("[TCP] Received response for id: %hX, len: %zu", req->tx_id, buflen); + free((void*)req->dns_req); + if (buf != NULL) { + if (buflen < (int)sizeof(uint16_t)) { + WLOG("[TCP] %04hX: Malformed response received (too short)", req->tx_id); + } else { + uint16_t response_id = ntohs(*((uint16_t*)buf)); + if (req->tx_id != response_id) { + WLOG("[TCP] DNS request and response IDs are not matching: %hX != %hX", req->tx_id, response_id); + } else { + dns_server_tcp_respond(req->client_fd, buf, buflen); + if (req->stat) { + stat_request_end(req->stat, buflen, ev_now(req->dns_server_tcp->loop) - req->start_tstamp); + } + } + } + } + free(req); +} + +static void dns_server_cb(dns_server_udp_t *dns_server, void *data, struct sockaddr* addr, uint16_t tx_id, char *dns_req, size_t dns_req_len) { app_state_t *app = (app_state_t *)data; - 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 - // gethostbyname() which can cause a DNS loop due to the nameserver listed - // in resolv.conf being or depending on https_dns_proxy itself. if(app->using_dns_poller && (app->resolv == NULL || app->resolv->data == NULL)) { WLOG("%04hX: Query received before bootstrapping is completed, discarding.", tx_id); free(dns_req); return; } - request_t *req = (request_t *)calloc(1, sizeof(request_t)); if (req == NULL) { 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, addr, dns_server->addrlen); req->dns_server = dns_server; - req->dns_req = dns_req; // To free buffer after https request is complete. + req->dns_req = dns_req; req->start_tstamp = ev_now(dns_server->loop); req->stat = app->stat; - if (req->stat) { stat_request_begin(app->stat, dns_req_len); } @@ -138,6 +164,33 @@ static void dns_server_cb(dns_server_t *dns_server, void *data, dns_req, dns_req_len, app->resolv, req->tx_id, https_resp_cb, req); } +static void dns_server_tcp_cb(dns_server_tcp_t *dns_server_tcp, void *data, + int client_fd, uint16_t tx_id, + char *dns_req, size_t dns_req_len) { + app_state_t *app = (app_state_t *)data; + DLOG("[TCP] Received request for id: %hX, len: %d", tx_id, dns_req_len); + if(app->using_dns_poller && (app->resolv == NULL || app->resolv->data == NULL)) { + WLOG("[TCP] %04hX: Query received before bootstrapping is completed, discarding.", tx_id); + free(dns_req); + return; + } + tcp_request_t *req = (tcp_request_t *)calloc(1, sizeof(tcp_request_t)); + if (req == NULL) { + FLOG("[TCP] %04hX: Out of mem", tx_id); + } + req->tx_id = tx_id; + req->dns_server_tcp = dns_server_tcp; + req->dns_req = dns_req; + req->start_tstamp = ev_now(dns_server_tcp->loop); + req->stat = app->stat; + req->client_fd = client_fd; + if (req->stat) { + stat_request_begin(app->stat, dns_req_len); + } + https_client_fetch(app->https_client, app->resolver_url, + dns_req, dns_req_len, app->resolv, req->tx_id, https_resp_tcp_cb, req); +} + static int addr_list_reduced(const char* full_list, const char* list) { const char *pos = list; const char *end = list + strlen(list); @@ -300,9 +353,13 @@ int main(int argc, char *argv[]) { app.using_dns_poller = 0; app.stat = (opt.stats_interval ? &stat : NULL); - dns_server_t dns_server; - dns_server_init(&dns_server, loop, opt.listen_addr, opt.listen_port, - dns_server_cb, &app); + dns_server_udp_t dns_server; + dns_server_udp_init(&dns_server, loop, opt.listen_addr, opt.listen_port, + dns_server_cb, &app); + + dns_server_tcp_t dns_server_tcp; + dns_server_tcp_init(&dns_server_tcp, loop, opt.listen_addr, opt.listen_port, + dns_server_tcp_cb, &app); if (opt.gid != (uid_t)-1 && setgroups(1, &opt.gid)) { FLOG("Failed to set groups"); @@ -366,14 +423,16 @@ int main(int argc, char *argv[]) { ev_signal_stop(loop, &sigterm); ev_signal_stop(loop, &sigint); ev_signal_stop(loop, &sigpipe); - dns_server_stop(&dns_server); + dns_server_udp_stop(&dns_server); + dns_server_tcp_stop(&dns_server_tcp); stat_stop(&stat); DLOG("re-entering loop"); ev_run(loop, 0); DLOG("loop finished all events"); - dns_server_cleanup(&dns_server); + dns_server_udp_cleanup(&dns_server); + dns_server_tcp_cleanup(&dns_server_tcp); https_client_cleanup(&https_client); stat_cleanup(&stat); diff --git a/tests/robot/functional_tests.robot b/tests/robot/functional_tests.robot index 7d11f3b..162a403 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 +Test Teardown Stop Proxy *** Variables *** @@ -10,10 +11,6 @@ ${BINARY_PATH} ${CURDIR}/../../https_dns_proxy ${PORT} 55353 -*** Settings *** -Test Teardown Stop Proxy - - *** Keywords *** Common Test Setup Set Test Variable &{expected_logs} loop destroyed=1 # last log line @@ -23,8 +20,7 @@ Start Proxy [Arguments] @{args} @{default_args} = Create List -v -v -v -4 -p ${PORT} @{proces_args} = Combine Lists ${default_args} ${args} - ${proxy} = Start Process ${BINARY_PATH} @{proces_args} - ... stderr=STDOUT alias=proxy + ${proxy} = Start Process ${BINARY_PATH} @{proces_args} alias=proxy stderr=STDOUT Set Test Variable ${proxy} Set Test Variable ${dig_timeout} 2 Set Test Variable ${dig_retry} 0 @@ -38,8 +34,7 @@ Start Proxy With Valgrind ... --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 @{proces_args} = Combine Lists ${default_args} ${args} - ${proxy} = Start Process valgrind @{proces_args} - ... stderr=STDOUT alias=proxy + ${proxy} = Start Process valgrind @{proces_args} alias=proxy stderr=STDOUT Set Test Variable ${proxy} Set Test Variable ${dig_timeout} 10 Set Test Variable ${dig_retry} 2 @@ -64,8 +59,7 @@ 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} - ... stderr=STDOUT alias=dig + ${handle} = Start Process dig +timeout\=${dig_timeout} +retry\=${dig_retry} @127.0.0.1 -p ${PORT} ${domain} alias=dig stderr=STDOUT RETURN ${handle} Stop Dig @@ -77,7 +71,7 @@ Stop Dig Run Dig [Arguments] ${domain}=google.com - ${handle} = Start Dig ${domain} + ${handle} = Start Dig ${domain} Stop Dig ${handle} Run Dig Parallel @@ -107,4 +101,28 @@ Reuse HTTP/2 Connection Valgrind Resource Leak Check Start Proxy With Valgrind Run Dig Parallel - \ No newline at end of file + +TCP Query Works + [Documentation] Ensure DNS queries over TCP are handled and answered + Start Proxy + ${args} = Create List dig +tcp +timeout=${dig_timeout} +retry=${dig_retry} @127.0.0.1 -p ${PORT} google.com + ${handle} = Start Process @{args} alias=dig_tcp stderr=STDOUT + ${result} = Wait For Process ${handle} timeout=15 secs + Log ${result.stdout} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} ANSWER SECTION + Stop Proxy + +TCP Connection Reuse + [Documentation] Ensure multiple DNS queries can be sent over the same TCP connection + Start Proxy + ${domains} = Create List facebook.com google.com amazon.com + FOR ${domain} IN @{domains} + ${args} = Create List dig +tcp +timeout=${dig_timeout} +retry=${dig_retry} @127.0.0.1 -p ${PORT} ${domain} + ${handle} = Start Process @{args} alias=dig_tcp stderr=STDOUT + ${result} = Wait For Process ${handle} timeout=15 secs + Log ${result.stdout} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} ANSWER SECTION + END + Stop Proxy