Skip to content

Commit 67ecae0

Browse files
authored
Add -S flag for configurable source address on outbound connections (#196)
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
1 parent b499aa9 commit 67ecae0

File tree

5 files changed

+25
-3
lines changed

5 files changed

+25
-3
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ Just run it as a daemon and point traffic at it. Commandline flags are:
161161
```
162162
Usage: ./https_dns_proxy [-a <listen_addr>] [-p <listen_port>] [-T <tcp_client_limit>]
163163
[-b <dns_servers>] [-i <polling_interval>] [-4]
164-
[-r <resolver_url>] [-t <proxy_server>] [-x] [-q] [-C <ca_path>] [-c <dscp_codepoint>]
164+
[-r <resolver_url>] [-t <proxy_server>] [-S <source_addr>] [-x] [-q] [-C <ca_path>] [-c <dscp_codepoint>]
165165
[-d] [-u <user>] [-g <group>]
166166
[-v]+ [-l <logfile>] [-s <statistic_interval>] [-F <log_limit>] [-V] [-h]
167167
@@ -187,6 +187,8 @@ Usage: ./https_dns_proxy [-a <listen_addr>] [-p <listen_port>] [-T <tcp_client_l
187187
supports it (http, https, socks4a, socks5h), otherwise
188188
initial DNS resolution will still be done via the
189189
bootstrap DNS servers.
190+
-S source_addr Source IPv4/v6 address for outbound HTTPS connections.
191+
(Default: system default)
190192
-x Use HTTP/1.1 instead of HTTP/2. Useful with broken
191193
or limited builds of libcurl.
192194
-q Use HTTP/3 (QUIC) only.

src/https_client.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,10 @@ static void https_fetch_ctx_init(https_client_t *client,
320320
DLOG_REQ("Using curl proxy: %s", client->opt->curl_proxy);
321321
ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_PROXY, client->opt->curl_proxy);
322322
}
323+
if (client->opt->source_addr) {
324+
DLOG_REQ("Using source address: %s", client->opt->source_addr);
325+
ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_INTERFACE, client->opt->source_addr);
326+
}
323327
if (client->opt->ca_info) {
324328
ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_CAINFO, client->opt->ca_info);
325329
}

src/options.c

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ void options_init(struct Options *opt) {
3939
opt->ipv4 = 0;
4040
opt->resolver_url = "https://dns.google/dns-query";
4141
opt->curl_proxy = NULL;
42+
opt->source_addr = NULL;
4243
opt->use_http_version = DEFAULT_HTTP_VERSION;
4344
opt->max_idle_time = 118;
4445
opt->conn_loss_time = 15;
@@ -58,7 +59,7 @@ int parse_int(char * str) {
5859

5960
enum OptionsParseResult options_parse_args(struct Options *opt, int argc, char **argv) {
6061
int c = 0;
61-
while ((c = getopt(argc, argv, "a:c:p:T:du:g:b:i:4r:e:t:l:vxqm:L:s:C:F:hV")) != -1) {
62+
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) {
6263
switch (c) {
6364
case 'a': // listen_addr
6465
opt->listen_addr = optarg;
@@ -123,6 +124,9 @@ enum OptionsParseResult options_parse_args(struct Options *opt, int argc, char *
123124
case 's': // stats interval
124125
opt->stats_interval = parse_int(optarg);
125126
break;
127+
case 'S': // source address
128+
opt->source_addr = optarg;
129+
break;
126130
case 'C': // CA info
127131
opt->ca_info = optarg;
128132
break;
@@ -222,7 +226,7 @@ void options_show_usage(int __attribute__((unused)) argc, char **argv) {
222226
options_init(&defaults);
223227
printf("Usage: %s [-a <listen_addr>] [-p <listen_port>] [-T <tcp_client_limit>]\n", argv[0]);
224228
printf(" [-b <dns_servers>] [-i <polling_interval>] [-4]\n");
225-
printf(" [-r <resolver_url>] [-t <proxy_server>] [-x] [-q] [-C <ca_path>] [-c <dscp_codepoint>]\n");
229+
printf(" [-r <resolver_url>] [-t <proxy_server>] [-S <source_addr>] [-x] [-q] [-C <ca_path>] [-c <dscp_codepoint>]\n");
226230
printf(" [-d] [-u <user>] [-g <group>] \n");
227231
printf(" [-v]+ [-l <logfile>] [-s <statistic_interval>] [-F <log_limit>] [-V] [-h]\n");
228232
printf("\n DNS server\n");
@@ -250,6 +254,8 @@ void options_show_usage(int __attribute__((unused)) argc, char **argv) {
250254
printf(" supports it (http, https, socks4a, socks5h), otherwise\n");
251255
printf(" initial DNS resolution will still be done via the\n");
252256
printf(" bootstrap DNS servers.\n");
257+
printf(" -S source_addr Source IPv4/v6 address for outbound HTTPS connections.\n");
258+
printf(" (Default: system default)\n");
253259
printf(" -x Use HTTP/1.1 instead of HTTP/2. Useful with broken\n"
254260
" or limited builds of libcurl.\n");
255261
printf(" -q Use HTTP/3 (QUIC) only.\n");

src/options.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ struct Options {
4343
// e.g. "socks5://127.0.0.1:1080"
4444
const char *curl_proxy;
4545

46+
// Source address for outbound HTTPS connections
47+
const char *source_addr;
48+
4649
// 1 = Use only HTTP/1.1 for limited OpenWRT libcurl (which is not built with HTTP/2 support)
4750
// 2 = Use only HTTP/2 default
4851
// 3 = Use only HTTP/3 QUIC

tests/robot/functional_tests.robot

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,3 +201,10 @@ Truncate UDP Impossible
201201
Wait Until Keyword Succeeds 5x 200ms
202202
# the only TXT answer record has to be dropped to met limit
203203
... Verify Truncation txtfill4096.test.dnscheck.tools 4096 12 100 ANSWER: 0
204+
205+
Source Address Binding
206+
[Documentation] Test source address binding with -S flag
207+
${eth0_ip} = Run ip -4 addr show eth0 | grep inet | awk '{print $2}' | cut -d/ -f1 | tr -d '\\n'
208+
Start Proxy -S ${eth0_ip}
209+
Set To Dictionary ${expected_logs} Using source address=1
210+
Run Dig

0 commit comments

Comments
 (0)