Skip to content

Add -S flag for configurable source address on outbound connections#196

Merged
aarond10 merged 1 commit intoaarond10:masterfrom
karl82:master
Dec 30, 2025
Merged

Add -S flag for configurable source address on outbound connections#196
aarond10 merged 1 commit intoaarond10:masterfrom
karl82:master

Conversation

@karl82
Copy link
Contributor

@karl82 karl82 commented Nov 23, 2025

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)

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)

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

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
@karl82 karl82 marked this pull request as ready for review November 23, 2025 18:26
@karl82
Copy link
Contributor Author

karl82 commented Nov 23, 2025

Once this is approved and merged, I will prepare changes for https://docs.openwrt.melmac.ca/https-dns-proxy and https://github.com/stangri/luci-app-https-dns-proxy cc @stangri


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'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if machines where these tests are running will always have eth0.

@stangri
Copy link
Contributor

stangri commented Nov 23, 2025

@karl82 great idea! Let me know if you want me to update makefile/init script of the principal package and/or luci app/docs when this is merged.

@karl82
Copy link
Contributor Author

karl82 commented Nov 27, 2025

Hey @aarond10 , is this something interesting for https_dns_proxy?

@stangri
Copy link
Contributor

stangri commented Nov 29, 2025

@karl82 it may take some time for maintainers to respond, especially around holidays, please be patient. ;)

@karl82
Copy link
Contributor Author

karl82 commented Dec 26, 2025

Hey @aarond10 , is this something interesting for https_dns_proxy?

@aarond10 I'm sorry to bother, but this functionality would be really helpful for me. Is there anything I should improve on change? Does it meet standards?

@aarond10 aarond10 merged commit 67ecae0 into aarond10:master Dec 30, 2025
karl82 added a commit to karl82/https_dns_proxy that referenced this pull request Feb 9, 2026
Motivation:
-----------
PR aarond10#196 added the `-S` flag to bind HTTPS connections to a source address, enabling policy-based routing. However, bootstrap DNS queries (used to resolve DoH server hostnames like "dns.google") were not bound to the source address.

This caused two issues:
1. **Privacy leak**: Bootstrap DNS queries go via default route (local ISP), exposing which DoH server you're using
2. **Routing mismatch**: HTTPS connection routes via VPN but may fail if resolved IP is unreachable from VPN

Implementation:
---------------
- Bind bootstrap DNS queries using `ares_set_local_ip4()` and `ares_set_local_ip6()` from c-ares
- Validate address family matches proxy mode (`-4`/`-6`), warn on mismatch
- Warn on invalid address literals
- Robot Framework tests for source binding and validation warnings
- Docker-based test infrastructure for CI/CD and macOS development

Example Usage:
--------------
```bash
https_dns_proxy -S 192.168.12.1 -b 1.1.1.1,8.8.8.8 -r https://dns.google/dns-query
```

With PBR rules routing traffic from source 192.168.12.1 via VPN:

```text
# Route DoH HTTPS (port 443) via VPN
config policy
	option name 'DoH WA via wg_wa'
	option interface 'wg_wa'
	option chain 'output'
	option proto 'tcp'
	option src_addr '192.168.12.1'
	option dest_port '443'

# Route bootstrap DNS (port 53) via VPN
config policy
	option name 'Bootstrap DNS WA via wg_wa'
	option interface 'wg_wa'
	option chain 'output'
	option proto 'udp'
	option src_addr '192.168.12.1'
	option dest_port '53'
	option dest_addr '1.1.1.1 8.8.8.8'
```

Both rules now match because `-S` binds both HTTPS and bootstrap DNS to the same source address.

Verification:
-------------
Bootstrap DNS bound to source address:
```
[I] dns_poller.c:163 Using source address: 192.168.12.1
[I] dns_poller.c:208 Received new DNS server IP: 142.250.80.110 for dns.google
```

Warning on address family mismatch:
```
[W] dns_poller.c:133 Bootstrap source address '::1' is IPv6, but IPv4-only mode is set
```

Warning on invalid address:
```
[W] dns_poller.c:141 Bootstrap source address 'not-an-ip' is not a valid IP literal
```

Files Modified:
---------------
- `src/dns_poller.c`: Added `set_bootstrap_source_addr()` function
- `src/dns_poller.h`: Added source_addr parameter to poller init
- `src/main.c`: Pass source_addr to dns_poller
- `tests/robot/functional_tests.robot`: Source binding and validation tests
- `tests/docker/Dockerfile`: Test image with dependencies
- `tests/docker/run_all_tests.sh`: Full test suite runner
- `tests/docker/bootstrap_dns_test.sh`: Quick bootstrap DNS test
karl82 added a commit to karl82/https_dns_proxy that referenced this pull request Feb 9, 2026
Motivation:
-----------
PR aarond10#196 added the `-S` flag to bind HTTPS connections to a source address, enabling policy-based routing. However, bootstrap DNS queries (used to resolve DoH server hostnames like "dns.google") were not bound to the source address.

This caused two issues:
1. **Privacy leak**: Bootstrap DNS queries go via default route (local ISP), exposing which DoH server you're using
2. **Routing mismatch**: HTTPS connection routes via VPN but may fail if resolved IP is unreachable from VPN

Implementation:
---------------
- Bind bootstrap DNS queries using `ares_set_local_ip4()` and `ares_set_local_ip6()` from c-ares
- Validate address family matches proxy mode (`-4`/`-6`), warn on mismatch
- Warn on invalid address literals
- Robot Framework tests for source binding and validation warnings
- Docker-based test infrastructure for CI/CD and macOS development

Example Usage:
--------------
```bash
https_dns_proxy -S 192.168.12.1 -b 1.1.1.1,8.8.8.8 -r https://dns.google/dns-query
```

With PBR rules routing traffic from source 192.168.12.1 via VPN:

```text
# Route DoH HTTPS (port 443) via VPN
config policy
	option name 'DoH WA via wg_wa'
	option interface 'wg_wa'
	option chain 'output'
	option proto 'tcp'
	option src_addr '192.168.12.1'
	option dest_port '443'

# Route bootstrap DNS (port 53) via VPN
config policy
	option name 'Bootstrap DNS WA via wg_wa'
	option interface 'wg_wa'
	option chain 'output'
	option proto 'udp'
	option src_addr '192.168.12.1'
	option dest_port '53'
	option dest_addr '1.1.1.1 8.8.8.8'
```

Both rules now match because `-S` binds both HTTPS and bootstrap DNS to the same source address.

Verification:
-------------
Bootstrap DNS bound to source address:
```
[I] dns_poller.c:163 Using source address: 192.168.12.1
[I] dns_poller.c:208 Received new DNS server IP: 142.250.80.110 for dns.google
```

Warning on address family mismatch:
```
[W] dns_poller.c:133 Bootstrap source address '::1' is IPv6, but IPv4-only mode is set
```

Warning on invalid address:
```
[W] dns_poller.c:141 Bootstrap source address 'not-an-ip' is not a valid IP literal
```

Files Modified:
---------------
- `src/dns_poller.c`: Added `set_bootstrap_source_addr()` function
- `src/dns_poller.h`: Added source_addr parameter to poller init
- `src/main.c`: Pass source_addr to dns_poller
- `tests/robot/functional_tests.robot`: Source binding and validation tests
- `tests/docker/Dockerfile`: Test image with dependencies
- `tests/docker/run_all_tests.sh`: Full test suite runner
- `tests/docker/bootstrap_dns_test.sh`: Quick bootstrap DNS test
karl82 added a commit to karl82/https_dns_proxy that referenced this pull request Feb 9, 2026
Motivation:
-----------
PR aarond10#196 added the `-S` flag to bind HTTPS connections to a source address, enabling policy-based routing. However, bootstrap DNS queries (used to resolve DoH server hostnames like "dns.google") were not bound to the source address.

This caused two issues:
1. **Privacy leak**: Bootstrap DNS queries go via default route (local ISP), exposing which DoH server you're using
2. **Routing mismatch**: HTTPS connection routes via VPN but may fail if resolved IP is unreachable from VPN

Implementation:
---------------
- Bind bootstrap DNS queries using `ares_set_local_ip4()` and `ares_set_local_ip6()` from c-ares
- Validate address family matches proxy mode (`-4`/`-6`), warn on mismatch
- Warn on invalid address literals
- Robot Framework tests for source binding and validation warnings
- Docker-based test infrastructure for CI/CD and macOS development

Example Usage:
--------------
```bash
https_dns_proxy -S 192.168.12.1 -b 1.1.1.1,8.8.8.8 -r https://dns.google/dns-query
```

With PBR rules routing traffic from source 192.168.12.1 via VPN:

```text
# Route DoH HTTPS (port 443) via VPN
config policy
	option name 'DoH WA via wg_wa'
	option interface 'wg_wa'
	option chain 'output'
	option proto 'tcp'
	option src_addr '192.168.12.1'
	option dest_port '443'

# Route bootstrap DNS (port 53) via VPN
config policy
	option name 'Bootstrap DNS WA via wg_wa'
	option interface 'wg_wa'
	option chain 'output'
	option proto 'udp'
	option src_addr '192.168.12.1'
	option dest_port '53'
	option dest_addr '1.1.1.1 8.8.8.8'
```

Both rules now match because `-S` binds both HTTPS and bootstrap DNS to the same source address.

Verification:
-------------
Bootstrap DNS bound to source address:
```
[I] dns_poller.c:163 Using source address: 192.168.12.1
[I] dns_poller.c:208 Received new DNS server IP: 142.250.80.110 for dns.google
```

Warning on address family mismatch:
```
[W] dns_poller.c:133 Bootstrap source address '::1' is IPv6, but IPv4-only mode is set
```

Warning on invalid address:
```
[W] dns_poller.c:141 Bootstrap source address 'not-an-ip' is not a valid IP literal
```

Files Modified:
---------------
- `src/dns_poller.c`: Added `set_bootstrap_source_addr()` function
- `src/dns_poller.h`: Added source_addr parameter to poller init
- `src/main.c`: Pass source_addr to dns_poller
- `tests/robot/functional_tests.robot`: Source binding and validation tests
- `tests/docker/Dockerfile`: Test image with dependencies
- `tests/docker/run_all_tests.sh`: Full test suite runner
karl82 added a commit to karl82/https_dns_proxy that referenced this pull request Feb 12, 2026
Motivation:
-----------
PR aarond10#196 added the `-S` flag to bind HTTPS connections to a source address, enabling policy-based routing. However, bootstrap DNS queries (used to resolve DoH server hostnames like "dns.google") were not bound to the source address.

This caused two issues:
1. **Privacy leak**: Bootstrap DNS queries go via default route (local ISP), exposing which DoH server you're using
2. **Routing mismatch**: HTTPS connection routes via VPN but may fail if resolved IP is unreachable from VPN

Implementation:
---------------
- Bind bootstrap DNS queries using `ares_set_local_ip4()` and `ares_set_local_ip6()` from c-ares
- Validate address family matches proxy mode (`-4`/`-6`), warn on mismatch
- Warn on invalid address literals
- Robot Framework tests for source binding and validation warnings
- Docker-based test infrastructure for CI/CD and macOS development

Example Usage:
--------------
```bash
https_dns_proxy -S 192.168.12.1 -b 1.1.1.1,8.8.8.8 -r https://dns.google/dns-query
```

With PBR rules routing traffic from source 192.168.12.1 via VPN:

```text
# Route DoH HTTPS (port 443) via VPN
config policy
	option name 'DoH WA via wg_wa'
	option interface 'wg_wa'
	option chain 'output'
	option proto 'tcp'
	option src_addr '192.168.12.1'
	option dest_port '443'

# Route bootstrap DNS (port 53) via VPN
config policy
	option name 'Bootstrap DNS WA via wg_wa'
	option interface 'wg_wa'
	option chain 'output'
	option proto 'udp'
	option src_addr '192.168.12.1'
	option dest_port '53'
	option dest_addr '1.1.1.1 8.8.8.8'
```

Both rules now match because `-S` binds both HTTPS and bootstrap DNS to the same source address.

Verification:
-------------
Bootstrap DNS bound to source address:
```
[I] dns_poller.c:163 Using source address: 192.168.12.1
[I] dns_poller.c:208 Received new DNS server IP: 142.250.80.110 for dns.google
```

Warning on address family mismatch:
```
[W] dns_poller.c:133 Bootstrap source address '::1' is IPv6, but IPv4-only mode is set
```

Warning on invalid address:
```
[W] dns_poller.c:141 Bootstrap source address 'not-an-ip' is not a valid IP literal
```

Files Modified:
---------------
- `src/dns_poller.c`: Added `set_bootstrap_source_addr()` function
- `src/dns_poller.h`: Added source_addr parameter to poller init
- `src/main.c`: Pass source_addr to dns_poller
- `tests/robot/functional_tests.robot`: Source binding and validation tests
- `tests/docker/Dockerfile`: Test image with dependencies
- `tests/docker/run_all_tests.sh`: Full test suite runner
karl82 added a commit to karl82/https_dns_proxy that referenced this pull request Feb 12, 2026
Motivation:
-----------
PR aarond10#196 added the `-S` flag to bind HTTPS connections to a source address, enabling policy-based routing. However, bootstrap DNS queries (used to resolve DoH server hostnames like "dns.google") were not bound to the source address.

This caused two issues:
1. **Privacy leak**: Bootstrap DNS queries go via default route (local ISP), exposing which DoH server you're using
2. **Routing mismatch**: HTTPS connection routes via VPN but may fail if resolved IP is unreachable from VPN

Implementation:
---------------
- Bind bootstrap DNS queries using `ares_set_local_ip4()` and `ares_set_local_ip6()` from c-ares
- Validate address family matches proxy mode (`-4`/`-6`), warn on mismatch
- Warn on invalid address literals
- Robot Framework tests for source binding and validation warnings
- Docker-based test infrastructure for CI/CD and macOS development

Example Usage:
--------------
```bash
https_dns_proxy -S 192.168.12.1 -b 1.1.1.1,8.8.8.8 -r https://dns.google/dns-query
```

With PBR rules routing traffic from source 192.168.12.1 via VPN:

```text
# Route DoH HTTPS (port 443) via VPN
config policy
	option name 'DoH WA via wg_wa'
	option interface 'wg_wa'
	option chain 'output'
	option proto 'tcp'
	option src_addr '192.168.12.1'
	option dest_port '443'

# Route bootstrap DNS (port 53) via VPN
config policy
	option name 'Bootstrap DNS WA via wg_wa'
	option interface 'wg_wa'
	option chain 'output'
	option proto 'udp'
	option src_addr '192.168.12.1'
	option dest_port '53'
	option dest_addr '1.1.1.1 8.8.8.8'
```

Both rules now match because `-S` binds both HTTPS and bootstrap DNS to the same source address.

Verification:
-------------
Bootstrap DNS bound to source address:
```
[I] dns_poller.c:163 Using source address: 192.168.12.1
[I] dns_poller.c:208 Received new DNS server IP: 142.250.80.110 for dns.google
```

Warning on address family mismatch:
```
[W] dns_poller.c:133 Bootstrap source address '::1' is IPv6, but IPv4-only mode is set
```

Warning on invalid address:
```
[W] dns_poller.c:141 Bootstrap source address 'not-an-ip' is not a valid IP literal
```

Files Modified:
---------------
- `src/dns_poller.c`: Added `set_bootstrap_source_addr()` function
- `src/dns_poller.h`: Added source_addr parameter to poller init
- `src/main.c`: Pass source_addr to dns_poller
- `src/options.c`: Fix format string type
- `tests/robot/functional_tests.robot`: Source binding and validation tests
- `tests/docker/Dockerfile`: Test image with valgrind and ctest integration
- `tests/docker/run_all_tests.sh`: Simplified test runner using Dockerfile CMD
- `CMakeLists.txt`: Fix robot test WORKING_DIRECTORY, add distclean target
- `README.md`: Update Docker test documentation
- `.gitignore`: Add build/ directory
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants