From c79c9ab73798b22f052040f14e6e9a6ad35896d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karel=20Rank=F0=9F=A4=96?= Date: Sun, 23 Nov 2025 18:25:06 +0000 Subject: [PATCH] Add -S flag for configurable source address on outbound connections Motivation: ----------- Enable routing DNS-over-HTTPS traffic through different network paths per instance. Primary use case: running multiple `https_dns_proxy` instances on a router where each WiFi LAN gateway routes through a different WireGuard tunnel to different geographic locations. This allows DNS traffic from different WiFi networks to exit via different VPN endpoints. Implementation: --------------- - Added `source_addr` field to `struct Options` - New `-S` command-line flag to specify source IPv4/v6 address - Uses `CURLOPT_INTERFACE` to bind outbound HTTPS connections - Backward compatible: without -S, uses system default routing - Logs `Using source address: X` at `debug` level when configured Example Usage: -------------- ### Instance 1: WiFi LAN 1 gateway (routes via WireGuard to US) ```shell https_dns_proxy -a 192.168.1.1 -p 53 -S 192.168.1.1 \ -r https://security.cloudflare-dns.com/dns-query \ -b 1.1.1.2,1.0.0.2 ``` ### Instance 2: WiFi LAN 2 gateway (routes via WireGuard to EU) ```shell https_dns_proxy -a 192.168.2.1 -p 53 -S 192.168.2.1 \ -r https://security.cloudflare-dns.com/dns-query \ -b 1.1.1.2,1.0.0.2 ``` Each instance binds to its WiFi interface address for both listening and outbound HTTPS, ensuring traffic routes through the correct WireGuard tunnel configured for that interface. Verification: ------------- With `-S` flag, CURL binds to specified source address: ``` [D] https_client.c:260 F0C1: Requesting HTTP/2 [D] https_client.c:324 F0C1: Using source address: 192.168.1.1 [D] https_client.c:218 F0C1: * Added security.cloudflare-dns.com:443:1.0.0.2,1.1.1.2,... to DNS cache [D] https_client.c:218 F0C1: * Hostname security.cloudflare-dns.com was found in DNS cache [D] https_client.c:94 curl opened socket: 9 [D] https_client.c:218 F0C1: * Trying 1.0.0.2:443... [D] https_client.c:218 F0C1: * Name '192.168.1.1' family 2 resolved to '192.168.1.1' family 2 [D] https_client.c:218 F0C1: * Local port: 0 [D] https_client.c:639 Reserved new io event: 0xffffc0ed3568 [D] https_client.c:218 F0C1: * Connected to security.cloudflare-dns.com (1.0.0.2) port 443 (#0) ``` Without `-S` flag, no source binding (backward compatible): ``` [D] https_client.c:260 39BF: Requesting HTTP/2 [D] https_client.c:218 39BF: * Added security.cloudflare-dns.com:443:1.1.1.2,1.0.0.2,... to DNS cache [D] https_client.c:218 39BF: * Hostname security.cloudflare-dns.com was found in DNS cache [D] https_client.c:94 curl opened socket: 9 [D] https_client.c:218 39BF: * Trying 1.1.1.2:443... [D] https_client.c:639 Reserved new io event: 0xffffe69a0f18 [D] https_client.c:218 39BF: * Connected to security.cloudflare-dns.com (1.1.1.2) port 443 (#0) ``` Note the presence of `Using source address` and `Name '192.168.1.1' ... resolved` lines only when `-S` is specified. Files Modified: --------------- - `src/options.h`: Added source_addr field - `src/options.c`: Added -S flag parsing and help text - `src/https_client.c`: Implemented CURLOPT_INTERFACE binding - `tests/robot/functional_tests.robot`: Added test case - `README.md`: Updated documentation --- README.md | 4 +++- src/https_client.c | 4 ++++ src/options.c | 10 ++++++++-- src/options.h | 3 +++ tests/robot/functional_tests.robot | 7 +++++++ 5 files changed, 25 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 08573af..4cc2790 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ Just run it as a daemon and point traffic at it. Commandline flags are: ``` Usage: ./https_dns_proxy [-a ] [-p ] [-T ] [-b ] [-i ] [-4] - [-r ] [-t ] [-x] [-q] [-C ] [-c ] + [-r ] [-t ] [-S ] [-x] [-q] [-C ] [-c ] [-d] [-u ] [-g ] [-v]+ [-l ] [-s ] [-F ] [-V] [-h] @@ -187,6 +187,8 @@ Usage: ./https_dns_proxy [-a ] [-p ] [-T opt->curl_proxy); ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_PROXY, client->opt->curl_proxy); } + if (client->opt->source_addr) { + DLOG_REQ("Using source address: %s", client->opt->source_addr); + ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_INTERFACE, client->opt->source_addr); + } if (client->opt->ca_info) { ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_CAINFO, client->opt->ca_info); } diff --git a/src/options.c b/src/options.c index fa003dc..3630d11 100644 --- a/src/options.c +++ b/src/options.c @@ -39,6 +39,7 @@ void options_init(struct Options *opt) { opt->ipv4 = 0; opt->resolver_url = "https://dns.google/dns-query"; opt->curl_proxy = NULL; + opt->source_addr = NULL; opt->use_http_version = DEFAULT_HTTP_VERSION; opt->max_idle_time = 118; opt->conn_loss_time = 15; @@ -58,7 +59,7 @@ int parse_int(char * str) { enum OptionsParseResult options_parse_args(struct Options *opt, int argc, char **argv) { int c = 0; - while ((c = getopt(argc, argv, "a:c:p:T:du:g:b:i:4r:e:t:l:vxqm:L: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:S:C:F:hV")) != -1) { switch (c) { case 'a': // listen_addr opt->listen_addr = optarg; @@ -123,6 +124,9 @@ enum OptionsParseResult options_parse_args(struct Options *opt, int argc, char * case 's': // stats interval opt->stats_interval = parse_int(optarg); break; + case 'S': // source address + opt->source_addr = optarg; + break; case 'C': // CA info opt->ca_info = optarg; break; @@ -222,7 +226,7 @@ void options_show_usage(int __attribute__((unused)) argc, char **argv) { options_init(&defaults); printf("Usage: %s [-a ] [-p ] [-T ]\n", argv[0]); printf(" [-b ] [-i ] [-4]\n"); - printf(" [-r ] [-t ] [-x] [-q] [-C ] [-c ]\n"); + printf(" [-r ] [-t ] [-S ] [-x] [-q] [-C ] [-c ]\n"); printf(" [-d] [-u ] [-g ] \n"); printf(" [-v]+ [-l ] [-s ] [-F ] [-V] [-h]\n"); printf("\n DNS server\n"); @@ -250,6 +254,8 @@ void options_show_usage(int __attribute__((unused)) argc, char **argv) { printf(" supports it (http, https, socks4a, socks5h), otherwise\n"); printf(" initial DNS resolution will still be done via the\n"); printf(" bootstrap DNS servers.\n"); + printf(" -S source_addr Source IPv4/v6 address for outbound HTTPS connections.\n"); + printf(" (Default: system default)\n"); printf(" -x Use HTTP/1.1 instead of HTTP/2. Useful with broken\n" " or limited builds of libcurl.\n"); printf(" -q Use HTTP/3 (QUIC) only.\n"); diff --git a/src/options.h b/src/options.h index 9320b50..6d0a9d3 100644 --- a/src/options.h +++ b/src/options.h @@ -43,6 +43,9 @@ struct Options { // e.g. "socks5://127.0.0.1:1080" const char *curl_proxy; + // Source address for outbound HTTPS connections + const char *source_addr; + // 1 = Use only HTTP/1.1 for limited OpenWRT libcurl (which is not built with HTTP/2 support) // 2 = Use only HTTP/2 default // 3 = Use only HTTP/3 QUIC diff --git a/tests/robot/functional_tests.robot b/tests/robot/functional_tests.robot index 7241f5c..84e2cf1 100644 --- a/tests/robot/functional_tests.robot +++ b/tests/robot/functional_tests.robot @@ -201,3 +201,10 @@ Truncate UDP Impossible 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 + +Source Address Binding + [Documentation] Test source address binding with -S flag + ${eth0_ip} = Run ip -4 addr show eth0 | grep inet | awk '{print $2}' | cut -d/ -f1 | tr -d '\\n' + Start Proxy -S ${eth0_ip} + Set To Dictionary ${expected_logs} Using source address=1 + Run Dig