Skip to content

Commit 6bb3d56

Browse files
authored
Merge pull request #128 from baranyaib90/quic_and_robot
Fixes 8: Quic and robot
2 parents da2501f + 2cbb096 commit 6bb3d56

File tree

9 files changed

+249
-43
lines changed

9 files changed

+249
-43
lines changed

.github/workflows/cmake.yml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ jobs:
2121
run: sudo apt-get update
2222

2323
- name: Setup Dependencies
24-
run: sudo apt-get install cmake libc-ares-dev libcurl4-openssl-dev libev-dev build-essential clang-tidy-12 ${{ matrix.compiler }}
24+
run: sudo apt-get install cmake libc-ares-dev libcurl4-openssl-dev libev-dev build-essential clang-tidy-12 ${{ matrix.compiler }} dnsutils python3-pip
25+
26+
- name: Setup Robot Framework
27+
run: sudo pip3 install robotframework
2528

2629
- name: Set clang-tidy
2730
run: sudo update-alternatives --install /usr/bin/clang-tidy clang-tidy /usr/bin/clang-tidy-12 100
@@ -36,3 +39,12 @@ jobs:
3639
CC: ${{ matrix.compiler }}
3740
# Build your program with the given configuration
3841
run: make -C ${{github.workspace}}/
42+
43+
- name: Test
44+
run: make -C ${{github.workspace}}/ test ARGS="--verbose"
45+
46+
- uses: actions/upload-artifact@v2
47+
if: ${{ success() || failure() }}
48+
with:
49+
name: robot-logs-${{ matrix.compiler }}
50+
path: ${{github.workspace}}/tests/robot/*.html

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,6 @@ install_manifest.txt
1313
.ninja_log
1414
build.ninja
1515
rules.ninja
16+
log.html
17+
output.xml
18+
report.html

CMakeLists.txt

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

4+
# FUNCTIONS
5+
46
# source: https://stackoverflow.com/a/27990434
57
function(define_file_basename_for_sources targetname)
68
get_target_property(source_files "${targetname}" SOURCES)
@@ -12,6 +14,8 @@ function(define_file_basename_for_sources targetname)
1214
endforeach()
1315
endfunction()
1416

17+
# CONFIG
18+
1519
set(CMAKE_BUILD_TYPE "Debug")
1620
#set(CMAKE_BUILD_TYPE "Release")
1721

@@ -24,6 +28,8 @@ if ((CMAKE_C_COMPILER_ID MATCHES GNU AND CMAKE_C_COMPILER_VERSION VERSION_GREA
2428
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-gnu-zero-variadic-macro-arguments -Wno-gnu-folding-constant")
2529
endif()
2630

31+
# VERSION
32+
2733
find_package(Git)
2834
if(Git_FOUND)
2935
execute_process(
@@ -42,13 +48,17 @@ else()
4248
message(WARNING "Could not find git command! Version is set to: ${GIT_VERSION}")
4349
endif()
4450

51+
# LIBRARY DEPENDENCIES
52+
4553
find_path(LIBCARES_INCLUDE_DIR ares.h)
4654
find_path(LIBCURL_INCLUDE_DIR curl/curl.h)
4755
find_path(LIBEV_INCLUDE_DIR ev.h)
4856
include_directories(
4957
${LIBCARES_INCLUDE_DIR} ${LIBCURL_INCLUDE_DIR}
5058
${LIBEV_INCLUDE_DIR} src)
5159

60+
# CLANG TIDY
61+
5262
find_program(
5363
CLANG_TIDY_EXE
5464
NAMES "clang-tidy"
@@ -61,6 +71,8 @@ else()
6171
set(DO_CLANG_TIDY "${CLANG_TIDY_EXE}" "-fix" "-checks=*,-clang-analyzer-alpha.*,-misc-unused-parameters,-cert-err34-c,-google-readability-todo,-hicpp-signed-bitwise,-cppcoreguidelines-avoid-magic-numbers,-readability-magic-numbers,-gnu-folding-constant,-gnu-zero-variadic-macro-arguments,-readability-function-cognitive-complexity,-concurrency-mt-unsafe")
6272
endif()
6373

74+
# BUILD
75+
6476
# The main binary
6577
set(TARGET_NAME "https_dns_proxy")
6678
aux_source_directory(src SRC_LIST)
@@ -82,6 +94,8 @@ if(CLANG_TIDY_EXE)
8294
)
8395
endif()
8496

97+
# INSTALL
98+
8599
install(TARGETS ${TARGET_NAME} DESTINATION bin)
86100

87101
set(SERVICE_EXTRA_OPTIONS "")
@@ -100,3 +114,20 @@ configure_file(${TARGET_NAME}.service.in ${TARGET_NAME}.service)
100114

101115
install(FILES ${TARGET_NAME}.service
102116
DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/systemd/system/)
117+
118+
# TESTING
119+
120+
find_program(
121+
PYTHON3_EXE
122+
NAMES "python3"
123+
DOC "Path to python3 executable"
124+
)
125+
if(NOT PYTHON3_EXE)
126+
message(STATUS "python3 not found, robot testing not possible")
127+
else()
128+
message(STATUS "python3 found: ${PYTHON3_EXE}")
129+
130+
enable_testing()
131+
add_test(NAME robot COMMAND ${PYTHON3_EXE} -m robot.run functional_tests.robot
132+
WORKING_DIRECTORY tests/robot)
133+
endif()

README.md

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,9 @@ Just run it as a daemon and point traffic at it. Commandline flags are:
146146
```
147147
Usage: ./https_dns_proxy [-a <listen_addr>] [-p <listen_port>]
148148
[-d] [-u <user>] [-g <group>] [-b <dns_servers>]
149-
[-r <resolver_url>] [-e <subnet_addr>]
150-
[-t <proxy_server>] [-l <logfile>] -c <dscp_codepoint>
151-
[-x] [-v]+
149+
[-i <polling_interval>] [-4] [-r <resolver_url>]
150+
[-t <proxy_server>] [-l <logfile>] [-c <dscp_codepoint>]
151+
[-x] [-q] [-s <statistic_interval>] [-v]+ [-V] [-h]
152152
153153
-a listen_addr Local IPv4/v6 address to bind to. (127.0.0.1)
154154
-p listen_port Local port to bind to. (5053)
@@ -162,7 +162,7 @@ Usage: ./https_dns_proxy [-a <listen_addr>] [-p <listen_port>]
162162
-i polling_interval Optional polling interval of DNS servers.
163163
(Default: 120, Min: 5, Max: 3600)
164164
-4 Force IPv4 hostnames for DNS resolvers non IPv6 networks.
165-
-r resolver_url The HTTPS path to the resolver URL. default: https://dns.google/dns-query
165+
-r resolver_url The HTTPS path to the resolver URL. Default: https://dns.google/dns-query
166166
-t proxy_server Optional HTTP proxy. e.g. socks5://127.0.0.1:1080
167167
Remote name resolution will be used if the protocol
168168
supports it (http, https, socks4a, socks5h), otherwise
@@ -173,18 +173,35 @@ Usage: ./https_dns_proxy [-a <listen_addr>] [-p <listen_port>]
173173
connections.
174174
-x Use HTTP/1.1 instead of HTTP/2. Useful with broken
175175
or limited builds of libcurl. (false)
176+
-q Use HTTP/3 (QUIC) only. (false)
176177
-s statistic_interval Optional statistic printout interval.
177178
(Default: 0, Disabled: 0, Min: 1, Max: 3600)
178-
-v Increase logging verbosity. (INFO)
179+
-v Increase logging verbosity. (Default: error)
180+
Levels: fatal, stats, error, warning, info, debug
181+
Request issues are logged on warning level.
179182
-V Print version and exit.
183+
-h Print help and exit.
184+
```
185+
186+
## Testing
187+
188+
Functional tests can be executed using [Robot Framework](https://robotframework.org/).
189+
190+
dig command is expected to be available.
191+
192+
```
193+
pip3 install robotframework
194+
python3 -m robot.run tests/robot/functional_tests.robot
180195
```
181196

182197
## TODO
183198

184199
* Add some tests.
200+
* Improve IPv6 handling and add automatic fallback to IPv4
185201

186202
## Authors
187203

188204
* Aaron Drew ([email protected]): Original https_dns_proxy.
189205
* Soumya ([github.com/soumya92](https://github.com/soumya92)): RFC 8484 implementation.
206+
* baranyaib90 ([github.com/baranyaib90](https://github.com/baranyaib90)): fixes and improvements.
190207

src/https_client.c

Lines changed: 61 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,60 @@ int https_curl_debug(CURL __attribute__((unused)) * handle, curl_infotype type,
212212
return 0;
213213
}
214214

215+
static const char * http_version_str(const long version) {
216+
switch (version) {
217+
case CURL_HTTP_VERSION_1_0:
218+
return "1.0";
219+
case CURL_HTTP_VERSION_1_1:
220+
return "1.1";
221+
case CURL_HTTP_VERSION_2_0: // fallthrough
222+
case CURL_HTTP_VERSION_2TLS:
223+
return "2";
224+
#ifdef CURL_VERSION_HTTP3
225+
case CURL_HTTP_VERSION_3:
226+
return "3";
227+
#endif
228+
default:
229+
FLOG("Unsupported HTTP version: %d", version);
230+
}
231+
return "UNKNOWN"; // unreachable code
232+
}
233+
234+
static void https_set_request_version(https_client_t *client,
235+
struct https_fetch_ctx *ctx) {
236+
long http_version_int = CURL_HTTP_VERSION_2TLS;
237+
switch (client->opt->use_http_version) {
238+
case 1:
239+
http_version_int = CURL_HTTP_VERSION_1_1;
240+
// fallthrough
241+
case 2:
242+
break;
243+
case 3:
244+
#ifdef CURL_VERSION_HTTP3
245+
http_version_int = CURL_HTTP_VERSION_3;
246+
#endif
247+
break;
248+
default:
249+
FLOG_REQ("Invalid HTTP version: %d", client->opt->use_http_version);
250+
}
251+
DLOG_REQ("Requesting HTTP/%s", http_version_str(http_version_int));
252+
253+
CURLcode easy_code = curl_easy_setopt(ctx->curl, CURLOPT_HTTP_VERSION, http_version_int);
254+
if (easy_code != CURLE_OK) {
255+
ELOG_REQ("Setting HTTP/%s version failed with %d: %s",
256+
http_version_str(http_version_int), easy_code, curl_easy_strerror(easy_code));
257+
258+
if (client->opt->use_http_version == 3) {
259+
ELOG("Try to run application without -q argument!"); // fallback unknown for current request
260+
client->opt->use_http_version = 2;
261+
} else if (client->opt->use_http_version == 2) {
262+
ELOG("Try to run application with -x argument! Falling back to HTTP/1.1 version.");
263+
client->opt->use_http_version = 1;
264+
// TODO: consider CURLMOPT_PIPELINING setting??
265+
}
266+
}
267+
}
268+
215269
static void https_fetch_ctx_init(https_client_t *client,
216270
struct https_fetch_ctx *ctx, const char *url,
217271
const char* data, size_t datalen,
@@ -228,20 +282,7 @@ static void https_fetch_ctx_init(https_client_t *client,
228282

229283
ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_RESOLVE, resolv);
230284

231-
DLOG_REQ("Requesting HTTP/1.1: %d", client->opt->use_http_1_1);
232-
CURLcode easy_code = curl_easy_setopt(ctx->curl, CURLOPT_HTTP_VERSION,
233-
client->opt->use_http_1_1 ?
234-
CURL_HTTP_VERSION_1_1 :
235-
CURL_HTTP_VERSION_2_0);
236-
if (easy_code != CURLE_OK) {
237-
// hopefully errors will be logged once
238-
ELOG_REQ("CURLOPT_HTTP_VERSION error %d: %s",
239-
easy_code, curl_easy_strerror(easy_code));
240-
if (!client->opt->use_http_1_1) {
241-
ELOG("Try to run application with -x argument! Forcing HTTP/1.1 version.");
242-
client->opt->use_http_1_1 = 1;
243-
}
244-
}
285+
https_set_request_version(client, ctx);
245286

246287
if (logging_debug_enabled()) {
247288
ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_VERBOSE, 1L);
@@ -301,7 +342,7 @@ static int https_fetch_ctx_process_response(https_client_t *client,
301342
faulty_response = 0;
302343
break;
303344
case CURLE_WRITE_ERROR:
304-
WLOG_REQ("Response content was too large");
345+
WLOG_REQ("curl request failed with write error (probably response content was too large)");
305346
break;
306347
default:
307348
WLOG_REQ("curl request failed with %d: %s", res, curl_easy_strerror(res));
@@ -322,9 +363,10 @@ static int https_fetch_ctx_process_response(https_client_t *client,
322363
uploaded_bytes > 0) {
323364
WLOG_REQ("Connecting and sending request to resolver was successful, "
324365
"but no response was sent back");
325-
if (client->opt->use_http_1_1) {
366+
if (client->opt->use_http_version == 1) {
326367
// for example Unbound DoH servers does not support HTTP/1.x, only HTTP/2
327-
WLOG("Resolver may not support current HTTP/1.1 protocol version");
368+
WLOG("Resolver may not support current HTTP/%s protocol version",
369+
http_version_str(client->opt->use_http_version));
328370
}
329371
} else {
330372
// in case of HTTP/1.1 this can happen very often depending on DNS query frequency
@@ -404,20 +446,8 @@ static int https_fetch_ctx_process_response(https_client_t *client,
404446
if ((res = curl_easy_getinfo(
405447
ctx->curl, CURLINFO_HTTP_VERSION, &long_resp)) != CURLE_OK) {
406448
ELOG_REQ("CURLINFO_HTTP_VERSION: %s", curl_easy_strerror(res));
407-
} else {
408-
switch (long_resp) {
409-
case CURL_HTTP_VERSION_1_0:
410-
DLOG_REQ("CURLINFO_HTTP_VERSION: 1.0");
411-
break;
412-
case CURL_HTTP_VERSION_1_1:
413-
DLOG_REQ("CURLINFO_HTTP_VERSION: 1.1");
414-
break;
415-
case CURL_HTTP_VERSION_2_0:
416-
DLOG_REQ("CURLINFO_HTTP_VERSION: 2");
417-
break;
418-
default:
419-
DLOG_REQ("CURLINFO_HTTP_VERSION: %d", long_resp);
420-
}
449+
} else if (long_resp != CURL_HTTP_VERSION_NONE) {
450+
DLOG_REQ("CURLINFO_HTTP_VERSION: %s", http_version_str(long_resp));
421451
}
422452
if ((res = curl_easy_getinfo(
423453
ctx->curl, CURLINFO_PROTOCOL, &long_resp)) != CURLE_OK) {

src/main.c

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,22 @@ int main(int argc, char *argv[]) {
226226
// through to errors about use of uninitialized values in our code. :(
227227
curl_global_init(CURL_GLOBAL_DEFAULT);
228228

229+
curl_version_info_data *curl_ver = curl_version_info(CURLVERSION_NOW);
230+
if (!(curl_ver->features & CURL_VERSION_HTTP2)) {
231+
WLOG("HTTP/2 is not supported by current libcurl");
232+
}
233+
#ifdef CURL_VERSION_HTTP3
234+
if (!(curl_ver->features & CURL_VERSION_HTTP3))
235+
{
236+
WLOG("HTTP/3 is not supported by current libcurl");
237+
}
238+
#else
239+
WLOG("HTTP/3 was not available at build time, it will not work at all");
240+
#endif
241+
if (!(curl_ver->features & CURL_VERSION_IPV6)) {
242+
WLOG("IPv6 is not supported by current libcurl");
243+
}
244+
229245
// Note: This calls ev_default_loop(0) which never cleans up.
230246
// valgrind will report a leak. :(
231247
struct ev_loop *loop = EV_DEFAULT;

src/options.c

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
#define O_CLOEXEC 0
1616
#endif
1717

18+
#define DEFAULT_HTTP_VERSION 2
19+
1820
void options_init(struct Options *opt) {
1921
opt->listen_addr = "127.0.0.1";
2022
opt->listen_port = 5053;
@@ -33,13 +35,13 @@ void options_init(struct Options *opt) {
3335
opt->ipv4 = 0;
3436
opt->resolver_url = "https://dns.google/dns-query";
3537
opt->curl_proxy = NULL;
36-
opt->use_http_1_1 = 0;
38+
opt->use_http_version = DEFAULT_HTTP_VERSION;
3739
opt->stats_interval = 0;
3840
}
3941

4042
int options_parse_args(struct Options *opt, int argc, char **argv) {
4143
int c = 0;
42-
while ((c = getopt(argc, argv, "a:c:p:du:g:b:i:4r:e:t:l:vxs:hV")) != -1) {
44+
while ((c = getopt(argc, argv, "a:c:p:du:g:b:i:4r:e:t:l:vxqs:hV")) != -1) {
4345
switch (c) {
4446
case 'a': // listen_addr
4547
opt->listen_addr = optarg;
@@ -82,8 +84,15 @@ int options_parse_args(struct Options *opt, int argc, char **argv) {
8284
opt->loglevel--;
8385
}
8486
break;
85-
case 'x': // http/1.1
86-
opt->use_http_1_1 = 1;
87+
case 'x': // http/1.1 fallthrough
88+
case 'q': // http/3
89+
if (opt->use_http_version == DEFAULT_HTTP_VERSION) {
90+
opt->use_http_version = (c == 'x' ? 1 : 3);
91+
} else {
92+
printf("HTTP version already set to: HTTP/%s\n",
93+
opt->use_http_version == 1 ? "1.1" : "3");
94+
return -1;
95+
}
8796
break;
8897
case 's': // stats interval
8998
opt->stats_interval = atoi(optarg);
@@ -193,6 +202,7 @@ void options_show_usage(int __attribute__((unused)) argc, char **argv) {
193202
printf(" connections.\n");
194203
printf(" -x Use HTTP/1.1 instead of HTTP/2. Useful with broken\n"
195204
" or limited builds of libcurl. (false)\n");
205+
printf(" -q Use HTTP/3 (QUIC) only. (false)\n");
196206
printf(" -s statistic_interval Optional statistic printout interval.\n"\
197207
" (Default: %d, Disabled: 0, Min: 1, Max: 3600)\n",
198208
defaults.stats_interval);

src/options.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,10 @@ struct Options {
4141
// e.g. "socks5://127.0.0.1:1080"
4242
const char *curl_proxy;
4343

44-
// Hack to fix OpenWRT issues due to dropping of HTTP/2 support from libcurl.
45-
int use_http_1_1;
44+
// 1 = Use only HTTP/1.1 for limited OpenWRT libcurl (which is not built with HTTP/2 support)
45+
// 2 = Use only HTTP/2 default
46+
// 3 = Use only HTTP/3 QUIC
47+
int use_http_version;
4648

4749
// Print statistic interval
4850
int stats_interval;

0 commit comments

Comments
 (0)