Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/cmake.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ jobs:
- name: Test
run: make -C ${{github.workspace}}/ test ARGS="--verbose"

- name: Test Linux interface binding
run: sudo env HDP_RUN_PRIVILEGED_TEST=1 ctest --test-dir "${{github.workspace}}" -R '^outbound_interface_linux$' --output-on-failure --no-tests=error

- uses: actions/upload-artifact@v7
if: ${{ success() || failure() }}
with:
Expand Down
66 changes: 66 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ if(BUILD_TESTING)
add_executable(dns_poller_watchers_test
tests/unit/test_dns_poller.c
src/logging.c
src/outbound_interface.c
src/ring_buffer.c)
set_property(SOURCE tests/unit/test_dns_poller.c APPEND
PROPERTY COMPILE_DEFINITIONS __FILENAME__="dns_poller_test")
Expand All @@ -225,6 +226,71 @@ if(BUILD_TESTING)
add_test(NAME dns_truncate COMMAND dns_truncate_test)
set_tests_properties(dns_truncate PROPERTIES LABELS unit)

add_executable(dns_listener_tcp_test
tests/unit/test_dns_listener_tcp.c
src/logging.c
src/ring_buffer.c)
set_property(SOURCE tests/unit/test_dns_listener_tcp.c APPEND
PROPERTY COMPILE_DEFINITIONS __FILENAME__="dns_listener_tcp_test")
target_link_libraries(dns_listener_tcp_test ev)
set_property(TARGET dns_listener_tcp_test PROPERTY C_STANDARD 11)
add_test(NAME dns_listener_tcp COMMAND dns_listener_tcp_test)
set_tests_properties(dns_listener_tcp PROPERTIES LABELS unit TIMEOUT 20)

add_executable(https_client_limits_test
tests/unit/test_https_client_limits.c
src/logging.c
src/outbound_interface.c
src/ring_buffer.c
src/stat.c)
set_property(SOURCE tests/unit/test_https_client_limits.c APPEND
PROPERTY COMPILE_DEFINITIONS __FILENAME__="https_client_limits_test")
target_link_libraries(https_client_limits_test curl ev m)
set_property(TARGET https_client_limits_test PROPERTY C_STANDARD 11)
add_test(NAME https_client_limits COMMAND https_client_limits_test)
set_tests_properties(https_client_limits PROPERTIES LABELS unit)

add_executable(doh_proxy_resolver_test
tests/unit/test_doh_proxy_resolver.c
src/logging.c
src/ring_buffer.c)
set_property(SOURCE tests/unit/test_doh_proxy_resolver.c APPEND
PROPERTY COMPILE_DEFINITIONS __FILENAME__="doh_proxy_resolver_test")
target_link_libraries(doh_proxy_resolver_test curl ev)
set_property(TARGET doh_proxy_resolver_test PROPERTY C_STANDARD 11)
add_test(NAME doh_proxy_resolver COMMAND doh_proxy_resolver_test)
set_tests_properties(doh_proxy_resolver PROPERTIES LABELS unit)

add_executable(outbound_interface_test
tests/unit/test_outbound_interface.c)
set_property(SOURCE tests/unit/test_outbound_interface.c APPEND
PROPERTY COMPILE_DEFINITIONS __FILENAME__="outbound_interface_test")
set_property(TARGET outbound_interface_test PROPERTY C_STANDARD 11)
add_test(NAME outbound_interface COMMAND outbound_interface_test)
set_tests_properties(outbound_interface PROPERTIES LABELS unit)

add_executable(options_test
tests/unit/test_options.c)
set_property(SOURCE tests/unit/test_options.c APPEND
PROPERTY COMPILE_DEFINITIONS __FILENAME__="options_test")
set_property(TARGET options_test PROPERTY C_STANDARD 11)
add_test(NAME options COMMAND options_test)
set_tests_properties(options PROPERTIES LABELS unit)

if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
add_executable(outbound_interface_linux_test
tests/linux/test_outbound_interface_linux.c
src/outbound_interface.c)
target_include_directories(outbound_interface_linux_test PRIVATE tests/unit)
set_property(SOURCE tests/linux/test_outbound_interface_linux.c APPEND
PROPERTY COMPILE_DEFINITIONS __FILENAME__="outbound_interface_linux_test")
set_property(TARGET outbound_interface_linux_test PROPERTY C_STANDARD 11)
add_test(NAME outbound_interface_linux COMMAND outbound_interface_linux_test)
set_tests_properties(outbound_interface_linux PROPERTIES
SKIP_RETURN_CODE 77
LABELS linux-privileged)
endif()

find_program(VALGRIND_EXE NAMES valgrind)
if(VALGRIND_EXE)
add_test(NAME dns_truncate_valgrind
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ Just run it as a daemon and point traffic at it. Commandline flags are:
Usage: ./https_dns_proxy [-a <listen_addr>] [-p <listen_port>] [-T <tcp_client_limit>]
[-b <dns_servers>] [-i <polling_interval>] [-4]
[-r <resolver_url>] [-t <proxy_server>] [-S <source_addr>] [-x] [-q] [-C <ca_path>] [-c <dscp_codepoint>]
[--outbound-interface <interface>]
[-d] [-u <user>] [-g <group>]
[-v]+ [-l <logfile>] [-s <statistic_interval>] [-F <log_limit>] [-V] [-h]

Expand Down Expand Up @@ -189,6 +190,9 @@ Usage: ./https_dns_proxy [-a <listen_addr>] [-p <listen_port>] [-T <tcp_client_l
bootstrap DNS servers.
-S source_addr Source IPv4/v6 address for outbound HTTPS and bootstrap DNS.
(Default: system default)
-I, --outbound-interface interface
Linux network interface for outbound HTTPS and bootstrap DNS.
Incompatible with -u; requires CAP_NET_RAW.
-x Use HTTP/1.1 instead of HTTP/2. Useful with broken
or limited builds of libcurl.
-q Use HTTP/3 (QUIC) only.
Expand All @@ -204,6 +208,7 @@ Usage: ./https_dns_proxy [-a <listen_addr>] [-p <listen_port>] [-T <tcp_client_l
Process
-d Daemonize.
-u user Optional user to drop to if launched as root.
Also uses the user's primary group unless -g is set.
-g group Optional group to drop to if launched as root.

Logging
Expand All @@ -220,6 +225,27 @@ Usage: ./https_dns_proxy [-a <listen_addr>] [-p <listen_port>] [-T <tcp_client_l
-h Print help and exit.
```

`--outbound-interface` may be combined with `-S`, `-d`, and the HTTP version
options. When combined with `-I`, `-S` must be an IP literal.
It cannot be combined with the in-process `-u` privilege drop, which
clears Linux capabilities. When using `-t`, the proxy connection is bound to the
interface, but local resolution of a proxy hostname is not.

Interface binding is checked before daemonization and on every subsequently
created HTTPS and bootstrap DNS socket. Missing permissions, an absent
interface, or loss of the interface therefore fails closed.

### Interface-binding privileges

Linux requires `CAP_NET_RAW`. Grant it to the final service user through the
service manager rather than using `-u`. For example, a systemd override can use:

```ini
[Service]
AmbientCapabilities=CAP_NET_RAW
CapabilityBoundingSet=CAP_NET_RAW
```

## Testing

Functional tests can be executed using [Robot Framework](https://robotframework.org/).
Expand Down
9 changes: 7 additions & 2 deletions src/dns_listener.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#define _DNS_LISTENER_H_

#include <sys/socket.h>
#include <stdint.h>
#include <sys/types.h>

// A DNS listener accepts requests on some transport (UDP, TCP, ...) and routes
Expand All @@ -23,16 +24,20 @@ typedef enum {
// Invoked once per fully-received DNS request. `dns_req` is heap-allocated
// and ownership transfers to the callee. `listener` is a back-pointer the
// callee uses later to deliver the matching response.
// connection_id identifies the TCP connection; UDP uses zero.
typedef void (*dns_request_fn)(void *ctx, dns_listener_t *listener,
struct sockaddr *raddr,
struct sockaddr *raddr, uint64_t connection_id,
char *dns_req, size_t dns_req_len);

struct dns_listener {
// Send `dns_resp` to `raddr`. UDP listeners may EDNS-truncate the response
// in place using `dns_req`; TCP listeners ignore the request bytes.
void (*respond)(dns_listener_t *self, struct sockaddr *raddr,
void (*respond)(dns_listener_t *self, struct sockaddr *raddr, uint64_t connection_id,
const char *dns_req, size_t dns_req_len,
char *dns_resp, size_t dns_resp_len);
// Release any transport-level capacity reserved for this request. The
// proxy core calls this exactly once whether the request succeeds or fails.
void (*request_complete)(dns_listener_t *self, struct sockaddr *raddr, uint64_t connection_id);
// Stop accepting new requests. Existing per-client state (TCP) is retained
// so any in-flight DoH responses can still be delivered during graceful
// drain.
Expand Down
Loading