Skip to content

Commit daa0d39

Browse files
authored
Large improvement pack (#192)
* Added DNS server with TCP support * Added TCP client limit option, also can be used to disable TCP DNS server in case of issues * Readme and help pages updated * Combined API with UDP DNS server * Use minimally necessary public header * Added TCP only statistic counters * Added TCP functional tests * Basic querries with valgrind - fragmented TCP request processing - testing 4k large response support * Started to use stricter compiler warnings * UDP truncation feature * Enable more compiler warnings * Replace c-ares deprecated functions * Reset HTTPS client on timeouts * systemd service improvements * Version bump
1 parent 2963960 commit daa0d39

22 files changed

+1089
-181
lines changed

.github/workflows/cmake.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
run: sudo apt-get update
2323

2424
- name: Setup Dependencies
25-
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 }}
25+
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 }}
2626

2727
- name: Setup Python Virtual Environment
2828
run: python3 -m venv ${{github.workspace}}/venv

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ output.xml
1818
report.html
1919
custom_curl/
2020
valgrind-*.log
21+
tests/robot/__pycache__

CMakeLists.txt

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
cmake_minimum_required(VERSION 3.7)
22
project(HttpsDnsProxy C)
33

4+
include(CheckIncludeFile)
5+
46
# FUNCTIONS
57

68
# source: https://stackoverflow.com/a/27990434
@@ -25,15 +27,19 @@ if (NOT CMAKE_INSTALL_BINDIR)
2527
set(CMAKE_INSTALL_BINDIR bin)
2628
endif()
2729

28-
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra --pedantic -Wno-strict-aliasing -Wno-variadic-macros")
30+
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")
2931
set(CMAKE_C_FLAGS_DEBUG "-gdwarf-4 -DDEBUG")
3032
set(CMAKE_C_FLAGS_RELEASE "-O2")
3133

32-
if ((CMAKE_C_COMPILER_ID MATCHES GNU AND CMAKE_C_COMPILER_VERSION VERSION_GREATER_EQUAL 9) OR
33-
(CMAKE_C_COMPILER_ID MATCHES Clang AND CMAKE_C_COMPILER_VERSION VERSION_GREATER_EQUAL 10))
34+
if (((CMAKE_C_COMPILER_ID MATCHES GNU AND CMAKE_C_COMPILER_VERSION VERSION_GREATER_EQUAL 9) AND
35+
(CMAKE_C_COMPILER_ID MATCHES GNU AND CMAKE_C_COMPILER_VERSION VERSION_LESS 14)) OR
36+
( CMAKE_C_COMPILER_ID MATCHES Clang AND CMAKE_C_COMPILER_VERSION VERSION_GREATER_EQUAL 10))
3437
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-gnu-zero-variadic-macro-arguments -Wno-gnu-folding-constant")
3538
endif()
3639

40+
set(SERVICE_EXTRA_OPTIONS "")
41+
set(SERVICE_TYPE "simple")
42+
3743
# VERSION
3844
# It is possible to define external default value like: cmake -DSW_VERSION=1.2-custom
3945

@@ -81,6 +87,15 @@ include_directories(
8187
${LIBCARES_INCLUDE_DIR} ${LIBCURL_INCLUDE_DIR}
8288
${LIBEV_INCLUDE_DIR} src)
8389

90+
check_include_file("systemd/sd-daemon.h" HAVE_SD_DAEMON_H)
91+
92+
if(HAVE_SD_DAEMON_H)
93+
message(STATUS "Using libsystemd")
94+
add_definitions(-DHAS_LIBSYSTEMD=1)
95+
set(LIBS ${LIBS} systemd)
96+
set(SERVICE_TYPE "notify")
97+
endif()
98+
8499
# CLANG TIDY
85100

86101
option(USE_CLANG_TIDY "Use clang-tidy during compilation" ON)
@@ -95,7 +110,7 @@ if(USE_CLANG_TIDY)
95110
message(STATUS "clang-tidy not found.")
96111
else()
97112
message(STATUS "clang-tidy found: ${CLANG_TIDY_EXE}")
98-
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")
113+
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")
99114
endif()
100115
else()
101116
message(STATUS "Not using clang-tidy.")
@@ -132,7 +147,6 @@ endif()
132147

133148
install(TARGETS ${TARGET_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR})
134149

135-
set(SERVICE_EXTRA_OPTIONS "")
136150
if(IS_DIRECTORY "/etc/munin/plugins" AND
137151
IS_DIRECTORY "/etc/munin/plugin-conf.d")
138152
set(SERVICE_EXTRA_OPTIONS "-s 300")

README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
`https_dns_proxy` is a light-weight DNS<-->HTTPS, non-caching translation
44
proxy for the [RFC 8484][rfc-8484] DNS-over-HTTPS standard. It receives
5-
regular (UDP) DNS requests and issues them via DoH.
5+
regular (UDP or TCP) DNS requests and issues them via DoH.
66

77
[Google's DNS-over-HTTPS][google-doh] service is default, but
88
[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)`.
4848
On Debian-derived systems those are libc-ares-dev,
4949
libcurl4-{openssl,nss,gnutls}-dev and libev-dev respectively.
5050
On Redhat-derived systems those are c-ares-devel, libcurl-devel and libev-devel.
51+
On systems with systemd it is recommended to have libsystemd development package installed.
5152

5253
On MacOS, you may run into issues with curl headers. Others have had success when first installing curl with brew.
5354
```
@@ -57,7 +58,7 @@ brew link curl --force
5758

5859
On Ubuntu
5960
```
60-
apt-get install cmake libc-ares-dev libcurl4-openssl-dev libev-dev build-essential
61+
apt-get install cmake libc-ares-dev libcurl4-openssl-dev libev-dev libsystemd-dev build-essential
6162
```
6263

6364
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 \
158159
Just run it as a daemon and point traffic at it. Commandline flags are:
159160

160161
```
161-
Usage: ./https_dns_proxy [-a <listen_addr>] [-p <listen_port>]
162+
Usage: ./https_dns_proxy [-a <listen_addr>] [-p <listen_port>] [-T <tcp_client_limit>]
162163
[-b <dns_servers>] [-i <polling_interval>] [-4]
163164
[-r <resolver_url>] [-t <proxy_server>] [-x] [-q] [-C <ca_path>] [-c <dscp_codepoint>]
164165
[-d] [-u <user>] [-g <group>]
@@ -167,6 +168,8 @@ Usage: ./https_dns_proxy [-a <listen_addr>] [-p <listen_port>]
167168
DNS server
168169
-a listen_addr Local IPv4/v6 address to bind to. (Default: 127.0.0.1)
169170
-p listen_port Local port to bind to. (Default: 5053)
171+
-T tcp_client_limit Number of TCP clients to serve.
172+
(Default: 20, Disabled: 0, Min: 1, Max: 200)
170173
171174
DNS client
172175
-b dns_servers Comma-separated IPv4/v6 addresses and ports (addr:port)
@@ -189,6 +192,9 @@ Usage: ./https_dns_proxy [-a <listen_addr>] [-p <listen_port>]
189192
-q Use HTTP/3 (QUIC) only.
190193
-m max_idle_time Maximum idle time in seconds allowed for reusing a HTTPS connection.
191194
(Default: 118, Min: 0, Max: 3600)
195+
-L conn_loss_time Time in seconds to tolerate connection timeouts of reused connections.
196+
This option mitigates half-open TCP connection issue (e.g. WAN IP change).
197+
(Default: 15, Min: 5, Max: 60)
192198
-C ca_path Optional file containing CA certificates.
193199
-c dscp_codepoint Optional DSCP codepoint to set on upstream HTTPS server
194200
connections. (Min: 0, Max: 63)

https_dns_proxy.service.in

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ Before=nss-lookup.target
66
After=network.target
77

88
[Service]
9-
Type=simple
9+
Type=${SERVICE_TYPE}
1010
DynamicUser=yes
11-
Restart=on-failure
1211
ExecStart=${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}/https_dns_proxy \
1312
-v -v ${SERVICE_EXTRA_OPTIONS}
13+
Restart=on-failure
14+
RestartSec=5
15+
TimeoutStartSec=20
1416
TimeoutStopSec=10
1517

1618
[Install]

munin/https_dns_proxy.plugin

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ graph_scale no
1111
graph_args --base 1000 --lower-limit 0
1212
requests.label Requests
1313
responses.label Responses
14+
tcprequests.label TcpRequests
15+
tcpresponses.label TcpResponses
1416
1517
multigraph https_dns_proxy_latency
1618
graph_title HTTPS DNS proxy - latency
@@ -19,6 +21,7 @@ graph_category network
1921
graph_scale no
2022
graph_args --base 1000 --lower-limit 0
2123
latency.label Latency
24+
tcplatency.label TcpLatency
2225
2326
multigraph https_dns_proxy_connections
2427
graph_title HTTPS DNS proxy - connections
@@ -40,7 +43,7 @@ EOM
4043
esac
4144

4245
log_lines=$(journalctl --unit https_dns_proxy.service --output cat --since '6 minutes ago')
43-
pattern='stat\.c:[0-9]+ ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+)$'
46+
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]+)$'
4447

4548
# match log lines with pattern (last match will be used)
4649
IFS='
@@ -53,18 +56,26 @@ for line in $log_lines; do
5356
fi
5457
done
5558

56-
latency='U'
5759
if [ -n "${stat[3]}" ] && \
5860
[ -n "${stat[2]}" ] && \
5961
[ "${stat[2]}" -gt "0" ]; then
6062
latency=$((${stat[3]} / ${stat[2]}))
6163
fi
6264

65+
if [ -n "${stat[11]}" ] && \
66+
[ -n "${stat[10]}" ] && \
67+
[ "${stat[10]}" -gt "0" ]; then
68+
tcplatency=$((${stat[11]} / ${stat[10]}))
69+
fi
70+
6371
echo "multigraph https_dns_proxy_count"
6472
echo "requests.value ${stat[1]:-U}"
6573
echo "responses.value ${stat[2]:-U}"
74+
echo "tcprequests.value ${stat[9]:-U}"
75+
echo "tcpresponses.value ${stat[10]:-U}"
6676
echo "multigraph https_dns_proxy_latency"
67-
echo "latency.value ${latency}"
77+
echo "latency.value ${latency:-0}"
78+
echo "tcplatency.value ${tcplatency:-0}"
6879
echo "multigraph https_dns_proxy_connections"
6980
echo "opened.value ${stat[6]:-U}"
7081
echo "closed.value ${stat[7]:-U}"

src/dns_poller.c

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
static void sock_cb(struct ev_loop __attribute__((unused)) *loop,
88
ev_io *w, int revents) {
99
dns_poller_t *d = (dns_poller_t *)w->data;
10-
ares_process_fd(d->ares, (revents & EV_READ) ? w->fd : ARES_SOCKET_BAD,
11-
(revents & EV_WRITE) ? w->fd : ARES_SOCKET_BAD);
10+
ares_process_fd(d->ares, (revents & EV_READ) ? w->fd : ARES_SOCKET_BAD,
11+
(revents & EV_WRITE) ? w->fd : ARES_SOCKET_BAD);
1212
}
1313

1414
static struct ev_io * get_io_event(dns_poller_t *d, int sock) {
15-
for (int i = 0; i < d->io_events_count; i++) {
15+
for (unsigned i = 0; i < d->io_events_count; i++) {
1616
if (d->io_events[i].fd == sock) {
1717
return &d->io_events[i];
1818
}
@@ -35,7 +35,7 @@ static void sock_state_cb(void *data, int fd, int read, int write) {
3535
// reserve and start new event on unused slot
3636
io_event_ptr = get_io_event(d, 0);
3737
if (!io_event_ptr) {
38-
FLOG("c-ares needed more IO event handler, than the number of provided nameservers: %d", d->io_events_count);
38+
FLOG("c-ares needed more IO event handler, than the number of provided nameservers: %u", d->io_events_count);
3939
}
4040
DLOG("Reserved new io event: %p", io_event_ptr);
4141
// NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling)
@@ -44,21 +44,43 @@ static void sock_state_cb(void *data, int fd, int read, int write) {
4444
ev_io_start(d->loop, io_event_ptr);
4545
}
4646

47-
static char *get_addr_listing(char** addr_list, const int af) {
47+
static char *get_addr_listing(struct ares_addrinfo_node * nodes) {
4848
char *list = (char *)calloc(1, POLLER_ADDR_LIST_SIZE);
49-
char *pos = list;
5049
if (list == NULL) {
5150
FLOG("Out of mem");
5251
}
53-
for (int i = 0; addr_list[i]; i++) {
54-
const char *res = ares_inet_ntop(af, addr_list[i], pos,
55-
list + POLLER_ADDR_LIST_SIZE - 1 - pos);
52+
char *pos = list;
53+
unsigned ipv4 = 0;
54+
unsigned ipv6 = 0;
55+
56+
for (struct ares_addrinfo_node *node = nodes; node != NULL; node = node->ai_next) {
57+
const char *res = NULL;
58+
59+
if (node->ai_family == AF_INET) {
60+
res = ares_inet_ntop(AF_INET, (const void *)&((struct sockaddr_in *)node->ai_addr)->sin_addr,
61+
pos, (ares_socklen_t)(list + POLLER_ADDR_LIST_SIZE - 1 - pos));
62+
ipv4++;
63+
} else if (node->ai_family == AF_INET6) {
64+
res = ares_inet_ntop(AF_INET6, (const void *)&((struct sockaddr_in6 *)node->ai_addr)->sin6_addr,
65+
pos, (ares_socklen_t)(list + POLLER_ADDR_LIST_SIZE - 1 - pos));
66+
ipv6++;
67+
} else {
68+
WLOG("Unhandled address family: %d", node->ai_family);
69+
continue;
70+
}
71+
5672
if (res != NULL) {
5773
pos += strlen(pos);
5874
*pos = ',';
5975
pos++;
76+
} else {
77+
DLOG("Not enough space left for further IP addresses"); // test with POLLER_ADDR_LIST_SIZE = 10 value
78+
break;
6079
}
6180
}
81+
82+
DLOG("Received %u IPv4 and %u IPv6 addresses", ipv4, ipv6);
83+
6284
if (pos == list) {
6385
free((void*)list);
6486
list = NULL;
@@ -69,19 +91,20 @@ static char *get_addr_listing(char** addr_list, const int af) {
6991
}
7092

7193
static void ares_cb(void *arg, int status, int __attribute__((unused)) timeouts,
72-
struct hostent *h) {
94+
struct ares_addrinfo *result) {
7395
dns_poller_t *d = (dns_poller_t *)arg;
7496
d->request_ongoing = 0;
7597
ev_tstamp interval = 5; // retry by default after some time
7698

7799
if (status != ARES_SUCCESS) {
78100
WLOG("DNS lookup of '%s' failed: %s", d->hostname, ares_strerror(status));
79-
} else if (!h || h->h_length < 1) {
101+
} else if (!result || result->nodes == NULL) {
80102
WLOG("No hosts found for '%s'", d->hostname);
81103
} else {
82104
interval = d->polling_interval;
83-
d->cb(d->hostname, d->cb_data, get_addr_listing(h->h_addr_list, h->h_addrtype));
105+
d->cb(d->hostname, d->cb_data, get_addr_listing(result->nodes));
84106
}
107+
ares_freeaddrinfo(result);
85108

86109
if (status != ARES_EDESTRUCTION) {
87110
DLOG("DNS poll interval changed to: %.0lf", interval);
@@ -97,18 +120,23 @@ static ev_tstamp get_timeout(dns_poller_t *d)
97120
struct timeval tv;
98121
struct timeval *tvp = ares_timeout(d->ares, &max_tv, &tv);
99122
// NOLINTNEXTLINE(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions)
100-
ev_tstamp after = tvp->tv_sec + tvp->tv_usec * 1e-6;
101-
return after ? after : 0.1;
123+
ev_tstamp after = (double)tvp->tv_sec + (double)tvp->tv_usec * 1e-6;
124+
return after > 0.1 ? after : 0.1;
102125
}
103126

104127
static void timer_cb(struct ev_loop __attribute__((unused)) *loop,
105128
ev_timer *w, int __attribute__((unused)) revents) {
106129
dns_poller_t *d = (dns_poller_t *)w->data;
107130

108131
if (d->request_ongoing) {
109-
// process query timeouts
110-
DLOG("Processing DNS queries");
132+
DLOG("Processing DNS query timeouts");
133+
#if ARES_VERSION_MAJOR >= 1 && ARES_VERSION_MINOR >= 34
134+
ares_process_fds(d->ares, NULL, 0, ARES_PROCESS_FLAG_NONE);
135+
#elif ARES_VERSION_MAJOR >= 1 && ARES_VERSION_MINOR >= 28
136+
ares_process_fd(d->ares, ARES_SOCKET_BAD, ARES_SOCKET_BAD);
137+
#else
111138
ares_process(d->ares, NULL, NULL);
139+
#endif
112140
} else {
113141
DLOG("Starting DNS query");
114142
// 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,
117145
// free memory tied up by any "zombie" queries.
118146
ares_cancel(d->ares);
119147
d->request_ongoing = 1;
120-
ares_gethostbyname(d->ares, d->hostname, d->family, ares_cb, d);
148+
149+
struct ares_addrinfo_hints hints;
150+
memset(&hints, 0, sizeof(hints)); // NOLINT(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling)
151+
hints.ai_flags = ARES_AI_CANONNAME;
152+
hints.ai_family = d->family;
153+
hints.ai_socktype = SOCK_STREAM;
154+
155+
ares_getaddrinfo(d->ares, d->hostname, "https", &hints, ares_cb, d);
121156
}
122157

123158
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,
170205
d->timer.data = d;
171206
ev_timer_start(d->loop, &d->timer);
172207

173-
int nameservers = 1;
174-
for (int i = 0; bootstrap_dns[i]; i++) {
208+
unsigned nameservers = 1;
209+
for (unsigned i = 0; bootstrap_dns[i]; i++) {
175210
if (bootstrap_dns[i] == ',') {
176211
nameservers++;
177212
}
@@ -181,7 +216,7 @@ void dns_poller_init(dns_poller_t *d, struct ev_loop *loop,
181216
if (!d->io_events) {
182217
FLOG("Out of mem");
183218
}
184-
for (int i = 0; i < nameservers; i++) {
219+
for (unsigned i = 0; i < nameservers; i++) {
185220
d->io_events[i].data = d;
186221
}
187222
d->io_events_count = nameservers;

src/dns_poller.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ typedef struct {
3030

3131
ev_timer timer;
3232
ev_io *io_events;
33-
int io_events_count;
33+
unsigned io_events_count;
3434
} dns_poller_t;
3535

3636
// Initializes c-ares and starts a timer for periodic DNS resolution on the

0 commit comments

Comments
 (0)