├── .github └── workflows │ └── cmake.yml ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── development_build_with_http3.sh ├── https_dns_proxy.service.in ├── munin ├── https_dns_proxy.config └── https_dns_proxy.plugin ├── src ├── dns_poller.c ├── dns_poller.h ├── dns_server.c ├── dns_server.h ├── https_client.c ├── https_client.h ├── logging.c ├── logging.h ├── main.c ├── options.c ├── options.h ├── ring_buffer.c ├── ring_buffer.h ├── stat.c └── stat.h └── tests └── robot ├── functional_tests.robot └── valgrind.supp /.github/workflows/cmake.yml: -------------------------------------------------------------------------------- 1 | name: CMake 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | # The CMake configure and build commands are platform agnostic and should work equally 8 | # well on Windows or Mac. You can convert this to a matrix build if you need 9 | # cross-platform coverage. 10 | # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix 11 | runs-on: ubuntu-24.04 12 | 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | compiler: [gcc-13, clang-18] 17 | 18 | steps: 19 | - uses: actions/checkout@main 20 | 21 | - name: Update APT 22 | run: sudo apt-get update 23 | 24 | - name: Setup Dependencies 25 | run: sudo apt-get install cmake libc-ares-dev libcurl4-openssl-dev libev-dev build-essential clang-tidy dnsutils python3-pip python3-venv valgrind ${{ matrix.compiler }} 26 | 27 | - name: Setup Python Virtual Environment 28 | run: python3 -m venv ${{github.workspace}}/venv 29 | 30 | - name: Setup Robot Framework 31 | run: ${{github.workspace}}/venv/bin/pip3 install robotframework 32 | 33 | - name: Configure CMake 34 | env: 35 | CC: ${{ matrix.compiler }} 36 | run: cmake -D CMAKE_BUILD_TYPE=Debug -D PYTHON3_EXE=${{github.workspace}}/venv/bin/python3 -B ${{github.workspace}}/ 37 | 38 | - name: Build 39 | env: 40 | CC: ${{ matrix.compiler }} 41 | # Build your program with the given configuration 42 | run: make -C ${{github.workspace}}/ 43 | 44 | - name: Test 45 | run: make -C ${{github.workspace}}/ test ARGS="--verbose" 46 | 47 | - uses: actions/upload-artifact@v4 48 | if: ${{ success() || failure() }} 49 | with: 50 | name: robot-logs-${{ matrix.compiler }} 51 | path: | 52 | ${{github.workspace}}/tests/robot/*.html 53 | ${{github.workspace}}/tests/robot/valgrind-*.log 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | CMakeCache.txt 2 | CTestTestfile.cmake 3 | CMakeFiles/ 4 | Testing/ 5 | Makefile 6 | lib/*.a 7 | cmake_install.cmake 8 | https_dns_proxy 9 | *_unittest 10 | https_dns_proxy.service 11 | install_manifest.txt 12 | .ninja_deps 13 | .ninja_log 14 | build.ninja 15 | rules.ninja 16 | log.html 17 | output.xml 18 | report.html 19 | custom_curl/ 20 | valgrind-*.log 21 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.7) 2 | project(HttpsDnsProxy C) 3 | 4 | # FUNCTIONS 5 | 6 | # source: https://stackoverflow.com/a/27990434 7 | function(define_file_basename_for_sources targetname) 8 | get_target_property(source_files "${targetname}" SOURCES) 9 | foreach(sourcefile ${source_files}) 10 | get_filename_component(basename "${sourcefile}" NAME) 11 | set_property( 12 | SOURCE "${sourcefile}" APPEND 13 | PROPERTY COMPILE_DEFINITIONS "__FILENAME__=\"${basename}\"") 14 | endforeach() 15 | endfunction() 16 | 17 | # CONFIG 18 | 19 | if(NOT CMAKE_BUILD_TYPE) 20 | set(CMAKE_BUILD_TYPE "Release") 21 | message(STATUS "Setting build type to '${CMAKE_BUILD_TYPE}' as none was specified.") 22 | endif() 23 | 24 | if (NOT CMAKE_INSTALL_BINDIR) 25 | set(CMAKE_INSTALL_BINDIR bin) 26 | endif() 27 | 28 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra --pedantic -Wno-strict-aliasing -Wno-variadic-macros") 29 | set(CMAKE_C_FLAGS_DEBUG "-gdwarf-4 -DDEBUG") 30 | set(CMAKE_C_FLAGS_RELEASE "-O2") 31 | 32 | if ((CMAKE_C_COMPILER_ID MATCHES GNU AND CMAKE_C_COMPILER_VERSION VERSION_GREATER_EQUAL 9) OR 33 | (CMAKE_C_COMPILER_ID MATCHES Clang AND CMAKE_C_COMPILER_VERSION VERSION_GREATER_EQUAL 10)) 34 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-gnu-zero-variadic-macro-arguments -Wno-gnu-folding-constant") 35 | endif() 36 | 37 | # VERSION 38 | # It is possible to define external default value like: cmake -DSW_VERSION=1.2-custom 39 | 40 | find_package(Git) 41 | if(Git_FOUND) 42 | execute_process( 43 | COMMAND "${GIT_EXECUTABLE}" show --date=format:%Y.%m.%d --format=%ad-%h --no-patch 44 | WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" 45 | OUTPUT_VARIABLE GIT_VERSION 46 | OUTPUT_STRIP_TRAILING_WHITESPACE) 47 | 48 | if(GIT_VERSION) 49 | set(SW_VERSION "${GIT_VERSION}") 50 | else() 51 | message(WARNING "Could not find out version from git command!") 52 | endif() 53 | 54 | # May not update version in some cases (example: git commit --amend) 55 | set_property(GLOBAL APPEND 56 | PROPERTY CMAKE_CONFIGURE_DEPENDS 57 | "${CMAKE_SOURCE_DIR}/.git/index") 58 | else() 59 | message(WARNING "Could not find git command!") 60 | endif() 61 | 62 | if(NOT SW_VERSION) 63 | message(WARNING "Version unset, using hardcoded!") 64 | endif() 65 | 66 | # LIBRARY DEPENDENCIES 67 | 68 | find_path(LIBCARES_INCLUDE_DIR ares.h) 69 | find_path(LIBEV_INCLUDE_DIR ev.h) 70 | 71 | if(CUSTOM_LIBCURL_INSTALL_PATH) 72 | message(STATUS "Using custom libcurl from: ${CUSTOM_LIBCURL_INSTALL_PATH}") 73 | set(LIBCURL_INCLUDE_DIR "${CUSTOM_LIBCURL_INSTALL_PATH}/include") 74 | link_directories(BEFORE "${CUSTOM_LIBCURL_INSTALL_PATH}/lib") 75 | else() 76 | message(STATUS "Using system libcurl") 77 | find_path(LIBCURL_INCLUDE_DIR curl/curl.h) 78 | endif() 79 | 80 | include_directories( 81 | ${LIBCARES_INCLUDE_DIR} ${LIBCURL_INCLUDE_DIR} 82 | ${LIBEV_INCLUDE_DIR} src) 83 | 84 | # CLANG TIDY 85 | 86 | option(USE_CLANG_TIDY "Use clang-tidy during compilation" ON) 87 | 88 | if(USE_CLANG_TIDY) 89 | find_program( 90 | CLANG_TIDY_EXE 91 | NAMES "clang-tidy" 92 | DOC "Path to clang-tidy executable" 93 | ) 94 | if(NOT CLANG_TIDY_EXE) 95 | message(STATUS "clang-tidy not found.") 96 | else() 97 | message(STATUS "clang-tidy found: ${CLANG_TIDY_EXE}") 98 | set(DO_CLANG_TIDY "${CLANG_TIDY_EXE}" "-fix" "-fix-errors" "-checks=*,-cert-err34-c,-readability-identifier-length,-altera-unroll-loops,-bugprone-easily-swappable-parameters,-concurrency-mt-unsafe,-*magic-numbers,-hicpp-signed-bitwise,-readability-function-cognitive-complexity,-altera-id-dependent-backward-branch,-google-readability-todo,-misc-include-cleaner") 99 | endif() 100 | else() 101 | message(STATUS "Not using clang-tidy.") 102 | endif() 103 | 104 | # BUILD 105 | 106 | # The main binary 107 | set(TARGET_NAME "https_dns_proxy") 108 | aux_source_directory(src SRC_LIST) 109 | set(SRC_LIST ${SRC_LIST}) 110 | add_executable(${TARGET_NAME} ${SRC_LIST}) 111 | set(LIBS ${LIBS} cares curl ev resolv) 112 | target_link_libraries(${TARGET_NAME} ${LIBS}) 113 | set_property(TARGET ${TARGET_NAME} PROPERTY C_STANDARD 11) 114 | 115 | define_file_basename_for_sources("https_dns_proxy") 116 | 117 | 118 | if(SW_VERSION) 119 | set_source_files_properties( 120 | src/main.c 121 | PROPERTIES COMPILE_FLAGS "-DSW_VERSION='\"${SW_VERSION}\"'") 122 | endif() 123 | 124 | if(CLANG_TIDY_EXE) 125 | set_target_properties( 126 | ${TARGET_NAME} PROPERTIES 127 | C_CLANG_TIDY "${DO_CLANG_TIDY}" 128 | ) 129 | endif() 130 | 131 | # INSTALL 132 | 133 | install(TARGETS ${TARGET_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR}) 134 | 135 | set(SERVICE_EXTRA_OPTIONS "") 136 | if(IS_DIRECTORY "/etc/munin/plugins" AND 137 | IS_DIRECTORY "/etc/munin/plugin-conf.d") 138 | set(SERVICE_EXTRA_OPTIONS "-s 300") 139 | install(PROGRAMS munin/${TARGET_NAME}.plugin 140 | DESTINATION /etc/munin/plugins/ 141 | RENAME ${TARGET_NAME}) 142 | install(FILES munin/${TARGET_NAME}.config 143 | DESTINATION /etc/munin/plugin-conf.d/ 144 | RENAME ${TARGET_NAME}) 145 | endif() 146 | 147 | configure_file(${TARGET_NAME}.service.in ${TARGET_NAME}.service) 148 | 149 | install(FILES ${CMAKE_BINARY_DIR}/${TARGET_NAME}.service 150 | DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/systemd/system/) 151 | 152 | # TESTING 153 | 154 | find_program( 155 | PYTHON3_EXE 156 | NAMES "python3" 157 | DOC "Path to python3 executable" 158 | ) 159 | if(NOT PYTHON3_EXE) 160 | message(STATUS "python3 not found, robot testing not possible") 161 | else() 162 | message(STATUS "python3 found: ${PYTHON3_EXE}") 163 | 164 | enable_testing() 165 | add_test(NAME robot COMMAND ${PYTHON3_EXE} -m robot.run functional_tests.robot 166 | WORKING_DIRECTORY tests/robot) 167 | endif() 168 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Aaron Drew 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # https-dns-proxy 2 | 3 | `https_dns_proxy` is a light-weight DNS<-->HTTPS, non-caching translation 4 | proxy for the [RFC 8484][rfc-8484] DNS-over-HTTPS standard. It receives 5 | regular (UDP) DNS requests and issues them via DoH. 6 | 7 | [Google's DNS-over-HTTPS][google-doh] service is default, but 8 | [Cloudflare's service][cloudflare-doh] also works with trivial commandline flag 9 | changes. 10 | 11 | [cloudflare-doh]: https://developers.cloudflare.com/1.1.1.1/dns-over-https/wireformat/ 12 | [rfc-8484]: https://tools.ietf.org/html/rfc8484 13 | [google-doh]: https://developers.google.com/speed/public-dns/docs/doh 14 | 15 | ### Using Google 16 | 17 | ```bash 18 | # ./https_dns_proxy -u nobody -g nogroup -d -b 8.8.8.8,8.8.4.4 \ 19 | -r "https://dns.google/dns-query" 20 | ``` 21 | 22 | ### Using Cloudflare 23 | 24 | ```bash 25 | # ./https_dns_proxy -u nobody -g nogroup -d -b 1.1.1.1,1.0.0.1 \ 26 | -r "https://cloudflare-dns.com/dns-query" 27 | ``` 28 | 29 | ## Why? 30 | 31 | Using DNS over HTTPS makes eavesdropping and spoofing of DNS traffic between you 32 | and the HTTPS DNS provider (Google/Cloudflare) much less likely. This of course 33 | only makes sense if you trust your DoH provider. 34 | 35 | ## Features 36 | 37 | * Tiny Size (<30kiB). 38 | * Uses curl for HTTP/2 and pipelining, keeping resolve latencies extremely low. 39 | * Single-threaded, non-blocking select() server for use on resource-starved 40 | embedded systems. 41 | * Designed to sit in front of dnsmasq or similar caching resolver for 42 | transparent use. 43 | 44 | ## Build 45 | 46 | Depends on `c-ares (>=1.11.0)`, `libcurl (>=7.66.0)`, `libev (>=4.25)`. 47 | 48 | On Debian-derived systems those are libc-ares-dev, 49 | libcurl4-{openssl,nss,gnutls}-dev and libev-dev respectively. 50 | On Redhat-derived systems those are c-ares-devel, libcurl-devel and libev-devel. 51 | 52 | On MacOS, you may run into issues with curl headers. Others have had success when first installing curl with brew. 53 | ``` 54 | brew install curl --with-openssl --with-c-ares --with-libssh2 --with-nghttp2 --with-gssapi --with-libmetalink 55 | brew link curl --force 56 | ``` 57 | 58 | On Ubuntu 59 | ``` 60 | apt-get install cmake libc-ares-dev libcurl4-openssl-dev libev-dev build-essential 61 | ``` 62 | 63 | If all pre-requisites are met, you should be able to build with: 64 | ``` 65 | $ cmake . 66 | $ make 67 | ``` 68 | 69 | ### Build with HTTP/3 support 70 | 71 | * If system libcurl supports it by default nothing else has to be done 72 | 73 | * If a custom build of libcurl supports HTTP/3 which is installed in a different location, that can be set when running cmake: 74 | ``` 75 | $ cmake -D CUSTOM_LIBCURL_INSTALL_PATH=/absolute/path/to/custom/libcurl/install . 76 | ``` 77 | 78 | * Just to test HTTP/3 support for development purpose, simply run the following command and wait for a long time: 79 | ``` 80 | $ ./development_build_with_http3.sh 81 | ``` 82 | 83 | ## INSTALL 84 | 85 | ### Install built program 86 | 87 | This method work fine on most Linux operating system, which uses systemd. 88 | Like: Raspberry Pi OS / Raspbian, Debian, Ubuntu, etc. 89 | 90 | To install the program binary, systemd service and munin plugin (if munin is pre-installed), 91 | simply execute the following after build: 92 | ``` 93 | $ sudo make install 94 | ``` 95 | 96 | To activate munin plugin, restart munin services: 97 | ``` 98 | $ sudo systemctl restart munin munin-node 99 | ``` 100 | 101 | To overwrite default service options use: 102 | ``` 103 | $ sudo systemctl edit https_dns_proxy.service 104 | ``` 105 | And re-define ExecStart with desired options: 106 | ``` 107 | [Service] 108 | ExecStart= 109 | ExecStart=/usr/local/bin/https_dns_proxy \ 110 | -u nobody -g nogroup -r https://doh.opendns.com/dns-query 111 | ``` 112 | 113 | ### OpenWRT package install 114 | 115 | There is a package in the [OpenWRT packages](https://github.com/openwrt/packages) repository as well. 116 | You can install as follows: 117 | 118 | ``` 119 | root@OpenWrt:~# opkg update 120 | root@OpenWrt:~# opkg install https-dns-proxy 121 | root@OpenWrt:~# /etc/init.d/https-dns-proxy enable 122 | root@OpenWrt:~# /etc/init.d/https-dns-proxy start 123 | ``` 124 | 125 | OpenWrt's init script automatically updates the `dnsmasq` config to include only DoH servers on its start and restores old settings on stop. Additional information on OpenWrt-specific configuration is available at the [README](https://github.com/openwrt/packages/blob/master/net/https-dns-proxy/files/README.md). 126 | 127 | If you are using any other resolver on your router you will need to manually replace any previously used servers with entries like: 128 | 129 | `127.0.0.1#5053` 130 | 131 | You may also want to prevent your resolver from using /etc/resolv.conf DNS servers, leaving only our proxy server. 132 | 133 | There's also a WebUI package available for OpenWrt (`luci-app-https-dns-proxy`) which contains the list of supported and tested DoH providers. 134 | 135 | ### archlinux package install 136 | 137 | There is also an externally maintained [AUR package](https://aur.archlinux.org/packages/https-dns-proxy-git/) for latest git version. You can install as follows: 138 | ``` 139 | user@arch:~# yay -S https-dns-proxy-git 140 | ``` 141 | 142 | ### Docker install 143 | 144 | There is also an externally maintained [Docker image](https://hub.docker.com/r/bwmoran/https-dns-proxy) for latest git version. Documentation, Dockerfile, and entrypoint script can be viewed on [GitHub](https://github.com/moranbw/https-dns-proxy-docker). An example run: 145 | 146 | ``` 147 | ### points towards AdGuard DNS, only use IPv4, increase logging ### 148 | 149 | docker run --name "https-dns-proxy" -p 5053:5053/udp \ 150 | -e DNS_SERVERS="94.140.14.14,94.140.15.15" \ 151 | -e RESOLVER_URL="https://dns.adguard.com/dns-query" \ 152 | -d bwmoran/https-dns-proxy \ 153 | -4 -vvv 154 | ``` 155 | 156 | ## Usage 157 | 158 | Just run it as a daemon and point traffic at it. Commandline flags are: 159 | 160 | ``` 161 | Usage: ./https_dns_proxy [-a ] [-p ] 162 | [-b ] [-i ] [-4] 163 | [-r ] [-t ] [-x] [-q] [-C ] [-c ] 164 | [-d] [-u ] [-g ] 165 | [-v]+ [-l ] [-s ] [-F ] [-V] [-h] 166 | 167 | DNS server 168 | -a listen_addr Local IPv4/v6 address to bind to. (Default: 127.0.0.1) 169 | -p listen_port Local port to bind to. (Default: 5053) 170 | 171 | DNS client 172 | -b dns_servers Comma-separated IPv4/v6 addresses and ports (addr:port) 173 | of DNS servers to resolve resolver host (e.g. dns.google). 174 | When specifying a port for IPv6, enclose the address in []. 175 | (Default: 8.8.8.8,1.1.1.1,8.8.4.4,1.0.0.1,145.100.185.15,145.100.185.16,185.49.141.37) 176 | -i polling_interval Optional polling interval of DNS servers. 177 | (Default: 120, Min: 5, Max: 3600) 178 | -4 Force IPv4 hostnames for DNS resolvers non IPv6 networks. 179 | 180 | HTTPS client 181 | -r resolver_url The HTTPS path to the resolver URL. (Default: https://dns.google/dns-query) 182 | -t proxy_server Optional HTTP proxy. e.g. socks5://127.0.0.1:1080 183 | Remote name resolution will be used if the protocol 184 | supports it (http, https, socks4a, socks5h), otherwise 185 | initial DNS resolution will still be done via the 186 | bootstrap DNS servers. 187 | -x Use HTTP/1.1 instead of HTTP/2. Useful with broken 188 | or limited builds of libcurl. 189 | -q Use HTTP/3 (QUIC) only. 190 | -m max_idle_time Maximum idle time in seconds allowed for reusing a HTTPS connection. 191 | (Default: 118, Min: 0, Max: 3600) 192 | -C ca_path Optional file containing CA certificates. 193 | -c dscp_codepoint Optional DSCP codepoint to set on upstream HTTPS server 194 | connections. (Min: 0, Max: 63) 195 | 196 | Process 197 | -d Daemonize. 198 | -u user Optional user to drop to if launched as root. 199 | -g group Optional group to drop to if launched as root. 200 | 201 | Logging 202 | -v Increase logging verbosity. (Default: error) 203 | Levels: fatal, stats, error, warning, info, debug 204 | Request issues are logged on warning level. 205 | -l logfile Path to file to log to. (Default: standard output) 206 | -s statistic_interval Optional statistic printout interval. 207 | (Default: 0, Disabled: 0, Min: 1, Max: 3600) 208 | -F log_limit Flight recorder: storing desired amount of logs from all levels 209 | in memory and dumping them on fatal error or on SIGUSR2 signal. 210 | (Default: 0, Disabled: 0, Min: 100, Max: 100000) 211 | -V Print versions and exit. 212 | -h Print help and exit. 213 | ``` 214 | 215 | ## Testing 216 | 217 | Functional tests can be executed using [Robot Framework](https://robotframework.org/). 218 | 219 | dig and valgrind commands are expected to be available. 220 | 221 | ``` 222 | pip3 install robotframework 223 | python3 -m robot.run tests/robot/functional_tests.robot 224 | ``` 225 | 226 | ## TODO 227 | 228 | * Add some tests. 229 | * Improve IPv6 handling and add automatic fallback to IPv4 230 | 231 | ## Authors 232 | 233 | * Aaron Drew (aarond10@gmail.com): Original https_dns_proxy. 234 | * Soumya ([github.com/soumya92](https://github.com/soumya92)): RFC 8484 implementation. 235 | * baranyaib90 ([github.com/baranyaib90](https://github.com/baranyaib90)): fixes and improvements. 236 | 237 | -------------------------------------------------------------------------------- /development_build_with_http3.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | echo 6 | echo "WARNING !!!" 7 | echo 8 | echo "Use only for development and testing!" 9 | echo "It is highly highly not recommended, to use in production!" 10 | echo "This script was based on: https://github.com/curl/curl/blob/curl-8_12_1/docs/HTTP3.md" 11 | echo 12 | echo "Extra packages suggested to be installed: autoconf libtool" 13 | echo 14 | 15 | sleep 5 16 | 17 | set -x 18 | 19 | INSTALL_DIR=$PWD/custom_curl/install 20 | mkdir -p $INSTALL_DIR 21 | cd custom_curl 22 | 23 | ### 24 | 25 | git clone --depth 1 -b openssl-3.1.4+quic https://github.com/quictls/openssl 26 | cd openssl 27 | ./config enable-tls1_3 --prefix=$INSTALL_DIR 28 | make -j build_libs 29 | make install_dev 30 | cd .. 31 | 32 | git clone --depth 1 -b v1.1.0 https://github.com/ngtcp2/nghttp3 33 | cd nghttp3 34 | git submodule update --init 35 | autoreconf -fi 36 | ./configure --prefix=$INSTALL_DIR --enable-lib-only 37 | make -j 38 | make install 39 | cd .. 40 | 41 | git clone --depth 1 -b v1.2.0 https://github.com/ngtcp2/ngtcp2 42 | cd ngtcp2 43 | autoreconf -fi 44 | ./configure PKG_CONFIG_PATH=$INSTALL_DIR/lib64/pkgconfig:$INSTALL_DIR/lib64/pkgconfig LDFLAGS="-Wl,-rpath,$INSTALL_DIR/lib64" --prefix=$INSTALL_DIR --enable-lib-only --with-openssl 45 | make -j 46 | make install 47 | cd .. 48 | 49 | git clone --depth 1 -b v1.64.0 https://github.com/nghttp2/nghttp2 50 | cd nghttp2 51 | autoreconf -fi 52 | ./configure PKG_CONFIG_PATH=$INSTALL_DIR/lib64/pkgconfig:$INSTALL_DIR/lib64/pkgconfig LDFLAGS="-Wl,-rpath,$INSTALL_DIR/lib64" --prefix=$INSTALL_DIR --enable-lib-only --with-openssl 53 | make -j 54 | make install 55 | cd .. 56 | 57 | git clone --depth 1 -b curl-8_12_1 https://github.com/curl/curl 58 | cd curl 59 | autoreconf -fi 60 | LDFLAGS="-Wl,-rpath,$INSTALL_DIR/lib64" ./configure --with-openssl=$INSTALL_DIR --with-nghttp2=$INSTALL_DIR --with-nghttp3=$INSTALL_DIR --with-ngtcp2=$INSTALL_DIR --prefix=$INSTALL_DIR 61 | make -j 62 | make install 63 | cd .. 64 | 65 | ### 66 | 67 | cd .. 68 | cmake -D CUSTOM_LIBCURL_INSTALL_PATH=$INSTALL_DIR -D CMAKE_BUILD_TYPE=Debug . 69 | make -j 70 | -------------------------------------------------------------------------------- /https_dns_proxy.service.in: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=https-dns-proxy - A light-weight DNS-HTTPS, non-caching translation proxy 3 | Requires=network.target 4 | Wants=nss-lookup.target 5 | Before=nss-lookup.target 6 | After=network.target 7 | 8 | [Service] 9 | Type=simple 10 | DynamicUser=yes 11 | Restart=on-failure 12 | ExecStart=${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}/https_dns_proxy \ 13 | -v -v ${SERVICE_EXTRA_OPTIONS} 14 | TimeoutStopSec=10 15 | 16 | [Install] 17 | WantedBy=multi-user.target 18 | -------------------------------------------------------------------------------- /munin/https_dns_proxy.config: -------------------------------------------------------------------------------- 1 | [https_dns_proxy] 2 | group systemd-journal 3 | -------------------------------------------------------------------------------- /munin/https_dns_proxy.plugin: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | case $1 in 4 | config) 5 | cat <<'EOM' 6 | multigraph https_dns_proxy_count 7 | graph_title HTTPS DNS proxy - count 8 | graph_vlabel count 9 | graph_category network 10 | graph_scale no 11 | graph_args --base 1000 --lower-limit 0 12 | requests.label Requests 13 | responses.label Responses 14 | 15 | multigraph https_dns_proxy_latency 16 | graph_title HTTPS DNS proxy - latency 17 | graph_vlabel latency 18 | graph_category network 19 | graph_scale no 20 | graph_args --base 1000 --lower-limit 0 21 | latency.label Latency 22 | 23 | multigraph https_dns_proxy_connections 24 | graph_title HTTPS DNS proxy - connections 25 | graph_vlabel count 26 | graph_category network 27 | graph_scale no 28 | graph_args --base 1000 --lower-limit 0 29 | opened.label Opened 30 | closed.label Closed 31 | reused.label Reused 32 | EOM 33 | exit 0;; 34 | 35 | autoconf) 36 | pgrep https_dns_proxy >/dev/null 2>&1 \ 37 | && echo "yes" \ 38 | || echo "no" 39 | exit 0;; 40 | esac 41 | 42 | log_lines=$(journalctl --unit https_dns_proxy.service --output cat --since '6 minutes ago') 43 | pattern='stat\.c:[0-9]+ ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+)$' 44 | 45 | # match log lines with pattern (last match will be used) 46 | IFS=' 47 | ' 48 | for line in $log_lines; do 49 | if [[ $line =~ $pattern ]]; then 50 | stat=("${BASH_REMATCH[@]}") 51 | # else 52 | # echo "stat regexp did not match with line: $line" >&2 53 | fi 54 | done 55 | 56 | latency='U' 57 | if [ -n "${stat[3]}" ] && \ 58 | [ -n "${stat[2]}" ] && \ 59 | [ "${stat[2]}" -gt "0" ]; then 60 | latency=$((${stat[3]} / ${stat[2]})) 61 | fi 62 | 63 | echo "multigraph https_dns_proxy_count" 64 | echo "requests.value ${stat[1]:-U}" 65 | echo "responses.value ${stat[2]:-U}" 66 | echo "multigraph https_dns_proxy_latency" 67 | echo "latency.value ${latency}" 68 | echo "multigraph https_dns_proxy_connections" 69 | echo "opened.value ${stat[6]:-U}" 70 | echo "closed.value ${stat[7]:-U}" 71 | echo "reused.value ${stat[8]:-U}" 72 | -------------------------------------------------------------------------------- /src/dns_poller.c: -------------------------------------------------------------------------------- 1 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 2 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 3 | 4 | #include "dns_poller.h" 5 | #include "logging.h" 6 | 7 | static void sock_cb(struct ev_loop __attribute__((unused)) *loop, 8 | ev_io *w, int revents) { 9 | dns_poller_t *d = (dns_poller_t *)w->data; 10 | ares_process_fd(d->ares, (revents & EV_READ) ? w->fd : ARES_SOCKET_BAD, 11 | (revents & EV_WRITE) ? w->fd : ARES_SOCKET_BAD); 12 | } 13 | 14 | static struct ev_io * get_io_event(dns_poller_t *d, int sock) { 15 | for (int i = 0; i < d->io_events_count; i++) { 16 | if (d->io_events[i].fd == sock) { 17 | return &d->io_events[i]; 18 | } 19 | } 20 | return NULL; 21 | } 22 | 23 | static void sock_state_cb(void *data, int fd, int read, int write) { 24 | dns_poller_t *d = (dns_poller_t *)data; 25 | // stop and release used event 26 | struct ev_io *io_event_ptr = get_io_event(d, fd); 27 | if (io_event_ptr) { 28 | ev_io_stop(d->loop, io_event_ptr); 29 | io_event_ptr->fd = 0; 30 | DLOG("Released used io event: %p", io_event_ptr); 31 | } 32 | if (!read && !write) { 33 | return; 34 | } 35 | // reserve and start new event on unused slot 36 | io_event_ptr = get_io_event(d, 0); 37 | if (!io_event_ptr) { 38 | FLOG("c-ares needed more IO event handler, than the number of provided nameservers: %d", d->io_events_count); 39 | } 40 | DLOG("Reserved new io event: %p", io_event_ptr); 41 | // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) 42 | ev_io_init(io_event_ptr, sock_cb, fd, 43 | (read ? EV_READ : 0) | (write ? EV_WRITE : 0)); 44 | ev_io_start(d->loop, io_event_ptr); 45 | } 46 | 47 | static char *get_addr_listing(char** addr_list, const int af) { 48 | char *list = (char *)calloc(1, POLLER_ADDR_LIST_SIZE); 49 | char *pos = list; 50 | if (list == NULL) { 51 | FLOG("Out of mem"); 52 | } 53 | for (int i = 0; addr_list[i]; i++) { 54 | const char *res = ares_inet_ntop(af, addr_list[i], pos, 55 | list + POLLER_ADDR_LIST_SIZE - 1 - pos); 56 | if (res != NULL) { 57 | pos += strlen(pos); 58 | *pos = ','; 59 | pos++; 60 | } 61 | } 62 | if (pos == list) { 63 | free((void*)list); 64 | list = NULL; 65 | } else { 66 | *(pos-1) = '\0'; 67 | } 68 | return list; 69 | } 70 | 71 | static void ares_cb(void *arg, int status, int __attribute__((unused)) timeouts, 72 | struct hostent *h) { 73 | dns_poller_t *d = (dns_poller_t *)arg; 74 | d->request_ongoing = 0; 75 | ev_tstamp interval = 5; // retry by default after some time 76 | 77 | if (status != ARES_SUCCESS) { 78 | WLOG("DNS lookup of '%s' failed: %s", d->hostname, ares_strerror(status)); 79 | } else if (!h || h->h_length < 1) { 80 | WLOG("No hosts found for '%s'", d->hostname); 81 | } else { 82 | interval = d->polling_interval; 83 | d->cb(d->hostname, d->cb_data, get_addr_listing(h->h_addr_list, h->h_addrtype)); 84 | } 85 | 86 | if (status != ARES_EDESTRUCTION) { 87 | DLOG("DNS poll interval changed to: %.0lf", interval); 88 | ev_timer_stop(d->loop, &d->timer); 89 | ev_timer_set(&d->timer, interval, 0); 90 | ev_timer_start(d->loop, &d->timer); 91 | } 92 | } 93 | 94 | static ev_tstamp get_timeout(dns_poller_t *d) 95 | { 96 | static struct timeval max_tv = {.tv_sec = 5, .tv_usec = 0}; 97 | struct timeval tv; 98 | struct timeval *tvp = ares_timeout(d->ares, &max_tv, &tv); 99 | // NOLINTNEXTLINE(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) 100 | ev_tstamp after = tvp->tv_sec + tvp->tv_usec * 1e-6; 101 | return after ? after : 0.1; 102 | } 103 | 104 | static void timer_cb(struct ev_loop __attribute__((unused)) *loop, 105 | ev_timer *w, int __attribute__((unused)) revents) { 106 | dns_poller_t *d = (dns_poller_t *)w->data; 107 | 108 | if (d->request_ongoing) { 109 | // process query timeouts 110 | DLOG("Processing DNS queries"); 111 | ares_process(d->ares, NULL, NULL); 112 | } else { 113 | DLOG("Starting DNS query"); 114 | // Cancel any pending queries before making new ones. c-ares can't be depended on to 115 | // execute ares_cb() even after the specified query timeout has been reached, e.g. if 116 | // the packet was dropped without any response from the network. This also serves to 117 | // free memory tied up by any "zombie" queries. 118 | ares_cancel(d->ares); 119 | d->request_ongoing = 1; 120 | ares_gethostbyname(d->ares, d->hostname, d->family, ares_cb, d); 121 | } 122 | 123 | if (d->request_ongoing) { // need to re-check, it might change! 124 | const ev_tstamp interval = get_timeout(d); 125 | DLOG("DNS poll interval changed to: %.03f", interval); 126 | ev_timer_stop(d->loop, &d->timer); 127 | ev_timer_set(&d->timer, interval, 0); 128 | ev_timer_start(d->loop, &d->timer); 129 | } 130 | } 131 | 132 | void dns_poller_init(dns_poller_t *d, struct ev_loop *loop, 133 | const char *bootstrap_dns, 134 | int bootstrap_dns_polling_interval, 135 | const char *hostname, 136 | int family, dns_poller_cb cb, void *cb_data) { 137 | int r = ares_library_init(ARES_LIB_INIT_ALL); 138 | if (r != ARES_SUCCESS) { 139 | FLOG("ares_library_init error: %s", ares_strerror(r)); 140 | } 141 | 142 | struct ares_options options = { 143 | .timeout = POLLER_QUERY_TIMEOUT_MS, 144 | .tries = POLLER_QUERY_TRIES, 145 | .sock_state_cb = sock_state_cb, 146 | .sock_state_cb_data = d 147 | }; 148 | int optmask = ARES_OPT_TIMEOUTMS | ARES_OPT_TRIES | ARES_OPT_SOCK_STATE_CB; 149 | 150 | r = ares_init_options(&d->ares, &options, optmask); 151 | if (r != ARES_SUCCESS) { 152 | FLOG("ares_init_options error: %s", ares_strerror(r)); 153 | } 154 | 155 | r = ares_set_servers_ports_csv(d->ares, bootstrap_dns); 156 | if (r != ARES_SUCCESS) { 157 | FLOG("ares_set_servers_ports_csv error: %s", ares_strerror(r)); 158 | } 159 | 160 | d->loop = loop; 161 | d->hostname = hostname; 162 | d->family = family; 163 | d->cb = cb; 164 | d->polling_interval = bootstrap_dns_polling_interval; 165 | d->request_ongoing = 0; 166 | d->cb_data = cb_data; 167 | 168 | // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) 169 | ev_timer_init(&d->timer, timer_cb, 0, 0); 170 | d->timer.data = d; 171 | ev_timer_start(d->loop, &d->timer); 172 | 173 | int nameservers = 1; 174 | for (int i = 0; bootstrap_dns[i]; i++) { 175 | if (bootstrap_dns[i] == ',') { 176 | nameservers++; 177 | } 178 | } 179 | DLOG("Nameservers count: %d", nameservers); 180 | d->io_events = (ev_io *)calloc(nameservers, sizeof(ev_io)); // zeroed! 181 | if (!d->io_events) { 182 | FLOG("Out of mem"); 183 | } 184 | for (int i = 0; i < nameservers; i++) { 185 | d->io_events[i].data = d; 186 | } 187 | d->io_events_count = nameservers; 188 | } 189 | 190 | void dns_poller_cleanup(dns_poller_t *d) { 191 | ares_destroy(d->ares); 192 | ev_timer_stop(d->loop, &d->timer); 193 | ares_library_cleanup(); 194 | free(d->io_events); 195 | } 196 | -------------------------------------------------------------------------------- /src/dns_poller.h: -------------------------------------------------------------------------------- 1 | #ifndef _DNS_POLLER_H_ 2 | #define _DNS_POLLER_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Fast DNS querying mode 8 | // Requests will be send after 0, 0.8, 1.3, 1.75 second 9 | // And waiting extra 4.2 seconds for reply 10 | // That's total: 8,05 second for trying 11 | #define POLLER_QUERY_TIMEOUT_MS 700 12 | #define POLLER_QUERY_TRIES 4 13 | 14 | // enough for minimum 64 pcs IPv4 address or 25 pcs IPv6 15 | #define POLLER_ADDR_LIST_SIZE 1024 16 | 17 | // Callback to be called periodically when we get a valid DNS response. 18 | typedef void (*dns_poller_cb)(const char* hostname, void *data, 19 | const char *addr_list); 20 | 21 | typedef struct { 22 | ares_channel ares; 23 | struct ev_loop *loop; 24 | const char *hostname; 25 | int family; // AF_UNSPEC for IPv4 or IPv6, AF_INET for IPv4 only. 26 | dns_poller_cb cb; 27 | int polling_interval; 28 | int request_ongoing; 29 | void *cb_data; 30 | 31 | ev_timer timer; 32 | ev_io *io_events; 33 | int io_events_count; 34 | } dns_poller_t; 35 | 36 | // Initializes c-ares and starts a timer for periodic DNS resolution on the 37 | // provided ev_loop. `bootstrap_dns` is a comma-separated list of DNS servers to 38 | // use for the lookup `hostname` every `interval_seconds`. For each successful 39 | // lookup, `cb` will be called with the resolved address. 40 | // `family` should be AF_INET for IPv4 or AF_UNSPEC for both IPv4 and IPv6. 41 | // 42 | // Note: hostname *not* copied. It should remain valid until 43 | // dns_poller_cleanup called. 44 | void dns_poller_init(dns_poller_t *d, struct ev_loop *loop, 45 | const char *bootstrap_dns, 46 | int bootstrap_dns_polling_interval, 47 | const char *hostname, 48 | int family, dns_poller_cb cb, void *cb_data); 49 | 50 | // Tears down timer and frees resources associated with a dns poller. 51 | void dns_poller_cleanup(dns_poller_t *d); 52 | 53 | #endif 54 | -------------------------------------------------------------------------------- /src/dns_server.c: -------------------------------------------------------------------------------- 1 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 2 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 3 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 4 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 5 | #include 6 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 7 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 8 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 9 | 10 | #include "dns_server.h" 11 | #include "logging.h" 12 | 13 | 14 | enum { 15 | REQUEST_MAX = 1500 // A default MTU. We don't do TCP so any bigger is likely a waste 16 | }; 17 | 18 | 19 | // Creates and bind a listening UDP socket for incoming requests. 20 | static int get_listen_sock(const char *listen_addr, int listen_port, 21 | unsigned int *addrlen) { 22 | struct addrinfo *ai = NULL; 23 | struct addrinfo hints; 24 | // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) 25 | memset(&hints, 0, sizeof(struct addrinfo)); 26 | /* prevent DNS lookups if leakage is our worry */ 27 | hints.ai_flags = AI_NUMERICHOST; 28 | 29 | int res = getaddrinfo(listen_addr, NULL, &hints, &ai); 30 | if(res != 0) { 31 | FLOG("Error parsing listen address %s:%d (getaddrinfo): %s", listen_addr, listen_port, 32 | gai_strerror(res)); 33 | if(ai) { 34 | freeaddrinfo(ai); 35 | } 36 | return -1; 37 | } 38 | 39 | struct sockaddr_in *saddr = (struct sockaddr_in*) ai->ai_addr; 40 | 41 | *addrlen = ai->ai_addrlen; 42 | saddr->sin_port = htons(listen_port); 43 | 44 | int sock = socket(ai->ai_family, SOCK_DGRAM, 0); 45 | if (sock < 0) { 46 | FLOG("Error creating socket"); 47 | } 48 | 49 | res = bind(sock, ai->ai_addr, ai->ai_addrlen); 50 | if (res < 0) { 51 | FLOG("Error binding %s:%d: %s (%d)", listen_addr, listen_port, 52 | strerror(errno), res); 53 | } 54 | 55 | freeaddrinfo(ai); 56 | 57 | ILOG("Listening on %s:%d", listen_addr, listen_port); 58 | return sock; 59 | } 60 | 61 | static void watcher_cb(struct ev_loop __attribute__((unused)) *loop, 62 | ev_io *w, int __attribute__((unused)) revents) { 63 | dns_server_t *d = (dns_server_t *)w->data; 64 | 65 | char *buf = (char *)calloc(1, REQUEST_MAX + 1); 66 | if (buf == NULL) { 67 | FLOG("Out of mem"); 68 | } 69 | struct sockaddr_storage raddr; 70 | /* recvfrom can write to addrlen */ 71 | socklen_t tmp_addrlen = d->addrlen; 72 | ssize_t len = recvfrom(w->fd, buf, REQUEST_MAX, 0, (struct sockaddr*)&raddr, 73 | &tmp_addrlen); 74 | if (len < 0) { 75 | ELOG("recvfrom failed: %s", strerror(errno)); 76 | return; 77 | } 78 | 79 | if (len < (int)sizeof(uint16_t)) { 80 | WLOG("Malformed request received (too short)."); 81 | return; 82 | } 83 | 84 | uint16_t tx_id = ntohs(*((uint16_t*)buf)); 85 | d->cb(d, d->cb_data, (struct sockaddr*)&raddr, tx_id, buf, len); 86 | } 87 | 88 | void dns_server_init(dns_server_t *d, struct ev_loop *loop, 89 | const char *listen_addr, int listen_port, 90 | dns_req_received_cb cb, void *data) { 91 | d->loop = loop; 92 | d->sock = get_listen_sock(listen_addr, listen_port, &d->addrlen); 93 | d->cb = cb; 94 | d->cb_data = data; 95 | 96 | // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) 97 | ev_io_init(&d->watcher, watcher_cb, d->sock, EV_READ); 98 | d->watcher.data = d; 99 | ev_io_start(d->loop, &d->watcher); 100 | } 101 | 102 | void dns_server_respond(dns_server_t *d, struct sockaddr *raddr, char *buf, 103 | size_t blen) { 104 | ssize_t len = sendto(d->sock, buf, blen, 0, raddr, d->addrlen); 105 | if(len == -1) { 106 | DLOG("sendto failed: %s", strerror(errno)); 107 | } 108 | } 109 | 110 | void dns_server_stop(dns_server_t *d) { 111 | ev_io_stop(d->loop, &d->watcher); 112 | } 113 | 114 | void dns_server_cleanup(dns_server_t *d) { 115 | close(d->sock); 116 | } 117 | -------------------------------------------------------------------------------- /src/dns_server.h: -------------------------------------------------------------------------------- 1 | #ifndef _DNS_SERVER_H_ 2 | #define _DNS_SERVER_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | struct dns_server_s; 9 | 10 | typedef void (*dns_req_received_cb)(struct dns_server_s *dns_server, void *data, 11 | struct sockaddr* addr, uint16_t tx_id, 12 | char *dns_req, size_t dns_req_len); 13 | 14 | typedef struct dns_server_s { 15 | struct ev_loop *loop; 16 | void *cb_data; 17 | dns_req_received_cb cb; 18 | int sock; 19 | socklen_t addrlen; 20 | ev_io watcher; 21 | } dns_server_t; 22 | 23 | void dns_server_init(dns_server_t *d, struct ev_loop *loop, 24 | const char *listen_addr, int listen_port, 25 | dns_req_received_cb cb, void *data); 26 | 27 | // Sends a DNS response 'buf' of length 'blen' to 'raddr'. 28 | void dns_server_respond(dns_server_t *d, struct sockaddr *raddr, char *buf, 29 | size_t blen); 30 | 31 | void dns_server_stop(dns_server_t *d); 32 | 33 | void dns_server_cleanup(dns_server_t *d); 34 | 35 | #endif // _DNS_SERVER_H_ 36 | -------------------------------------------------------------------------------- /src/https_client.c: -------------------------------------------------------------------------------- 1 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 2 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 3 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 4 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 5 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 6 | #include 7 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 8 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 9 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 10 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 11 | 12 | #include "https_client.h" 13 | #include "logging.h" 14 | #include "options.h" 15 | #include "stat.h" 16 | 17 | #define DOH_CONTENT_TYPE "application/dns-message" 18 | enum { 19 | DOH_MAX_RESPONSE_SIZE = 65535 20 | }; 21 | 22 | // the following macros require to have ctx pointer to https_fetch_ctx structure 23 | // else: compilation failure will occur 24 | #define LOG_REQ(level, format, args...) LOG(level, "%04hX: " format, ctx->id, ## args) 25 | #define DLOG_REQ(format, args...) DLOG("%04hX: " format, ctx->id, ## args) 26 | #define ILOG_REQ(format, args...) ILOG("%04hX: " format, ctx->id, ## args) 27 | #define WLOG_REQ(format, args...) WLOG("%04hX: " format, ctx->id, ## args) 28 | #define ELOG_REQ(format, args...) ELOG("%04hX: " format, ctx->id, ## args) 29 | #define FLOG_REQ(format, args...) FLOG("%04hX: " format, ctx->id, ## args) 30 | 31 | #define ASSERT_CURL_MULTI_SETOPT(curlm, option, param) \ 32 | do { \ 33 | CURLMcode code = curl_multi_setopt((curlm), (option), (param)); \ 34 | if (code != CURLM_OK) { \ 35 | FLOG(#option " error %d: %s", code, curl_multi_strerror(code)); \ 36 | } \ 37 | } while(0); 38 | 39 | #define ASSERT_CURL_EASY_SETOPT(ctx, option, param) \ 40 | do { \ 41 | CURLcode code = curl_easy_setopt((ctx)->curl, (option), (param)); \ 42 | if (code != CURLE_OK) { \ 43 | FLOG_REQ(#option " error %d: %s", code, curl_easy_strerror(code)); \ 44 | } \ 45 | } while(0); 46 | 47 | #define GET_PTR(type, var_name, from) \ 48 | type *var_name = (type *)(from); \ 49 | if ((var_name) == NULL) { \ 50 | FLOG("Unexpected NULL pointer for " #var_name "(" #type ")"); \ 51 | } 52 | 53 | static void https_fetch_ctx_cleanup(https_client_t *client, 54 | struct https_fetch_ctx *prev, 55 | struct https_fetch_ctx *ctx, 56 | int curl_result_code); 57 | 58 | static size_t write_buffer(void *buf, size_t size, size_t nmemb, void *userp) { 59 | GET_PTR(struct https_fetch_ctx, ctx, userp); 60 | size_t write_size = size * nmemb; 61 | size_t new_size = ctx->buflen + write_size; 62 | if (new_size > DOH_MAX_RESPONSE_SIZE) { 63 | WLOG_REQ("Response size is too large!"); 64 | return 0; 65 | } 66 | char *new_buf = (char *)realloc(ctx->buf, new_size + 1); 67 | if (new_buf == NULL) { 68 | ELOG_REQ("Out of memory!"); 69 | return 0; 70 | } 71 | ctx->buf = new_buf; 72 | // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) 73 | memcpy(&(ctx->buf[ctx->buflen]), buf, write_size); 74 | ctx->buflen = new_size; 75 | // We always expect to receive valid non-null ASCII but just to be safe... 76 | ctx->buf[ctx->buflen] = '\0'; 77 | return write_size; 78 | } 79 | 80 | static curl_socket_t opensocket_callback(void *clientp, curlsocktype purpose, 81 | struct curl_sockaddr *addr) { 82 | GET_PTR(https_client_t, client, clientp); 83 | 84 | if (client->connections >= HTTPS_SOCKET_LIMIT) { 85 | ELOG("curl needed more socket, than the number of maximum sockets: %d", HTTPS_SOCKET_LIMIT); 86 | return CURL_SOCKET_BAD; 87 | } 88 | 89 | curl_socket_t sock = socket(addr->family, addr->socktype, addr->protocol); 90 | if (sock == -1) { 91 | ELOG("Could not open curl socket %d:%s", errno, strerror(errno)); 92 | return CURL_SOCKET_BAD; 93 | } 94 | 95 | DLOG("curl opened socket: %d", sock); 96 | client->connections++; 97 | 98 | if (client->stat) { 99 | stat_connection_opened(client->stat); 100 | } 101 | 102 | #if defined(IP_TOS) 103 | if (purpose != CURLSOCKTYPE_IPCXN) { 104 | return sock; 105 | } 106 | 107 | if (addr->family == AF_INET) { 108 | setsockopt(sock, IPPROTO_IP, IP_TOS, 109 | &client->opt->dscp, sizeof(client->opt->dscp)); 110 | } 111 | #if defined(IPV6_TCLASS) 112 | else if (addr->family == AF_INET6) { 113 | setsockopt(sock, IPPROTO_IPV6, IPV6_TCLASS, 114 | &client->opt->dscp, sizeof(client->opt->dscp)); 115 | } 116 | #endif 117 | #endif 118 | 119 | return sock; 120 | } 121 | 122 | static int closesocket_callback(void __attribute__((unused)) *clientp, curl_socket_t sock) 123 | { 124 | GET_PTR(https_client_t, client, clientp); 125 | 126 | if (close(sock) != 0) { 127 | ELOG("Could not close curl socket %d:%s", errno, strerror(errno)); 128 | return 1; 129 | } 130 | 131 | DLOG("curl closed socket: %d", sock); 132 | client->connections--; 133 | 134 | if (client->stat) { 135 | stat_connection_closed(client->stat); 136 | } 137 | 138 | return 0; 139 | } 140 | 141 | static void https_log_data(enum LogSeverity level, struct https_fetch_ctx *ctx, 142 | const char * prefix, char *ptr, size_t size) 143 | { 144 | const size_t width = 0x10; 145 | 146 | for (size_t i = 0; i < size; i += width) { 147 | char hex[3 * width + 1]; 148 | char str[width + 1]; 149 | size_t hex_off = 0; 150 | size_t str_off = 0; 151 | // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) 152 | memset(hex, 0, sizeof(hex)); 153 | // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) 154 | memset(str, 0, sizeof(str)); 155 | 156 | for (size_t c = 0; c < width; c++) { 157 | if (i+c < size) { 158 | // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) 159 | hex_off += snprintf(hex + hex_off, sizeof(hex) - hex_off, 160 | "%02x ", (unsigned char)ptr[i+c]); 161 | // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) 162 | str_off += snprintf(str + str_off, sizeof(str) - str_off, 163 | "%c", isprint(ptr[i+c]) ? ptr[i+c] : '.'); 164 | } else { 165 | // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) 166 | hex_off += snprintf(hex + hex_off, sizeof(hex) - hex_off, " "); 167 | } 168 | } 169 | 170 | LOG_REQ(level, "%s%4.4lx: %s%s", prefix, (long)i, hex, str); 171 | } 172 | } 173 | 174 | static 175 | int https_curl_debug(CURL __attribute__((unused)) * handle, curl_infotype type, 176 | char *data, size_t size, void *userp) 177 | { 178 | GET_PTR(struct https_fetch_ctx, ctx, userp); 179 | const char *prefix = NULL; 180 | 181 | switch (type) { 182 | case CURLINFO_TEXT: 183 | prefix = "* "; 184 | break; 185 | case CURLINFO_HEADER_OUT: 186 | prefix = "> "; 187 | break; 188 | case CURLINFO_HEADER_IN: 189 | prefix = "< "; 190 | break; 191 | // not dumping DNS packets because of privacy 192 | case CURLINFO_DATA_OUT: 193 | case CURLINFO_DATA_IN: 194 | https_log_data(LOG_DEBUG, ctx, (type == CURLINFO_DATA_IN ? "< " : "> "), data, size); 195 | return 0; 196 | // uninformative 197 | case CURLINFO_SSL_DATA_OUT: 198 | case CURLINFO_SSL_DATA_IN: 199 | return 0; 200 | default: 201 | WLOG("Unhandled curl info type: %d", type); 202 | return 0; 203 | } 204 | 205 | // for extra debugging purpose 206 | // if (type != CURLINFO_TEXT) { 207 | // https_log_data(LOG_DEBUG, ctx, "", data, size); 208 | // } 209 | 210 | // process lines one-by one 211 | char *start = NULL; // start position of currently processed line 212 | for (char *pos = data; pos <= (data + size); pos++) { 213 | // tokenize by end of string and line splitting characters 214 | if (pos == (data + size) || *pos == '\r' || *pos == '\n') { 215 | // skip empty string and curl info Expire 216 | if (start != NULL && (pos - start) > 0 && 217 | strncmp(start, "Expire", sizeof("Expire") - 1) != 0) { 218 | // https_log_data(LOG_DEBUG, ctx, "", start, pos - start); 219 | DLOG_REQ("%s%.*s", prefix, pos - start, start); 220 | start = NULL; 221 | } 222 | } else if (start == NULL) { 223 | start = pos; 224 | } 225 | } 226 | return 0; 227 | } 228 | 229 | static const char * http_version_str(const long version) { 230 | switch (version) { 231 | case CURL_HTTP_VERSION_1_0: 232 | return "1.0"; 233 | case CURL_HTTP_VERSION_1_1: 234 | return "1.1"; 235 | case CURL_HTTP_VERSION_2_0: // fallthrough 236 | case CURL_HTTP_VERSION_2TLS: 237 | return "2"; 238 | case CURL_HTTP_VERSION_3: 239 | return "3"; 240 | default: 241 | FLOG("Unsupported HTTP version: %d", version); 242 | } 243 | return "UNKNOWN"; // unreachable code 244 | } 245 | 246 | static void https_set_request_version(https_client_t *client, 247 | struct https_fetch_ctx *ctx) { 248 | long http_version_int = CURL_HTTP_VERSION_2TLS; 249 | switch (client->opt->use_http_version) { 250 | case 1: 251 | http_version_int = CURL_HTTP_VERSION_1_1; 252 | // fallthrough 253 | case 2: 254 | break; 255 | case 3: 256 | http_version_int = CURL_HTTP_VERSION_3; 257 | break; 258 | default: 259 | FLOG_REQ("Invalid HTTP version: %d", client->opt->use_http_version); 260 | } 261 | DLOG_REQ("Requesting HTTP/%s", http_version_str(http_version_int)); 262 | 263 | CURLcode easy_code = curl_easy_setopt(ctx->curl, CURLOPT_HTTP_VERSION, http_version_int); 264 | if (easy_code != CURLE_OK) { 265 | ELOG_REQ("Setting HTTP/%s version failed with %d: %s", 266 | http_version_str(http_version_int), easy_code, curl_easy_strerror(easy_code)); 267 | 268 | if (client->opt->use_http_version == 3) { 269 | ELOG("Try to run application without -q argument!"); // fallback unknown for current request 270 | client->opt->use_http_version = 2; 271 | } else if (client->opt->use_http_version == 2) { 272 | ELOG("Try to run application with -x argument! Falling back to HTTP/1.1 version."); 273 | client->opt->use_http_version = 1; 274 | } 275 | } 276 | } 277 | 278 | static void https_fetch_ctx_init(https_client_t *client, 279 | struct https_fetch_ctx *ctx, const char *url, 280 | const char* data, size_t datalen, 281 | struct curl_slist *resolv, uint16_t id, 282 | https_response_cb cb, void *cb_data) { 283 | ctx->curl = curl_easy_init(); // if fails, first setopt will fail 284 | ctx->id = id; 285 | ctx->cb = cb; 286 | ctx->cb_data = cb_data; 287 | ctx->buf = NULL; 288 | ctx->buflen = 0; 289 | ctx->next = client->fetches; 290 | client->fetches = ctx; 291 | 292 | ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_RESOLVE, resolv); 293 | 294 | https_set_request_version(client, ctx); 295 | 296 | if (logging_debug_enabled()) { 297 | ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_VERBOSE, 1L); 298 | ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_DEBUGFUNCTION, https_curl_debug); 299 | ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_DEBUGDATA, ctx); 300 | } 301 | ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_OPENSOCKETFUNCTION, opensocket_callback); 302 | ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_OPENSOCKETDATA, client); 303 | ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_CLOSESOCKETFUNCTION, closesocket_callback); 304 | ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_CLOSESOCKETDATA, client); 305 | ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_URL, url); 306 | ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_HTTPHEADER, client->header_list); 307 | ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_POSTFIELDSIZE, datalen); 308 | ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_POSTFIELDS, data); 309 | ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_WRITEFUNCTION, &write_buffer); 310 | ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_WRITEDATA, ctx); 311 | ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_MAXAGE_CONN, client->opt->max_idle_time); 312 | ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_PIPEWAIT, client->opt->use_http_version > 1); 313 | ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_USERAGENT, "https_dns_proxy/0.3"); 314 | ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_FOLLOWLOCATION, 0); 315 | ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_NOSIGNAL, 0); 316 | ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_TIMEOUT, client->connections > 0 ? 5 : 10 /* seconds */); 317 | // We know Google supports this, so force it. 318 | ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); 319 | ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_ERRORBUFFER, ctx->curl_errbuf); // zeroed by calloc 320 | if (client->opt->curl_proxy) { 321 | DLOG_REQ("Using curl proxy: %s", client->opt->curl_proxy); 322 | ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_PROXY, client->opt->curl_proxy); 323 | } 324 | if (client->opt->ca_info) { 325 | ASSERT_CURL_EASY_SETOPT(ctx, CURLOPT_CAINFO, client->opt->ca_info); 326 | } 327 | CURLMcode multi_code = curl_multi_add_handle(client->curlm, ctx->curl); 328 | if (multi_code != CURLM_OK) { 329 | ELOG_REQ("curl_multi_add_handle error %d: %s", multi_code, curl_multi_strerror(multi_code)); 330 | if (multi_code == CURLM_ABORTED_BY_CALLBACK) { 331 | WLOG_REQ("Resetting HTTPS client to recover from faulty state!"); 332 | https_client_reset(client); 333 | } else { 334 | https_fetch_ctx_cleanup(client, NULL, client->fetches, -1); // dropping current failed request 335 | } 336 | } 337 | } 338 | 339 | static int https_fetch_ctx_process_response(https_client_t *client, 340 | struct https_fetch_ctx *ctx, 341 | int curl_result_code) 342 | { 343 | CURLcode res = 0; 344 | long long_resp = 0; 345 | char *str_resp = NULL; 346 | int faulty_response = 1; 347 | 348 | switch (curl_result_code) { 349 | case CURLE_OK: 350 | DLOG_REQ("curl request succeeded"); 351 | faulty_response = 0; 352 | break; 353 | case CURLE_WRITE_ERROR: 354 | WLOG_REQ("curl request failed with write error (probably response content was too large)"); 355 | break; 356 | default: 357 | WLOG_REQ("curl request failed with %d: %s", res, curl_easy_strerror(res)); 358 | if (ctx->curl_errbuf[0] != 0) { 359 | WLOG_REQ("curl error message: %s", ctx->curl_errbuf); 360 | } 361 | } 362 | 363 | res = curl_easy_getinfo(ctx->curl, CURLINFO_RESPONSE_CODE, &long_resp); 364 | if (res != CURLE_OK) { 365 | ELOG_REQ("CURLINFO_RESPONSE_CODE: %s", curl_easy_strerror(res)); 366 | faulty_response = 1; 367 | } else if (long_resp != 200) { 368 | faulty_response = 1; 369 | if (long_resp == 0) { 370 | curl_off_t uploaded_bytes = 0; 371 | if (curl_easy_getinfo(ctx->curl, CURLINFO_SIZE_UPLOAD_T, &uploaded_bytes) == CURLE_OK && 372 | uploaded_bytes > 0) { 373 | WLOG_REQ("Connecting and sending request to resolver was successful, " 374 | "but no response was sent back"); 375 | if (client->opt->use_http_version == 1) { 376 | // for example Unbound DoH servers does not support HTTP/1.x, only HTTP/2 377 | WLOG("Resolver may not support current HTTP/%s protocol version", 378 | http_version_str(client->opt->use_http_version)); 379 | } 380 | } else { 381 | // in case of HTTP/1.1 this can happen very often depending on DNS query frequency 382 | // example: server side closes the connection or curl force closes connections 383 | // that have been opened a long time ago (if CURLOPT_MAXAGE_CONN can not be increased 384 | // it is 118 seconds) 385 | // also: when no internet connection, this floods the log for every failed request 386 | WLOG_REQ("No response (probably connection has been closed or timed out)"); 387 | } 388 | } else { 389 | WLOG_REQ("curl response code: %d, content length: %zu", long_resp, ctx->buflen); 390 | if (ctx->buflen > 0) { 391 | https_log_data(LOG_WARNING, ctx, "", ctx->buf, ctx->buflen); 392 | } 393 | } 394 | } 395 | 396 | if (!faulty_response) 397 | { 398 | res = curl_easy_getinfo(ctx->curl, CURLINFO_CONTENT_TYPE, &str_resp); 399 | if (res != CURLE_OK) { 400 | ELOG_REQ("CURLINFO_CONTENT_TYPE: %s", curl_easy_strerror(res)); 401 | } else if (str_resp == NULL || 402 | strncmp(str_resp, DOH_CONTENT_TYPE, sizeof(DOH_CONTENT_TYPE) - 1) != 0) { // at least, start with it 403 | WLOG_REQ("Invalid response Content-Type: %s", str_resp ? str_resp : "UNSET"); 404 | faulty_response = 1; 405 | } 406 | } 407 | 408 | if (logging_debug_enabled() || faulty_response || ctx->buflen == 0) { 409 | res = curl_easy_getinfo(ctx->curl, CURLINFO_REDIRECT_URL, &str_resp); 410 | if (res != CURLE_OK) { 411 | ELOG_REQ("CURLINFO_REDIRECT_URL: %s", curl_easy_strerror(res)); 412 | } else if (str_resp != NULL) { 413 | WLOG_REQ("Request would be redirected to: %s", str_resp); 414 | if (strcmp(str_resp, client->opt->resolver_url) != 0) { 415 | WLOG("Please update Resolver URL to avoid redirection!"); 416 | } 417 | } 418 | 419 | res = curl_easy_getinfo(ctx->curl, CURLINFO_SSL_VERIFYRESULT, &long_resp); 420 | if (res != CURLE_OK) { 421 | ELOG_REQ("CURLINFO_SSL_VERIFYRESULT: %s", curl_easy_strerror(res)); 422 | } else if (long_resp != CURLE_OK) { 423 | WLOG_REQ("CURLINFO_SSL_VERIFYRESULT: %s", curl_easy_strerror(long_resp)); 424 | } 425 | 426 | res = curl_easy_getinfo(ctx->curl, CURLINFO_OS_ERRNO, &long_resp); 427 | if (res != CURLE_OK) { 428 | ELOG_REQ("CURLINFO_OS_ERRNO: %s", curl_easy_strerror(res)); 429 | } else if (long_resp != 0) { 430 | WLOG_REQ("CURLINFO_OS_ERRNO: %d %s", long_resp, strerror(long_resp)); 431 | if (long_resp == ENETUNREACH && !client->opt->ipv4) { 432 | // this can't be fixed here with option overwrite because of dns_poller 433 | WLOG("Try to run application with -4 argument!"); 434 | } 435 | } 436 | } 437 | 438 | if (logging_debug_enabled() || client->stat) { 439 | res = curl_easy_getinfo(ctx->curl, CURLINFO_NUM_CONNECTS , &long_resp); 440 | if (res != CURLE_OK) { 441 | ELOG_REQ("CURLINFO_NUM_CONNECTS: %s", curl_easy_strerror(res)); 442 | } else { 443 | DLOG_REQ("CURLINFO_NUM_CONNECTS: %d", long_resp); 444 | if (long_resp == 0 && client->stat) { 445 | stat_connection_reused(client->stat); 446 | } 447 | } 448 | } 449 | 450 | if (logging_debug_enabled()) { 451 | res = curl_easy_getinfo(ctx->curl, CURLINFO_EFFECTIVE_URL, &str_resp); 452 | if (res != CURLE_OK) { 453 | ELOG_REQ("CURLINFO_EFFECTIVE_URL: %s", curl_easy_strerror(res)); 454 | } else { 455 | DLOG_REQ("CURLINFO_EFFECTIVE_URL: %s", str_resp); 456 | } 457 | 458 | res = curl_easy_getinfo(ctx->curl, CURLINFO_HTTP_VERSION, &long_resp); 459 | if (res != CURLE_OK) { 460 | ELOG_REQ("CURLINFO_HTTP_VERSION: %s", curl_easy_strerror(res)); 461 | } else if (long_resp != CURL_HTTP_VERSION_NONE) { 462 | DLOG_REQ("CURLINFO_HTTP_VERSION: %s", http_version_str(long_resp)); 463 | } 464 | 465 | res = curl_easy_getinfo(ctx->curl, CURLINFO_SCHEME, &str_resp); 466 | if (res != CURLE_OK) { 467 | ELOG_REQ("CURLINFO_SCHEME: %s", curl_easy_strerror(res)); 468 | } else if (strcasecmp(str_resp, "https") != 0) { 469 | DLOG_REQ("CURLINFO_SCHEME: %s", str_resp); 470 | } 471 | 472 | double namelookup_time = NAN; 473 | double connect_time = NAN; 474 | double appconnect_time = NAN; 475 | double pretransfer_time = NAN; 476 | double starttransfer_time = NAN; 477 | double total_time = NAN; 478 | if (curl_easy_getinfo(ctx->curl, 479 | CURLINFO_NAMELOOKUP_TIME, &namelookup_time) != CURLE_OK || 480 | curl_easy_getinfo(ctx->curl, 481 | CURLINFO_CONNECT_TIME, &connect_time) != CURLE_OK || 482 | curl_easy_getinfo(ctx->curl, 483 | CURLINFO_APPCONNECT_TIME, &appconnect_time) != CURLE_OK || 484 | curl_easy_getinfo(ctx->curl, 485 | CURLINFO_PRETRANSFER_TIME, &pretransfer_time) != CURLE_OK || 486 | curl_easy_getinfo(ctx->curl, 487 | CURLINFO_STARTTRANSFER_TIME, &starttransfer_time) != CURLE_OK || 488 | curl_easy_getinfo(ctx->curl, 489 | CURLINFO_TOTAL_TIME, &total_time) != CURLE_OK) { 490 | ELOG_REQ("Error getting timing"); 491 | } else { 492 | DLOG_REQ("Times: %lf, %lf, %lf, %lf, %lf, %lf", 493 | namelookup_time, connect_time, appconnect_time, pretransfer_time, 494 | starttransfer_time, total_time); 495 | } 496 | } 497 | 498 | return faulty_response; 499 | } 500 | 501 | static void https_fetch_ctx_cleanup(https_client_t *client, 502 | struct https_fetch_ctx *prev, 503 | struct https_fetch_ctx *ctx, 504 | int curl_result_code) { 505 | CURLMcode code = curl_multi_remove_handle(client->curlm, ctx->curl); 506 | if (code != CURLM_OK) { 507 | FLOG_REQ("curl_multi_remove_handle error %d: %s", code, curl_multi_strerror(code)); 508 | } 509 | int drop_reply = 0; 510 | if (curl_result_code < 0) { 511 | WLOG_REQ("Request was aborted"); 512 | drop_reply = 1; 513 | } else if (https_fetch_ctx_process_response(client, ctx, curl_result_code) != 0) { 514 | ILOG_REQ("Response was faulty, skipping DNS reply"); 515 | drop_reply = 1; 516 | } 517 | if (drop_reply) { 518 | free(ctx->buf); 519 | ctx->buf = NULL; 520 | ctx->buflen = 0; 521 | } 522 | // callback must be called to avoid memleak 523 | ctx->cb(ctx->cb_data, ctx->buf, ctx->buflen); 524 | curl_easy_cleanup(ctx->curl); 525 | free(ctx->buf); 526 | if (prev) { 527 | prev->next = ctx->next; 528 | } else { 529 | client->fetches = ctx->next; 530 | } 531 | free(ctx); 532 | } 533 | 534 | static void check_multi_info(https_client_t *c) { 535 | CURLMsg *msg = NULL; 536 | int msgs_left = 0; 537 | while ((msg = curl_multi_info_read(c->curlm, &msgs_left))) { 538 | if (msg->msg == CURLMSG_DONE) { 539 | struct https_fetch_ctx *prev = NULL; 540 | struct https_fetch_ctx *cur = c->fetches; 541 | while (cur) { 542 | if (cur->curl == msg->easy_handle) { 543 | https_fetch_ctx_cleanup(c, prev, cur, msg->data.result); 544 | break; 545 | } 546 | prev = cur; 547 | cur = cur->next; 548 | } 549 | } 550 | else { 551 | ELOG("Unhandled curl message: %d", msg->msg); // unlikely 552 | } 553 | } 554 | } 555 | 556 | static void sock_cb(struct ev_loop __attribute__((unused)) *loop, 557 | struct ev_io *w, int revents) { 558 | GET_PTR(https_client_t, c, w->data); 559 | int ignore = 0; 560 | CURLMcode code = curl_multi_socket_action( 561 | c->curlm, w->fd, (revents & EV_READ ? CURL_CSELECT_IN : 0) | 562 | (revents & EV_WRITE ? CURL_CSELECT_OUT : 0), 563 | &ignore); 564 | if (code == CURLM_OK) { 565 | check_multi_info(c); 566 | } 567 | else { 568 | FLOG("curl_multi_socket_action error %d: %s", code, curl_multi_strerror(code)); 569 | if (code == CURLM_ABORTED_BY_CALLBACK) { 570 | WLOG("Resetting HTTPS client to recover from faulty state!"); 571 | https_client_reset(c); 572 | } 573 | } 574 | } 575 | 576 | static void timer_cb(struct ev_loop __attribute__((unused)) *loop, 577 | struct ev_timer *w, int __attribute__((unused)) revents) { 578 | GET_PTR(https_client_t, c, w->data); 579 | int ignore = 0; 580 | CURLMcode code = curl_multi_socket_action(c->curlm, CURL_SOCKET_TIMEOUT, 0, 581 | &ignore); 582 | if (code != CURLM_OK) { 583 | ELOG("curl_multi_socket_action error %d: %s", code, curl_multi_strerror(code)); 584 | } 585 | check_multi_info(c); 586 | } 587 | 588 | static struct ev_io * get_io_event(struct ev_io io_events[], curl_socket_t sock) { 589 | for (int i = 0; i < HTTPS_SOCKET_LIMIT; i++) { 590 | if (io_events[i].fd == sock) { 591 | return &io_events[i]; 592 | } 593 | } 594 | return NULL; 595 | } 596 | 597 | static void dump_io_events(struct ev_io io_events[]) { 598 | for (int i = 0; i < HTTPS_SOCKET_LIMIT; i++) { 599 | ILOG("IO event #%d: fd=%d, events=%d/%s%s", 600 | i+1, io_events[i].fd, io_events[i].events, 601 | (io_events[i].events & EV_READ ? "R" : ""), 602 | (io_events[i].events & EV_WRITE ? "W" : "")); 603 | } 604 | } 605 | 606 | static int multi_sock_cb(CURL *curl, curl_socket_t sock, int what, 607 | void *userp, void __attribute__((unused)) *sockp) { 608 | GET_PTR(https_client_t, c, userp); 609 | if (!curl) { 610 | FLOG("Unexpected NULL pointer for CURL"); 611 | } 612 | // stop and release used event 613 | struct ev_io *io_event_ptr = get_io_event(c->io_events, sock); 614 | if (io_event_ptr) { 615 | ev_io_stop(c->loop, io_event_ptr); 616 | io_event_ptr->fd = 0; 617 | DLOG("Released used io event: %p", io_event_ptr); 618 | } 619 | if (what == CURL_POLL_REMOVE) { 620 | return 0; 621 | } 622 | // reserve and start new event on unused slot 623 | io_event_ptr = get_io_event(c->io_events, 0); 624 | if (!io_event_ptr) { 625 | ELOG("curl needed more IO event handler, than the number of maximum sockets: %d", HTTPS_SOCKET_LIMIT); 626 | dump_io_events(c->io_events); 627 | logging_flight_recorder_dump(); 628 | return -1; 629 | } 630 | DLOG("Reserved new io event: %p", io_event_ptr); 631 | // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) 632 | ev_io_init(io_event_ptr, sock_cb, sock, 633 | ((what & CURL_POLL_IN) ? EV_READ : 0) | 634 | ((what & CURL_POLL_OUT) ? EV_WRITE : 0)); 635 | ev_io_start(c->loop, io_event_ptr); 636 | return 0; 637 | } 638 | 639 | static int multi_timer_cb(CURLM __attribute__((unused)) *multi, 640 | long timeout_ms, void *userp) { 641 | GET_PTR(https_client_t, c, userp); 642 | ev_timer_stop(c->loop, &c->timer); 643 | if (timeout_ms >= 0) { 644 | // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) 645 | ev_timer_init(&c->timer, timer_cb, timeout_ms / 1000.0, 0); 646 | ev_timer_start(c->loop, &c->timer); 647 | } 648 | return 0; 649 | } 650 | 651 | void https_client_init(https_client_t *c, options_t *opt, 652 | stat_t *stat, struct ev_loop *loop) { 653 | // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) 654 | memset(c, 0, sizeof(*c)); 655 | c->loop = loop; 656 | c->curlm = curl_multi_init(); // if fails, first setopt will fail 657 | c->header_list = curl_slist_append(curl_slist_append(NULL, 658 | "Accept: " DOH_CONTENT_TYPE), 659 | "Content-Type: " DOH_CONTENT_TYPE); 660 | c->fetches = NULL; 661 | c->timer.data = c; 662 | for (int i = 0; i < HTTPS_SOCKET_LIMIT; i++) { 663 | c->io_events[i].data = c; 664 | } 665 | c->opt = opt; 666 | c->stat = stat; 667 | 668 | ASSERT_CURL_MULTI_SETOPT(c->curlm, CURLMOPT_PIPELINING, CURLPIPE_HTTP1 | CURLPIPE_MULTIPLEX); 669 | ASSERT_CURL_MULTI_SETOPT(c->curlm, CURLMOPT_MAX_TOTAL_CONNECTIONS, HTTPS_CONNECTION_LIMIT); 670 | ASSERT_CURL_MULTI_SETOPT(c->curlm, CURLMOPT_MAX_HOST_CONNECTIONS, HTTPS_CONNECTION_LIMIT); 671 | ASSERT_CURL_MULTI_SETOPT(c->curlm, CURLMOPT_SOCKETDATA, c); 672 | ASSERT_CURL_MULTI_SETOPT(c->curlm, CURLMOPT_SOCKETFUNCTION, multi_sock_cb); 673 | ASSERT_CURL_MULTI_SETOPT(c->curlm, CURLMOPT_TIMERDATA, c); 674 | ASSERT_CURL_MULTI_SETOPT(c->curlm, CURLMOPT_TIMERFUNCTION, multi_timer_cb); 675 | } 676 | 677 | void https_client_fetch(https_client_t *c, const char *url, 678 | const char* postdata, size_t postdata_len, 679 | struct curl_slist *resolv, uint16_t id, 680 | https_response_cb cb, void *data) { 681 | struct https_fetch_ctx *ctx = 682 | (struct https_fetch_ctx *)calloc(1, sizeof(struct https_fetch_ctx)); 683 | if (!ctx) { 684 | FLOG("Out of mem"); 685 | } 686 | https_fetch_ctx_init(c, ctx, url, postdata, postdata_len, resolv, id, cb, data); 687 | } 688 | 689 | void https_client_reset(https_client_t *c) { 690 | options_t *opt = c->opt; 691 | stat_t *stat = c->stat; 692 | struct ev_loop *loop = c->loop; 693 | https_client_cleanup(c); 694 | https_client_init(c, opt, stat, loop); 695 | } 696 | 697 | void https_client_cleanup(https_client_t *c) { 698 | while (c->fetches) { 699 | https_fetch_ctx_cleanup(c, NULL, c->fetches, -1); 700 | } 701 | curl_slist_free_all(c->header_list); 702 | curl_multi_cleanup(c->curlm); 703 | } 704 | -------------------------------------------------------------------------------- /src/https_client.h: -------------------------------------------------------------------------------- 1 | #ifndef _HTTPS_CLIENT_H_ 2 | #define _HTTPS_CLIENT_H_ 3 | 4 | #include 5 | 6 | #include "options.h" 7 | #include "stat.h" 8 | 9 | enum { 10 | HTTPS_SOCKET_LIMIT = 12, 11 | HTTPS_CONNECTION_LIMIT = 8, 12 | }; 13 | 14 | // Callback type for receiving data when a transfer finishes. 15 | typedef void (*https_response_cb)(void *data, char *buf, size_t buflen); 16 | 17 | // Internal: Holds state on an individual transfer. 18 | struct https_fetch_ctx { 19 | CURL *curl; 20 | char curl_errbuf[CURL_ERROR_SIZE]; 21 | 22 | uint16_t id; 23 | 24 | https_response_cb cb; 25 | void *cb_data; 26 | 27 | char *buf; 28 | size_t buflen; 29 | 30 | struct https_fetch_ctx *next; 31 | }; 32 | 33 | // Holds state on the whole multiplexed CURL machine. 34 | typedef struct { 35 | struct ev_loop *loop; 36 | CURLM *curlm; 37 | struct curl_slist *header_list; 38 | struct https_fetch_ctx *fetches; 39 | 40 | ev_timer timer; 41 | ev_io io_events[HTTPS_SOCKET_LIMIT]; 42 | int connections; 43 | 44 | options_t *opt; 45 | stat_t *stat; 46 | } https_client_t; 47 | 48 | void https_client_init(https_client_t *c, options_t *opt, 49 | stat_t *stat, struct ev_loop *loop); 50 | 51 | void https_client_fetch(https_client_t *c, const char *url, 52 | const char* postdata, size_t postdata_len, 53 | struct curl_slist *resolv, uint16_t id, 54 | https_response_cb cb, void *data); 55 | 56 | // Used to reset state of libcurl because streaming connections + IP changes 57 | // seem to cause curl to flip out. 58 | void https_client_reset(https_client_t *c); 59 | 60 | void https_client_cleanup(https_client_t *c); 61 | 62 | #endif // _HTTPS_CLIENT_H_ 63 | -------------------------------------------------------------------------------- /src/logging.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 5 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 6 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 7 | 8 | #include "logging.h" 9 | #include "ring_buffer.h" 10 | 11 | // logs of this severity or higher are flushed immediately after write 12 | #define LOG_FLUSH_LEVEL LOG_WARNING 13 | enum { 14 | LOG_LINE_SIZE = 2048 // Log line should be at least 100 15 | }; 16 | 17 | static FILE *logfile = NULL; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) 18 | static int loglevel = LOG_ERROR; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) 19 | static ev_timer logging_timer; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) 20 | static ev_signal sigusr2; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) 21 | static struct ring_buffer * flight_recorder = NULL; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) 22 | 23 | static const char * const SeverityStr[] = { 24 | "[D]", 25 | "[I]", 26 | "[W]", 27 | "[E]", 28 | "[S]", 29 | "[F]" 30 | }; 31 | 32 | void logging_timer_cb(struct ev_loop __attribute__((unused)) *loop, 33 | ev_timer __attribute__((unused)) *w, 34 | int __attribute__((unused)) revents) { 35 | if (logfile) { 36 | (void)fflush(logfile); 37 | } 38 | } 39 | 40 | void logging_flight_recorder_dump(void) { 41 | if (flight_recorder) { 42 | ILOG("Flight recorder dump"); // will be also at the end of the dump :) 43 | ring_buffer_dump(flight_recorder, logfile); 44 | } else { 45 | ILOG("Flight recorder is disabled"); 46 | } 47 | } 48 | 49 | static void logging_flight_recorder_dump_cb(struct ev_loop __attribute__((unused)) *loop, 50 | ev_signal __attribute__((__unused__)) *w, 51 | int __attribute__((__unused__)) revents) { 52 | logging_flight_recorder_dump(); 53 | } 54 | 55 | void logging_events_init(struct ev_loop *loop) { 56 | /* don't start timer if we will never write messages that are not flushed */ 57 | if (loglevel < LOG_FLUSH_LEVEL) { 58 | DLOG("starting periodic log flush timer"); 59 | // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) 60 | ev_timer_init(&logging_timer, logging_timer_cb, 0, 10); 61 | ev_timer_start(loop, &logging_timer); 62 | } 63 | 64 | DLOG("starting SIGUSR2 handler"); 65 | ev_signal_init(&sigusr2, logging_flight_recorder_dump_cb, SIGUSR2); 66 | ev_signal_start(loop, &sigusr2); 67 | } 68 | 69 | void logging_events_cleanup(struct ev_loop *loop) { 70 | ev_timer_stop(loop, &logging_timer); 71 | ev_signal_stop(loop, &sigusr2); 72 | } 73 | 74 | void logging_init(int fd, int level, uint32_t flight_recorder_size) { 75 | if (logfile) { 76 | (void)fclose(logfile); 77 | } 78 | logfile = fdopen(fd, "a"); 79 | loglevel = level; 80 | 81 | ring_buffer_init(&flight_recorder, flight_recorder_size); 82 | } 83 | 84 | void logging_cleanup(void) { 85 | if (flight_recorder) { 86 | ring_buffer_free(&flight_recorder); 87 | flight_recorder = NULL; 88 | } 89 | 90 | if (logfile) { 91 | (void)fclose(logfile); 92 | } 93 | logfile = NULL; 94 | } 95 | 96 | int logging_debug_enabled(void) { 97 | return loglevel <= LOG_DEBUG || flight_recorder; 98 | } 99 | 100 | // NOLINTNEXTLINE(misc-no-recursion) because of severity check 101 | void _log(const char *file, int line, int severity, const char *fmt, ...) { 102 | if (severity < loglevel && !flight_recorder) { 103 | return; 104 | } 105 | if (severity < 0 || severity >= LOG_MAX) { 106 | FLOG("Unknown log severity: %d", severity); 107 | } 108 | if (!logfile) { 109 | logfile = fdopen(STDOUT_FILENO, "w"); 110 | } 111 | 112 | struct timeval tv; 113 | gettimeofday(&tv, NULL); 114 | 115 | char buff[LOG_LINE_SIZE]; 116 | uint32_t buff_pos = 0; 117 | 118 | // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) 119 | int chars = snprintf(buff, LOG_LINE_SIZE, "%s %8"PRIu64".%06"PRIu64" %s:%d ", 120 | SeverityStr[severity], (uint64_t)tv.tv_sec, (uint64_t)tv.tv_usec, file, line); 121 | if (chars < 0 || chars >= LOG_LINE_SIZE/2) { 122 | abort(); // must be impossible 123 | } 124 | buff_pos += chars; 125 | 126 | va_list args; 127 | va_start(args, fmt); 128 | // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) 129 | chars = vsnprintf(buff + buff_pos, LOG_LINE_SIZE - buff_pos, fmt, args); 130 | va_end(args); 131 | 132 | if (chars < 0) { 133 | abort(); // must be impossible 134 | } 135 | buff_pos += chars; 136 | if (buff_pos >= LOG_LINE_SIZE) { 137 | buff_pos = LOG_LINE_SIZE - 1; 138 | buff[buff_pos - 1] = '$'; // indicate truncation 139 | } 140 | 141 | if (flight_recorder) { 142 | ring_buffer_push_back(flight_recorder, buff, buff_pos); 143 | } 144 | 145 | if (severity < loglevel) { 146 | return; 147 | } 148 | 149 | // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) 150 | (void)fprintf(logfile, "%s\n", buff); 151 | 152 | if (severity >= LOG_FLUSH_LEVEL) { 153 | (void)fflush(logfile); 154 | } 155 | if (severity == LOG_FATAL) { 156 | if (flight_recorder) { 157 | ring_buffer_dump(flight_recorder, logfile); 158 | } 159 | #ifdef DEBUG 160 | abort(); 161 | #else 162 | exit(1); 163 | #endif 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/logging.h: -------------------------------------------------------------------------------- 1 | #ifndef _LOGGING_H_ 2 | #define _LOGGING_H_ 3 | 4 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 5 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 6 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 7 | 8 | #ifdef __cplusplus 9 | extern "C" { 10 | #endif 11 | 12 | // Initializes logging. 13 | // Writes logs to descriptor 'fd' for log levels above or equal to 'level'. 14 | void logging_init(int fd, int level, unsigned flight_recorder_size); 15 | 16 | // Initialize periodic timer to flush logs. 17 | void logging_events_init(struct ev_loop *loop); 18 | void logging_events_cleanup(struct ev_loop *loop); 19 | 20 | // Cleans up and flushes open logs. 21 | void logging_cleanup(void); 22 | 23 | // Returns 1 if debug logging is enabled. 24 | int logging_debug_enabled(void); 25 | 26 | // Dump flight recorder. 27 | void logging_flight_recorder_dump(void); 28 | 29 | // Internal. Don't use. 30 | void _log(const char *file, int line, int severity, const char *fmt, ...); 31 | 32 | #ifdef __cplusplus 33 | } 34 | #endif 35 | 36 | enum LogSeverity { 37 | LOG_DEBUG, 38 | LOG_INFO, 39 | LOG_WARNING, 40 | LOG_ERROR, 41 | LOG_STATS, 42 | LOG_FATAL, 43 | LOG_MAX 44 | }; 45 | 46 | #define LOG(level, ...) _log(__FILENAME__, __LINE__, level, __VA_ARGS__) 47 | #define DLOG(...) _log(__FILENAME__, __LINE__, LOG_DEBUG, __VA_ARGS__) 48 | #define ILOG(...) _log(__FILENAME__, __LINE__, LOG_INFO, __VA_ARGS__) 49 | #define WLOG(...) _log(__FILENAME__, __LINE__, LOG_WARNING, __VA_ARGS__) 50 | #define ELOG(...) _log(__FILENAME__, __LINE__, LOG_ERROR, __VA_ARGS__) 51 | #define SLOG(...) _log(__FILENAME__, __LINE__, LOG_STATS, __VA_ARGS__) 52 | #define FLOG(...) do { \ 53 | _log(__FILENAME__, __LINE__, LOG_FATAL, __VA_ARGS__); \ 54 | exit(1); /* for clang-tidy! */ \ 55 | } while(0) 56 | 57 | #endif // _LOGGING_H_ 58 | -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | // Simple UDP-to-HTTPS DNS Proxy 2 | // (C) 2016 Aaron Drew 3 | 4 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 5 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 6 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 7 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 8 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 9 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 10 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 11 | 12 | #include "dns_poller.h" 13 | #include "dns_server.h" 14 | #include "https_client.h" 15 | #include "logging.h" 16 | #include "options.h" 17 | #include "stat.h" 18 | 19 | // Holds app state required for dns_server_cb. 20 | // NOLINTNEXTLINE(altera-struct-pack-align) 21 | typedef struct { 22 | https_client_t *https_client; 23 | struct curl_slist *resolv; 24 | const char *resolver_url; 25 | stat_t *stat; 26 | uint8_t using_dns_poller; 27 | } app_state_t; 28 | 29 | // NOLINTNEXTLINE(altera-struct-pack-align) 30 | typedef struct { 31 | dns_server_t *dns_server; 32 | char* dns_req; 33 | stat_t *stat; 34 | ev_tstamp start_tstamp; 35 | uint16_t tx_id; 36 | struct sockaddr_storage raddr; 37 | } request_t; 38 | 39 | static int is_ipv4_address(char *str) { 40 | struct in6_addr addr; 41 | return inet_pton(AF_INET, str, &addr) == 1; 42 | } 43 | 44 | static int hostname_from_url(const char* url_in, 45 | char* hostname, const size_t hostname_len) { 46 | int res = 0; 47 | CURLU *url = curl_url(); 48 | if (url != NULL) { 49 | CURLUcode rc = curl_url_set(url, CURLUPART_URL, url_in, 0); 50 | if (rc == CURLUE_OK) { 51 | char *host = NULL; 52 | rc = curl_url_get(url, CURLUPART_HOST, &host, 0); 53 | const size_t host_len = strlen(host); 54 | if (rc == CURLUE_OK && host_len < hostname_len && 55 | host[0] != '[' && host[host_len-1] != ']' && // skip IPv6 address 56 | !is_ipv4_address(host)) { 57 | strncpy(hostname, host, hostname_len-1); // NOLINT(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) 58 | hostname[hostname_len-1] = '\0'; 59 | res = 1; // success 60 | } 61 | curl_free(host); 62 | } 63 | curl_url_cleanup(url); 64 | } 65 | return res; 66 | } 67 | 68 | static void signal_shutdown_cb(struct ev_loop *loop, 69 | ev_signal __attribute__((__unused__)) *w, 70 | int __attribute__((__unused__)) revents) { 71 | ILOG("Shutting down gracefully. To force exit, send signal again."); 72 | ev_break(loop, EVBREAK_ALL); 73 | } 74 | 75 | static void sigpipe_cb(struct ev_loop __attribute__((__unused__)) *loop, 76 | ev_signal __attribute__((__unused__)) *w, 77 | int __attribute__((__unused__)) revents) { 78 | ELOG("Received SIGPIPE. Ignoring."); 79 | } 80 | 81 | static void https_resp_cb(void *data, char *buf, size_t buflen) { 82 | request_t *req = (request_t *)data; 83 | DLOG("Received response for id: %hX, len: %zu", req->tx_id, buflen); 84 | if (req == NULL) { 85 | FLOG("%04hX: data NULL", req->tx_id); 86 | } 87 | free((void*)req->dns_req); 88 | if (buf != NULL) { // May be NULL for timeout, DNS failure, or something similar. 89 | if (buflen < (int)sizeof(uint16_t)) { 90 | WLOG("%04hX: Malformed response received (too short)", req->tx_id); 91 | } else { 92 | uint16_t response_id = ntohs(*((uint16_t*)buf)); 93 | if (req->tx_id != response_id) { 94 | WLOG("DNS request and response IDs are not matching: %hX != %hX", 95 | req->tx_id, response_id); 96 | } else { 97 | dns_server_respond(req->dns_server, (struct sockaddr*)&req->raddr, buf, buflen); 98 | if (req->stat) { 99 | stat_request_end(req->stat, buflen, ev_now(req->dns_server->loop) - req->start_tstamp); 100 | } 101 | } 102 | } 103 | } 104 | free(req); 105 | } 106 | 107 | static void dns_server_cb(dns_server_t *dns_server, void *data, 108 | struct sockaddr* addr, uint16_t tx_id, 109 | char *dns_req, size_t dns_req_len) { 110 | app_state_t *app = (app_state_t *)data; 111 | 112 | DLOG("Received request for id: %hX, len: %d", tx_id, dns_req_len); 113 | 114 | // If we're not yet bootstrapped, don't answer. libcurl will fall back to 115 | // gethostbyname() which can cause a DNS loop due to the nameserver listed 116 | // in resolv.conf being or depending on https_dns_proxy itself. 117 | if(app->using_dns_poller && (app->resolv == NULL || app->resolv->data == NULL)) { 118 | WLOG("%04hX: Query received before bootstrapping is completed, discarding.", tx_id); 119 | free(dns_req); 120 | return; 121 | } 122 | 123 | request_t *req = (request_t *)calloc(1, sizeof(request_t)); 124 | if (req == NULL) { 125 | FLOG("%04hX: Out of mem", tx_id); 126 | } 127 | req->tx_id = tx_id; 128 | memcpy(&req->raddr, addr, dns_server->addrlen); // NOLINT(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) 129 | req->dns_server = dns_server; 130 | req->dns_req = dns_req; // To free buffer after https request is complete. 131 | req->start_tstamp = ev_now(dns_server->loop); 132 | req->stat = app->stat; 133 | 134 | if (req->stat) { 135 | stat_request_begin(app->stat, dns_req_len); 136 | } 137 | https_client_fetch(app->https_client, app->resolver_url, 138 | dns_req, dns_req_len, app->resolv, req->tx_id, https_resp_cb, req); 139 | } 140 | 141 | static int addr_list_reduced(const char* full_list, const char* list) { 142 | const char *pos = list; 143 | const char *end = list + strlen(list); 144 | while (pos < end) { 145 | char current[50]; 146 | const char *comma = strchr(pos, ','); 147 | size_t ip_len = comma ? comma - pos : end - pos; 148 | strncpy(current, pos, ip_len); // NOLINT(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) 149 | current[ip_len] = '\0'; 150 | 151 | const char *match_begin = strstr(full_list, current); 152 | if (!match_begin || 153 | !(match_begin == full_list || *(match_begin - 1) == ',') || 154 | !(*(match_begin + ip_len) == ',' || *(match_begin + ip_len) == '\0')) { 155 | DLOG("IP address missing: %s", current); 156 | return 1; 157 | } 158 | 159 | pos += ip_len + 1; 160 | } 161 | return 0; 162 | } 163 | 164 | static void dns_poll_cb(const char* hostname, void *data, 165 | const char* addr_list) { 166 | app_state_t *app = (app_state_t *)data; 167 | char buf[255 + (sizeof(":443:") - 1) + POLLER_ADDR_LIST_SIZE]; 168 | memset(buf, 0, sizeof(buf)); // NOLINT(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) 169 | if (strlen(hostname) > 254) { FLOG("Hostname too long."); } 170 | int ip_start = snprintf(buf, sizeof(buf) - 1, "%s:443:", hostname); // NOLINT(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) 171 | (void)snprintf(buf + ip_start, sizeof(buf) - 1 - ip_start, "%s", addr_list); // NOLINT(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) 172 | if (app->resolv && app->resolv->data) { 173 | char * old_addr_list = strstr(app->resolv->data, ":443:"); 174 | if (old_addr_list) { 175 | old_addr_list += sizeof(":443:") - 1; 176 | if (!addr_list_reduced(addr_list, old_addr_list)) { 177 | DLOG("DNS server IP address unchanged (%s).", buf + ip_start); 178 | free((void*)addr_list); 179 | return; 180 | } 181 | } 182 | } 183 | free((void*)addr_list); 184 | DLOG("Received new DNS server IP '%s'", buf + ip_start); 185 | curl_slist_free_all(app->resolv); 186 | app->resolv = curl_slist_append(NULL, buf); 187 | // Resets curl or it gets in a mess due to IP of streaming connection not 188 | // matching that of configured DNS. 189 | https_client_reset(app->https_client); 190 | } 191 | 192 | static int proxy_supports_name_resolution(const char *proxy) 193 | { 194 | size_t i = 0; 195 | const char *ptypes[] = {"http:", "https:", "socks4a:", "socks5h:"}; 196 | 197 | if (proxy == NULL) { 198 | return 0; 199 | } 200 | for (i = 0; i < sizeof(ptypes) / sizeof(*ptypes); i++) { 201 | if (strncasecmp(proxy, ptypes[i], strlen(ptypes[i])) == 0) { 202 | return 1; 203 | } 204 | } 205 | return 0; 206 | } 207 | 208 | static const char * sw_version(void) { 209 | #ifdef SW_VERSION 210 | return SW_VERSION; 211 | #else 212 | return "2025.5.10-atLeast"; // update date sometimes, like 1-2 times a year 213 | #endif 214 | } 215 | 216 | int main(int argc, char *argv[]) { 217 | struct Options opt; 218 | options_init(&opt); 219 | switch (options_parse_args(&opt, argc, argv)) { 220 | case OPR_SUCCESS: 221 | break; 222 | case OPR_HELP: 223 | options_show_usage(argc, argv); 224 | exit(0); // asking for help is not a problem 225 | case OPR_VERSION: { 226 | printf("%s\n", sw_version()); 227 | CURLcode init_res = curl_global_init(CURL_GLOBAL_DEFAULT); 228 | curl_version_info_data *curl_ver = curl_version_info(CURLVERSION_NOW); 229 | if (init_res == CURLE_OK && curl_ver != NULL) { 230 | printf("Using: ev/%d.%d c-ares/%s %s\n", 231 | ev_version_major(), ev_version_minor(), 232 | ares_version(NULL), curl_version()); 233 | printf("Features: %s%s%s%s\n", 234 | curl_ver->features & CURL_VERSION_HTTP2 ? "HTTP2 " : "", 235 | curl_ver->features & CURL_VERSION_HTTP3 ? "HTTP3 " : "", 236 | curl_ver->features & CURL_VERSION_HTTPS_PROXY ? "HTTPS-proxy " : "", 237 | curl_ver->features & CURL_VERSION_IPV6 ? "IPv6" : ""); 238 | exit(0); 239 | } else { 240 | printf("\nFailed to get curl version info!\n"); 241 | exit(1); 242 | } 243 | } 244 | case OPR_PARSING_ERROR: 245 | printf("Failed to parse options!\n"); 246 | // fallthrough 247 | case OPR_OPTION_ERROR: 248 | printf("\n"); 249 | options_show_usage(argc, argv); 250 | exit(1); 251 | default: 252 | abort(); // must not happen 253 | } 254 | 255 | logging_init(opt.logfd, opt.loglevel, opt.flight_recorder_size); 256 | 257 | ILOG("Version: %s", sw_version()); 258 | ILOG("Built: " __DATE__ " " __TIME__); 259 | ILOG("System ev library: %d.%d", ev_version_major(), ev_version_minor()); 260 | ILOG("System c-ares library: %s", ares_version(NULL)); 261 | ILOG("System curl library: %s", curl_version()); 262 | 263 | // Note: curl intentionally uses uninitialized stack variables and similar 264 | // tricks to increase it's entropy pool. This confuses valgrind and leaks 265 | // through to errors about use of uninitialized values in our code. :( 266 | CURLcode code = curl_global_init(CURL_GLOBAL_DEFAULT); 267 | if (code != CURLE_OK) { 268 | FLOG("Failed to initialize curl, error code %d: %s", 269 | code, curl_easy_strerror(code)); 270 | } 271 | 272 | curl_version_info_data *curl_ver = curl_version_info(CURLVERSION_NOW); 273 | if (curl_ver == NULL) { 274 | FLOG("Failed to get curl version info!"); 275 | } 276 | if (!(curl_ver->features & CURL_VERSION_HTTP2)) { 277 | WLOG("HTTP/2 is not supported by current libcurl"); 278 | } 279 | if (!(curl_ver->features & CURL_VERSION_HTTP3)) { 280 | WLOG("HTTP/3 is not supported by current libcurl"); 281 | } 282 | if (!(curl_ver->features & CURL_VERSION_IPV6)) { 283 | WLOG("IPv6 is not supported by current libcurl"); 284 | } 285 | 286 | // Note: This calls ev_default_loop(0) which never cleans up. 287 | // valgrind will report a leak. :( 288 | struct ev_loop *loop = EV_DEFAULT; 289 | 290 | stat_t stat; 291 | stat_init(&stat, loop, opt.stats_interval); 292 | 293 | https_client_t https_client; 294 | https_client_init(&https_client, &opt, (opt.stats_interval ? &stat : NULL), loop); 295 | 296 | app_state_t app; 297 | app.https_client = &https_client; 298 | app.resolv = NULL; 299 | app.resolver_url = opt.resolver_url; 300 | app.using_dns_poller = 0; 301 | app.stat = (opt.stats_interval ? &stat : NULL); 302 | 303 | dns_server_t dns_server; 304 | dns_server_init(&dns_server, loop, opt.listen_addr, opt.listen_port, 305 | dns_server_cb, &app); 306 | 307 | if (opt.gid != (uid_t)-1 && setgroups(1, &opt.gid)) { 308 | FLOG("Failed to set groups"); 309 | } 310 | if (opt.gid != (uid_t)-1 && setgid(opt.gid)) { 311 | FLOG("Failed to set gid"); 312 | } 313 | if (opt.uid != (uid_t)-1 && setuid(opt.uid)) { 314 | FLOG("Failed to set uid"); 315 | } 316 | 317 | if (opt.daemonize) { 318 | // daemon() is non-standard. If needed, see OpenSSH openbsd-compat/daemon.c 319 | if (daemon(0, 0) == -1) { 320 | FLOG("daemon failed: %s", strerror(errno)); 321 | } 322 | } 323 | 324 | ev_signal sigpipe; 325 | // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) 326 | ev_signal_init(&sigpipe, sigpipe_cb, SIGPIPE); 327 | ev_signal_start(loop, &sigpipe); 328 | 329 | ev_signal sigint; 330 | // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) 331 | ev_signal_init(&sigint, signal_shutdown_cb, SIGINT); 332 | ev_signal_start(loop, &sigint); 333 | 334 | ev_signal sigterm; 335 | // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) 336 | ev_signal_init(&sigterm, signal_shutdown_cb, SIGTERM); 337 | ev_signal_start(loop, &sigterm); 338 | 339 | logging_events_init(loop); 340 | 341 | dns_poller_t dns_poller; 342 | char hostname[255] = {0}; // Domain names shouldn't exceed 253 chars. 343 | if (!proxy_supports_name_resolution(opt.curl_proxy)) { 344 | if (hostname_from_url(opt.resolver_url, hostname, sizeof(hostname))) { 345 | app.using_dns_poller = 1; 346 | dns_poller_init(&dns_poller, loop, opt.bootstrap_dns, 347 | opt.bootstrap_dns_polling_interval, hostname, 348 | opt.ipv4 ? AF_INET : AF_UNSPEC, 349 | dns_poll_cb, &app); 350 | ILOG("DNS polling initialized for '%s'", hostname); 351 | } else { 352 | ILOG("Resolver prefix '%s' doesn't appear to contain a " 353 | "hostname. DNS polling disabled.", opt.resolver_url); 354 | } 355 | } 356 | 357 | ev_run(loop, 0); 358 | DLOG("loop breaked"); 359 | 360 | if (app.using_dns_poller) { 361 | dns_poller_cleanup(&dns_poller); 362 | } 363 | curl_slist_free_all(app.resolv); 364 | 365 | logging_events_cleanup(loop); 366 | ev_signal_stop(loop, &sigterm); 367 | ev_signal_stop(loop, &sigint); 368 | ev_signal_stop(loop, &sigpipe); 369 | dns_server_stop(&dns_server); 370 | stat_stop(&stat); 371 | 372 | DLOG("re-entering loop"); 373 | ev_run(loop, 0); 374 | DLOG("loop finished all events"); 375 | 376 | dns_server_cleanup(&dns_server); 377 | https_client_cleanup(&https_client); 378 | stat_cleanup(&stat); 379 | 380 | ev_loop_destroy(loop); 381 | DLOG("loop destroyed"); 382 | 383 | curl_global_cleanup(); 384 | logging_cleanup(); 385 | options_cleanup(&opt); 386 | 387 | return 0; 388 | } 389 | -------------------------------------------------------------------------------- /src/options.c: -------------------------------------------------------------------------------- 1 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 2 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 3 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 4 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 5 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 6 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 7 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 8 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 9 | 10 | #include "logging.h" 11 | #include "options.h" 12 | 13 | // Hack for platforms that don't support O_CLOEXEC. 14 | #ifndef O_CLOEXEC 15 | #define O_CLOEXEC 0 16 | #endif 17 | 18 | enum { 19 | DEFAULT_HTTP_VERSION = 2 20 | }; 21 | 22 | void options_init(struct Options *opt) { 23 | opt->listen_addr = "127.0.0.1"; 24 | opt->listen_port = 5053; 25 | opt->logfile = "-"; 26 | opt->logfd = STDOUT_FILENO; 27 | opt->loglevel = LOG_ERROR; 28 | opt->daemonize = 0; 29 | opt->dscp = 0; 30 | opt->user = NULL; 31 | opt->group = NULL; 32 | opt->uid = (uid_t)-1; 33 | opt->gid = (uid_t)-1; 34 | //new as from https://dnsprivacy.org/wiki/display/DP/DNS+Privacy+Test+Servers 35 | opt->bootstrap_dns = "8.8.8.8,1.1.1.1,8.8.4.4,1.0.0.1,145.100.185.15,145.100.185.16,185.49.141.37"; 36 | opt->bootstrap_dns_polling_interval = 120; 37 | opt->ipv4 = 0; 38 | opt->resolver_url = "https://dns.google/dns-query"; 39 | opt->curl_proxy = NULL; 40 | opt->use_http_version = DEFAULT_HTTP_VERSION; 41 | opt->max_idle_time = 118; 42 | opt->stats_interval = 0; 43 | opt->ca_info = NULL; 44 | opt->flight_recorder_size = 0; 45 | } 46 | 47 | enum OptionsParseResult options_parse_args(struct Options *opt, int argc, char **argv) { 48 | int c = 0; 49 | while ((c = getopt(argc, argv, "a:c:p:du:g:b:i:4r:e:t:l:vxqm:s:C:F:hV")) != -1) { 50 | switch (c) { 51 | case 'a': // listen_addr 52 | opt->listen_addr = optarg; 53 | break; 54 | case 'c': // DSCP codepoint 55 | opt->dscp = atoi(optarg); 56 | break; 57 | case 'p': // listen_port 58 | opt->listen_port = atoi(optarg); 59 | break; 60 | case 'd': // daemonize 61 | opt->daemonize = 1; 62 | break; 63 | case 'u': // user 64 | opt->user = optarg; 65 | break; 66 | case 'g': // group 67 | opt->group = optarg; 68 | break; 69 | case 'b': // bootstrap dns servers 70 | opt->bootstrap_dns = optarg; 71 | break; 72 | case 'i': // bootstrap dns servers polling interval 73 | opt->bootstrap_dns_polling_interval = atoi(optarg); 74 | break; 75 | case '4': // ipv4 mode - don't use v6 addresses. 76 | opt->ipv4 = 1; 77 | break; 78 | case 'r': // resolver url prefix 79 | opt->resolver_url = optarg; 80 | break; 81 | case 't': // curl http proxy 82 | opt->curl_proxy = optarg; 83 | break; 84 | case 'l': // logfile 85 | opt->logfile = optarg; 86 | break; 87 | case 'v': // verbose 88 | if (opt->loglevel) { 89 | opt->loglevel--; 90 | } 91 | break; 92 | case 'x': // http/1.1 fallthrough 93 | case 'q': // http/3 94 | if (opt->use_http_version == DEFAULT_HTTP_VERSION) { 95 | opt->use_http_version = (c == 'x' ? 1 : 3); 96 | } else { 97 | printf("HTTP version already set to: HTTP/%s\n", 98 | opt->use_http_version == 1 ? "1.1" : "3"); 99 | return OPR_OPTION_ERROR; 100 | } 101 | break; 102 | case 'm': 103 | opt->max_idle_time = atoi(optarg); 104 | break; 105 | case 's': // stats interval 106 | opt->stats_interval = atoi(optarg); 107 | break; 108 | case 'C': // CA info 109 | opt->ca_info = optarg; 110 | break; 111 | case 'F': // Flight recorder size 112 | opt->flight_recorder_size = atoi(optarg); 113 | break; 114 | case 'h': 115 | return OPR_HELP; 116 | case 'V': // version 117 | return OPR_VERSION; 118 | case '?': 119 | default: 120 | return OPR_PARSING_ERROR; 121 | } 122 | } 123 | 124 | if (opt->user) { 125 | struct passwd *p = getpwnam(opt->user); 126 | if (!p || !p->pw_uid) { 127 | printf("Username (%s) invalid.\n", opt->user); 128 | return OPR_OPTION_ERROR; 129 | } 130 | opt->uid = p->pw_uid; 131 | } 132 | if (opt->group) { 133 | struct group *g = getgrnam(opt->group); 134 | if (!g || !g->gr_gid) { 135 | printf("Group (%s) invalid.\n", opt->group); 136 | return OPR_OPTION_ERROR; 137 | } 138 | opt->gid = g->gr_gid; 139 | } 140 | if (opt->dscp < 0 || opt->dscp >63) { 141 | printf("DSCP code (%d) invalid:[0-63]\n", opt->dscp); 142 | return OPR_OPTION_ERROR; 143 | } 144 | opt->dscp <<= 2; 145 | // Get noisy about bad security practices. 146 | if (getuid() == 0 && (!opt->user || !opt->group)) { 147 | printf("----------------------------\n" 148 | "WARNING: Running as root without dropping privileges " 149 | "is NOT recommended.\n" 150 | "----------------------------\n"); 151 | sleep(1); 152 | } 153 | if (opt->logfile != NULL && strcmp(opt->logfile, "-") != 0) { 154 | opt->logfd = open(opt->logfile, 155 | O_CREAT | O_WRONLY | O_APPEND | O_CLOEXEC, 156 | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP); 157 | if (opt->logfd <= 0) { 158 | printf("Could not open logfile '%s' for writing.\n", opt->logfile); 159 | } 160 | } 161 | if (opt->resolver_url == NULL || 162 | strncmp(opt->resolver_url, "https://", 8) != 0) { 163 | printf("Resolver prefix (%s) must be a https:// address.\n", 164 | opt->resolver_url); 165 | return OPR_OPTION_ERROR; 166 | } 167 | if (opt->bootstrap_dns_polling_interval < 5 || 168 | opt->bootstrap_dns_polling_interval > 3600) { 169 | printf("DNS servers polling interval must be between 5 and 3600.\n"); 170 | return OPR_OPTION_ERROR; 171 | } 172 | if (opt->max_idle_time < 0 || 173 | opt->max_idle_time > 3600) { 174 | printf("Maximum idle time must be between 0 and 3600.\n"); 175 | return OPR_OPTION_ERROR; 176 | } 177 | if (opt->stats_interval < 0 || opt->stats_interval > 3600) { 178 | printf("Statistic interval must be between 0 and 3600.\n"); 179 | return OPR_OPTION_ERROR; 180 | } 181 | if (opt->flight_recorder_size != 0 && 182 | (opt->flight_recorder_size < 100 || opt->flight_recorder_size > 100000)) { 183 | printf("Flight recorder limit must be between 100 and 100000.\n"); 184 | return OPR_OPTION_ERROR; 185 | } 186 | return OPR_SUCCESS; 187 | } 188 | 189 | void options_show_usage(int __attribute__((unused)) argc, char **argv) { 190 | struct Options defaults; 191 | options_init(&defaults); 192 | printf("Usage: %s [-a ] [-p ]\n", argv[0]); 193 | printf(" [-b ] [-i ] [-4]\n"); 194 | printf(" [-r ] [-t ] [-x] [-q] [-C ] [-c ]\n"); 195 | printf(" [-d] [-u ] [-g ] \n"); 196 | printf(" [-v]+ [-l ] [-s ] [-F ] [-V] [-h]\n"); 197 | printf("\n DNS server\n"); 198 | printf(" -a listen_addr Local IPv4/v6 address to bind to. (Default: %s)\n", 199 | defaults.listen_addr); 200 | printf(" -p listen_port Local port to bind to. (Default: %d)\n", 201 | defaults.listen_port); 202 | printf("\n DNS client\n"); 203 | printf(" -b dns_servers Comma-separated IPv4/v6 addresses and ports (addr:port)\n"); 204 | printf(" of DNS servers to resolve resolver host (e.g. dns.google).\n"\ 205 | " When specifying a port for IPv6, enclose the address in [].\n"\ 206 | " (Default: %s)\n", 207 | defaults.bootstrap_dns); 208 | printf(" -i polling_interval Optional polling interval of DNS servers.\n"\ 209 | " (Default: %d, Min: 5, Max: 3600)\n", 210 | defaults.bootstrap_dns_polling_interval); 211 | printf(" -4 Force IPv4 hostnames for DNS resolvers non IPv6 networks.\n"); 212 | printf("\n HTTPS client\n"); 213 | printf(" -r resolver_url The HTTPS path to the resolver URL. (Default: %s)\n", 214 | defaults.resolver_url); 215 | printf(" -t proxy_server Optional HTTP proxy. e.g. socks5://127.0.0.1:1080\n"); 216 | printf(" Remote name resolution will be used if the protocol\n"); 217 | printf(" supports it (http, https, socks4a, socks5h), otherwise\n"); 218 | printf(" initial DNS resolution will still be done via the\n"); 219 | printf(" bootstrap DNS servers.\n"); 220 | printf(" -x Use HTTP/1.1 instead of HTTP/2. Useful with broken\n" 221 | " or limited builds of libcurl.\n"); 222 | printf(" -q Use HTTP/3 (QUIC) only.\n"); 223 | printf(" -m max_idle_time Maximum idle time in seconds allowed for reusing a HTTPS connection.\n"\ 224 | " (Default: %d, Min: 0, Max: 3600)\n", 225 | defaults.max_idle_time); 226 | printf(" -C ca_path Optional file containing CA certificates.\n"); 227 | printf(" -c dscp_codepoint Optional DSCP codepoint to set on upstream HTTPS server\n"); 228 | printf(" connections. (Min: 0, Max: 63)\n"); 229 | printf("\n Process\n"); 230 | printf(" -d Daemonize.\n"); 231 | printf(" -u user Optional user to drop to if launched as root.\n"); 232 | printf(" -g group Optional group to drop to if launched as root.\n"); 233 | printf("\n Logging\n"); 234 | printf(" -v Increase logging verbosity. (Default: error)\n"); 235 | printf(" Levels: fatal, stats, error, warning, info, debug\n"); 236 | printf(" Request issues are logged on warning level.\n"); 237 | printf(" -l logfile Path to file to log to. (Default: standard output)\n"); 238 | printf(" -s statistic_interval Optional statistic printout interval.\n"\ 239 | " (Default: %d, Disabled: 0, Min: 1, Max: 3600)\n", 240 | defaults.stats_interval); 241 | printf(" -F log_limit Flight recorder: storing desired amount of logs from all levels\n"\ 242 | " in memory and dumping them on fatal error or on SIGUSR2 signal.\n" 243 | " (Default: %u, Disabled: 0, Min: 100, Max: 100000)\n", 244 | defaults.flight_recorder_size); 245 | printf(" -V Print versions and exit.\n"); 246 | printf(" -h Print help and exit.\n"); 247 | options_cleanup(&defaults); 248 | } 249 | 250 | void options_cleanup(struct Options *opt) { 251 | if (opt->logfd != STDOUT_FILENO && opt->logfd > 0) { 252 | close(opt->logfd); 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/options.h: -------------------------------------------------------------------------------- 1 | // Holds options that can be supplied via commandline. 2 | #ifndef _OPTIONS_H_ 3 | #define _OPTIONS_H_ 4 | 5 | #include 6 | 7 | struct Options { 8 | const char *listen_addr; 9 | uint16_t listen_port; 10 | 11 | // Logfile. 12 | const char *logfile; 13 | int logfd; 14 | int loglevel; 15 | 16 | // Whether to fork into background. 17 | int daemonize; 18 | 19 | // User/group to drop permissions to if root. 20 | // Not used if running as non-root. 21 | const char *user; 22 | const char *group; 23 | 24 | // Derived from the above. 25 | uid_t uid; 26 | gid_t gid; 27 | 28 | // DNS servers to look up resolver host (e.g. dns.google) 29 | const char *bootstrap_dns; 30 | 31 | int bootstrap_dns_polling_interval; 32 | 33 | int ipv4; // if non-zero, will only use AF_INET addresses. 34 | 35 | int dscp; // mark packet with DSCP 36 | 37 | // Resolver URL prefix to use. Must start with https://. 38 | const char *resolver_url; 39 | 40 | // Optional http proxy if required. 41 | // e.g. "socks5://127.0.0.1:1080" 42 | const char *curl_proxy; 43 | 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; 48 | 49 | int max_idle_time; 50 | 51 | // Print statistic interval 52 | int stats_interval; 53 | 54 | // Path to a file containing CA certificates 55 | const char *ca_info; 56 | 57 | // Number of logs to be kept by flight recorder 58 | uint32_t flight_recorder_size; 59 | } __attribute__((aligned(128))); 60 | typedef struct Options options_t; 61 | 62 | enum OptionsParseResult { 63 | OPR_SUCCESS, 64 | OPR_HELP, 65 | OPR_VERSION, 66 | OPR_OPTION_ERROR, 67 | OPR_PARSING_ERROR 68 | }; 69 | 70 | #ifdef __cplusplus 71 | extern "C" { 72 | #endif 73 | void options_init(struct Options *opt); 74 | enum OptionsParseResult options_parse_args(struct Options *opt, int argc, char **argv); 75 | void options_show_usage(int argc, char **argv); 76 | void options_cleanup(struct Options *opt); 77 | #ifdef __cplusplus 78 | } 79 | #endif 80 | 81 | #endif // _OPTIONS_H_ 82 | -------------------------------------------------------------------------------- /src/ring_buffer.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 3 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 4 | #include // NOLINT(llvmlibc-restrict-system-libc-headers) 5 | 6 | #include "ring_buffer.h" 7 | 8 | struct ring_buffer 9 | { 10 | char ** storage; 11 | uint32_t size; 12 | uint32_t next; // next slot to use in storage 13 | uint8_t full; 14 | } __attribute__((packed)) __attribute__((aligned(32))); 15 | 16 | void ring_buffer_init(struct ring_buffer **rbp, uint32_t size) 17 | { 18 | *rbp = NULL; 19 | if (size < 1) { 20 | return; 21 | } 22 | struct ring_buffer *rb = (struct ring_buffer *)calloc(1, sizeof(struct ring_buffer)); 23 | if (!rb) { 24 | return; 25 | } 26 | rb->storage = (char**)calloc(size, sizeof(char*)); 27 | if (!rb->storage) { 28 | free((void*) rb); 29 | return; 30 | } 31 | rb->size = size; 32 | *rbp = rb; 33 | } 34 | 35 | void ring_buffer_free(struct ring_buffer **rbp) 36 | { 37 | struct ring_buffer *rb = *rbp; 38 | if (!rb->storage) { 39 | return; 40 | } 41 | for (uint32_t i = 0; i < rb->size; i++) { 42 | if (rb->storage[i]) { 43 | free(rb->storage[i]); 44 | } 45 | } 46 | free((void*) rb->storage); 47 | free((void*) rb); 48 | *rbp = NULL; 49 | } 50 | 51 | void ring_buffer_dump(struct ring_buffer *rb, FILE * file) 52 | { 53 | if (!rb->storage) { 54 | return; 55 | } 56 | if (rb->next == 0 && !rb->full) { 57 | return; // empty 58 | } 59 | 60 | uint32_t current = rb->full ? rb->next : 0; 61 | do 62 | { 63 | // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) 64 | (void)fprintf(file, "%s\n", rb->storage[current]); 65 | 66 | if (++current == rb->size) { 67 | current = 0; 68 | } 69 | } 70 | while (current != rb->next); 71 | (void)fflush(file); 72 | } 73 | 74 | void ring_buffer_push_back(struct ring_buffer *rb, char* data, uint32_t size) 75 | { 76 | if (!rb->storage) { 77 | return; 78 | } 79 | 80 | if (rb->storage[rb->next]) { 81 | free(rb->storage[rb->next]); 82 | } 83 | rb->storage[rb->next] = (char*)malloc(size + 1); 84 | // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) 85 | memcpy(rb->storage[rb->next], data, size); 86 | rb->storage[rb->next][size] = '\0'; 87 | 88 | if (++rb->next == rb->size) { 89 | rb->next = 0; 90 | rb->full = 1; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/ring_buffer.h: -------------------------------------------------------------------------------- 1 | #ifndef _RING_BUFFER_H_ 2 | #define _RING_BUFFER_H_ 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | #include 9 | 10 | struct ring_buffer; 11 | 12 | void ring_buffer_init(struct ring_buffer **rbp, uint32_t size); 13 | void ring_buffer_free(struct ring_buffer **rbp); 14 | void ring_buffer_dump(struct ring_buffer *rb, FILE * file); 15 | void ring_buffer_push_back(struct ring_buffer *rb, char* data, uint32_t size); 16 | 17 | #ifdef __cplusplus 18 | } 19 | #endif 20 | 21 | #endif // RING_BUFFER_H_ 22 | -------------------------------------------------------------------------------- /src/stat.c: -------------------------------------------------------------------------------- 1 | #include "stat.h" 2 | #include "logging.h" 3 | 4 | static void reset_counters(stat_t *s) { 5 | s->requests_size = 0; 6 | s->responses_size = 0; 7 | s->requests = 0; 8 | s->responses = 0; 9 | s->query_times_sum = 0; 10 | s->connections_opened = 0; 11 | s->connections_closed = 0; 12 | s->connections_reused = 0; 13 | } 14 | 15 | static void stat_print(stat_t *s) { 16 | SLOG("%llu %llu %llu %zu %zu %llu %llu %llu", 17 | s->requests, s->responses, s->query_times_sum, 18 | s->requests_size, s->responses_size, 19 | s->connections_opened, s->connections_closed, 20 | s->connections_reused); 21 | reset_counters(s); 22 | } 23 | 24 | static void stat_timer_cb(struct ev_loop __attribute__((unused)) *loop, 25 | ev_timer *w, int __attribute__((unused)) revents) { 26 | stat_t *s = (stat_t *)w->data; 27 | stat_print(s); 28 | } 29 | 30 | void stat_init(stat_t *s, struct ev_loop *loop, int stats_interval) { 31 | s->loop = loop; 32 | s->stats_interval = stats_interval; 33 | reset_counters(s); 34 | 35 | // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) 36 | ev_timer_init(&s->stats_timer, stat_timer_cb, 37 | s->stats_interval, s->stats_interval); 38 | s->stats_timer.data = s; 39 | if (s->stats_interval > 0) { 40 | ev_timer_start(loop, &s->stats_timer); 41 | SLOG("RequestsCount ResponsesCount LatencyMilisecondsSummary " 42 | "RequestsSize ResponsesSize ConnectionsOpened ConnectionsClosed " 43 | "ConnectionsReused"); 44 | } 45 | } 46 | 47 | void stat_request_begin(stat_t *s, size_t req_len) 48 | { 49 | s->requests_size += req_len; 50 | s->requests++; 51 | } 52 | 53 | void stat_request_end(stat_t *s, size_t resp_len, ev_tstamp latency) 54 | { 55 | if (resp_len) { 56 | s->responses_size += resp_len; 57 | s->responses++; 58 | // NOLINTNEXTLINE(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) 59 | s->query_times_sum += (latency * 1000); 60 | } 61 | } 62 | 63 | void stat_connection_opened(stat_t *s) 64 | { 65 | s->connections_opened++; 66 | } 67 | 68 | void stat_connection_closed(stat_t *s) 69 | { 70 | s->connections_closed++; 71 | } 72 | 73 | void stat_connection_reused(stat_t *s) 74 | { 75 | s->connections_reused++; 76 | } 77 | 78 | void stat_stop(stat_t *s) { 79 | ev_timer_stop(s->loop, &s->stats_timer); 80 | } 81 | 82 | void stat_cleanup(stat_t *s) { 83 | if (s->stats_interval > 0) { 84 | stat_print(s); // final one 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/stat.h: -------------------------------------------------------------------------------- 1 | // Statistics tracking 2 | // 3 | // Connection, request and latency statistics are accumulated in a struct 4 | // and output periodically via a libev timer callback. 5 | // 6 | // stat_init() initializes this struct and starts the timer if required. 7 | // stat_stop() stops the timer for shutdown. 8 | // stat_cleanup() prints the final measurement. 9 | // stat_request_(begin|end) and 10 | // stat_connection_(open|closed|reused) update the tallies. 11 | // 12 | 13 | #ifndef _STAT_H_ 14 | #define _STAT_H_ 15 | 16 | #include 17 | #include 18 | 19 | typedef struct { 20 | struct ev_loop *loop; 21 | int stats_interval; 22 | size_t requests_size; 23 | size_t responses_size; 24 | uint64_t requests; 25 | uint64_t responses; 26 | uint64_t query_times_sum; 27 | uint64_t connections_opened; 28 | uint64_t connections_closed; 29 | uint64_t connections_reused; 30 | ev_timer stats_timer; 31 | } stat_t; 32 | 33 | void stat_init(stat_t *s, struct ev_loop *loop, int stats_interval); 34 | 35 | void stat_request_begin(stat_t *s, size_t req_len); 36 | 37 | void stat_request_end(stat_t *s, size_t resp_len, ev_tstamp latency); 38 | 39 | void stat_connection_opened(stat_t *s); 40 | 41 | void stat_connection_closed(stat_t *s); 42 | 43 | void stat_connection_reused(stat_t *s); 44 | 45 | void stat_stop(stat_t *s); 46 | 47 | void stat_cleanup(stat_t *s); 48 | 49 | #endif // _STAT_H_ 50 | -------------------------------------------------------------------------------- /tests/robot/functional_tests.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Documentation Simple functional tests for https_dns_proxy 3 | Library OperatingSystem 4 | Library Process 5 | Library Collections 6 | 7 | 8 | *** Variables *** 9 | ${BINARY_PATH} ${CURDIR}/../../https_dns_proxy 10 | ${PORT} 55353 11 | 12 | 13 | *** Settings *** 14 | Test Teardown Stop Proxy 15 | 16 | 17 | *** Keywords *** 18 | Common Test Setup 19 | Set Test Variable &{expected_logs} loop destroyed=1 # last log line 20 | Set Test Variable @{error_logs} [F] # any fatal error 21 | 22 | Start Proxy 23 | [Arguments] @{args} 24 | @{default_args} = Create List -v -v -v -4 -p ${PORT} 25 | @{proces_args} = Combine Lists ${default_args} ${args} 26 | ${proxy} = Start Process ${BINARY_PATH} @{proces_args} 27 | ... stderr=STDOUT alias=proxy 28 | Set Test Variable ${proxy} 29 | Set Test Variable ${dig_timeout} 2 30 | Set Test Variable ${dig_retry} 0 31 | Sleep 0.5 32 | Common Test Setup 33 | 34 | Start Proxy With Valgrind 35 | [Arguments] @{args} 36 | @{default_args} = Create List --track-fds=yes --time-stamp=yes --log-file=valgrind-%p.log --suppressions=valgrind.supp 37 | ... --gen-suppressions=all --tool=memcheck --leak-check=full --leak-resolution=high 38 | ... --show-leak-kinds=all --track-origins=yes --keep-stacktraces=alloc-and-free 39 | ... ${BINARY_PATH} -v -v -v -F 100 -4 -p ${PORT} # using flight recorder with smallest possible buffer size to test memory leak 40 | @{proces_args} = Combine Lists ${default_args} ${args} 41 | ${proxy} = Start Process valgrind @{proces_args} 42 | ... stderr=STDOUT alias=proxy 43 | Set Test Variable ${proxy} 44 | Set Test Variable ${dig_timeout} 10 45 | Set Test Variable ${dig_retry} 2 46 | Sleep 6 # wait for valgrind to fire up the proxy 47 | Common Test Setup 48 | 49 | Stop Proxy 50 | Send Signal To Process SIGINT ${proxy} 51 | ${result} = Wait For Process ${proxy} timeout=15 secs 52 | Log ${result.rc} 53 | Log ${result.stdout} 54 | Log ${result.stderr} 55 | FOR ${log} ${times} IN &{expected_logs} 56 | Should Contain X Times ${result.stdout} ${log} ${times} 57 | END 58 | FOR ${log} IN @{error_logs} 59 | Run Keyword And Expect Error not found 60 | ... Should Contain ${result.stdout} ${log} msg=not found values=False 61 | END 62 | Should Be Equal As Integers ${result.rc} 0 63 | 64 | 65 | Start Dig 66 | [Arguments] ${domain}=google.com 67 | ${handle} = Start Process dig +timeout\=${dig_timeout} +retry\=${dig_retry} @127.0.0.1 -p ${PORT} ${domain} 68 | ... stderr=STDOUT alias=dig 69 | RETURN ${handle} 70 | 71 | Stop Dig 72 | [Arguments] ${handle} 73 | ${result} = Wait For Process ${handle} timeout=15 secs 74 | Log ${result.stdout} 75 | Should Be Equal As Integers ${result.rc} 0 76 | Should Contain ${result.stdout} ANSWER SECTION 77 | 78 | Run Dig 79 | [Arguments] ${domain}=google.com 80 | ${handle} = Start Dig ${domain} 81 | Stop Dig ${handle} 82 | 83 | Run Dig Parallel 84 | ${dig_handles} = Create List 85 | FOR ${domain} IN facebook.com microsoft.com youtube.com maps.google.com wikipedia.org amazon.com 86 | ${handle} = Start Dig ${domain} 87 | Append To List ${dig_handles} ${handle} 88 | END 89 | FOR ${handle} IN @{dig_handles} 90 | Stop Dig ${handle} 91 | END 92 | 93 | 94 | *** Test Cases *** 95 | Handle Unbound Server Does Not Support HTTP/1.1 96 | Start Proxy -x -r https://doh.mullvad.net/dns-query # resolver uses Unbound 97 | Run Keyword And Expect Error 9 != 0 # timeout exit code 98 | ... Run Dig 99 | 100 | Reuse HTTP/2 Connection 101 | [Documentation] After first successful request, further requests should not open new connections 102 | Start Proxy 103 | Run Dig # Simple smoke test and opens first connection 104 | Run Dig Parallel 105 | Set To Dictionary ${expected_logs} curl opened socket=1 # curl must not open more sockets then 1 106 | 107 | Valgrind Resource Leak Check 108 | Start Proxy With Valgrind 109 | Run Dig Parallel 110 | -------------------------------------------------------------------------------- /tests/robot/valgrind.supp: -------------------------------------------------------------------------------- 1 | { 2 | ignore dlopen 3 | Memcheck:Leak 4 | match-leak-kinds: reachable 5 | ... 6 | fun:_dl_open 7 | } 8 | { 9 | curl version ldap dependency 10 | Memcheck:Leak 11 | match-leak-kinds: reachable 12 | ... 13 | fun:ldap_int_sasl_init 14 | fun:ldap_int_initialize 15 | fun:ldap_get_option 16 | fun:curl_version 17 | fun:main 18 | } 19 | --------------------------------------------------------------------------------