├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ ├── main.yml │ └── rebuild-latest-release.yml ├── .gitignore ├── .vscode └── settings.json ├── Dockerfile.m4 ├── LICENSE.md ├── Makefile ├── README.md ├── config ├── hblock │ ├── environment │ ├── footer │ └── header └── knot-resolver │ ├── kresd.conf │ └── kresd.conf.d │ ├── 000-utils.conf │ ├── 010-log.conf │ ├── 010-watcher.conf │ ├── 020-cache.conf │ ├── 020-net.conf │ ├── 030-dns-over-https.conf │ ├── 030-dns-over-tls.conf │ ├── 030-dns.conf │ ├── 040-module-bogus-log.conf │ ├── 040-module-hints.conf │ ├── 040-module-rebinding.conf │ ├── 040-module-stats.conf │ ├── 050-module-policy.conf │ ├── 050-module-view.conf │ ├── 060-module-http.conf │ ├── 070-policy-special.conf │ ├── 080-policy-blocklist.conf │ ├── 090-policy-forward.conf │ └── 100-http-blocklist.conf ├── examples ├── traefik-grafana │ ├── .env.sample │ ├── compose.yaml │ └── config │ │ ├── grafana │ │ └── provisioning │ │ │ ├── dashboards │ │ │ ├── dashboards.yml │ │ │ └── main.json │ │ │ ├── datasources │ │ │ └── datasources.yml │ │ │ ├── notifiers │ │ │ └── .gitkeep │ │ │ └── plugins │ │ │ └── .gitkeep │ │ ├── prometheus │ │ └── prometheus.yml │ │ └── traefik │ │ ├── dynamic │ │ └── ingress.yml │ │ └── traefik.yml └── traefik │ ├── .env.sample │ ├── compose.yaml │ └── config │ └── traefik │ ├── dynamic │ └── ingress.yml │ └── traefik.yml ├── run.sh └── scripts ├── bin ├── container-healthcheck ├── container-init ├── is-sv-status ├── kres-cert-updater └── kres-console └── service ├── hblock └── run ├── kres-cache-gc └── run ├── kres-cert-updater └── run └── kresd0 ├── finish └── run /.dockerignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | /examples/ 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/dependabot-2.0.json 2 | version: 2 3 | 4 | updates: 5 | - package-ecosystem: "docker" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | groups: 10 | docker-all: 11 | patterns: ["*"] 12 | 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "monthly" 17 | groups: 18 | github-actions-all: 19 | patterns: ["*"] 20 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json 2 | name: "Main" 3 | 4 | on: 5 | push: 6 | tags: ["*"] 7 | branches: ["*"] 8 | pull_request: 9 | branches: ["*"] 10 | workflow_dispatch: 11 | 12 | permissions: {} 13 | 14 | jobs: 15 | build: 16 | name: "Build ${{ matrix.arch }} image" 17 | runs-on: "ubuntu-latest" 18 | permissions: 19 | contents: "read" 20 | strategy: 21 | matrix: 22 | arch: ["native", "amd64", "arm64v8"] 23 | steps: 24 | - name: "Checkout project" 25 | uses: "actions/checkout@v4" 26 | - name: "Register binfmt entries" 27 | if: "matrix.arch != 'native'" 28 | run: | 29 | make binfmt-register 30 | - name: "Build and save image" 31 | run: | 32 | make IMAGE_BUILD_OPTS="--pull" "build-${{ matrix.arch }}-image" "save-${{ matrix.arch }}-image" 33 | - name: "Upload artifacts" 34 | if: "startsWith(github.ref, 'refs/tags/v') && matrix.arch != 'native'" 35 | uses: "actions/upload-artifact@v4" 36 | with: 37 | name: "dist-${{ matrix.arch }}" 38 | path: "./dist/" 39 | retention-days: 1 40 | 41 | push: 42 | name: "Push ${{ matrix.arch }} image" 43 | if: "startsWith(github.ref, 'refs/tags/v')" 44 | needs: ["build"] 45 | runs-on: "ubuntu-latest" 46 | permissions: 47 | contents: "read" 48 | strategy: 49 | matrix: 50 | arch: ["amd64", "arm64v8"] 51 | steps: 52 | - name: "Checkout project" 53 | uses: "actions/checkout@v4" 54 | - name: "Download artifacts" 55 | uses: "actions/download-artifact@v4" 56 | with: 57 | name: "dist-${{ matrix.arch }}" 58 | path: "./dist/" 59 | - name: "Login to Docker Hub" 60 | uses: "docker/login-action@v3" 61 | with: 62 | registry: "docker.io" 63 | username: "${{ secrets.DOCKERHUB_USERNAME }}" 64 | password: "${{ secrets.DOCKERHUB_TOKEN }}" 65 | - name: "Load and push image" 66 | run: | 67 | make "load-${{ matrix.arch }}-image" "push-${{ matrix.arch }}-image" 68 | 69 | push-manifest: 70 | name: "Push manifest" 71 | if: "startsWith(github.ref, 'refs/tags/v')" 72 | needs: ["push"] 73 | runs-on: "ubuntu-latest" 74 | permissions: 75 | contents: "read" 76 | steps: 77 | - name: "Checkout project" 78 | uses: "actions/checkout@v4" 79 | - name: "Login to Docker Hub" 80 | uses: "docker/login-action@v3" 81 | with: 82 | registry: "docker.io" 83 | username: "${{ secrets.DOCKERHUB_USERNAME }}" 84 | password: "${{ secrets.DOCKERHUB_TOKEN }}" 85 | - name: "Push manifest" 86 | run: | 87 | make push-cross-manifest 88 | 89 | publish-github-release: 90 | name: "Publish GitHub release" 91 | if: "startsWith(github.ref, 'refs/tags/v')" 92 | needs: ["push-manifest"] 93 | runs-on: "ubuntu-latest" 94 | permissions: 95 | contents: "write" 96 | steps: 97 | - name: "Publish" 98 | uses: "hectorm/ghaction-release@066200d04c3549852afa243d631ea3dc93390f68" 99 | -------------------------------------------------------------------------------- /.github/workflows/rebuild-latest-release.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json 2 | name: "Rebuild latest release" 3 | 4 | on: 5 | schedule: 6 | - cron: "20 04 * * 1" 7 | workflow_dispatch: 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | trigger-rebuild: 13 | name: "Trigger rebuild" 14 | runs-on: "ubuntu-latest" 15 | permissions: 16 | actions: "write" 17 | contents: "read" 18 | steps: 19 | - name: "Trigger rebuild" 20 | uses: "hectorm/ghaction-trigger-workflow@04c79e7a4e0c0b94bbcff3829f38359e34f1ea9e" 21 | with: 22 | workflow-id: "main.yml" 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | .env 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "**/kresd.conf": "lua", 4 | "**/kresd.conf.d/*.conf": "lua" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Dockerfile.m4: -------------------------------------------------------------------------------- 1 | m4_changequote([[, ]]) 2 | 3 | ################################################## 4 | ## "build" stage 5 | ################################################## 6 | 7 | m4_ifdef([[CROSS_ARCH]], [[FROM docker.io/CROSS_ARCH/ubuntu:24.04]], [[FROM docker.io/ubuntu:24.04]]) AS build 8 | 9 | # Install system packages 10 | RUN export DEBIAN_FRONTEND=noninteractive \ 11 | && apt-get update \ 12 | && apt-get install -y --no-install-recommends \ 13 | autoconf \ 14 | automake \ 15 | build-essential \ 16 | ca-certificates \ 17 | curl \ 18 | dns-root-data \ 19 | file \ 20 | gawk \ 21 | git \ 22 | libaugeas-dev \ 23 | libcap-ng-dev \ 24 | libcap2-bin \ 25 | libcmocka-dev \ 26 | libedit-dev \ 27 | libffi-dev \ 28 | libgeoip-dev \ 29 | libgnutls28-dev \ 30 | libidn2-dev \ 31 | libjansson-dev \ 32 | libjemalloc-dev \ 33 | liblmdb-dev \ 34 | libnghttp2-dev \ 35 | libpsl-dev \ 36 | libssl-dev \ 37 | libsystemd-dev \ 38 | libtool \ 39 | libunistring-dev \ 40 | liburcu-dev \ 41 | libuv1-dev \ 42 | meson \ 43 | ninja-build \ 44 | pkgconf \ 45 | tzdata \ 46 | unzip \ 47 | && rm -rf /var/lib/apt/lists/* 48 | 49 | # Build Knot DNS (only libknot and utilities) 50 | ARG KNOT_DNS_TREEISH=v3.4.4 51 | ARG KNOT_DNS_REMOTE=https://gitlab.nic.cz/knot/knot-dns.git 52 | RUN mkdir /tmp/knot-dns/ 53 | WORKDIR /tmp/knot-dns/ 54 | RUN git clone "${KNOT_DNS_REMOTE:?}" ./ 55 | RUN git checkout "${KNOT_DNS_TREEISH:?}" 56 | RUN git submodule update --init --recursive 57 | RUN ./autogen.sh 58 | RUN ./configure \ 59 | --prefix=/usr \ 60 | --enable-utilities \ 61 | --enable-fastparser \ 62 | --enable-quic \ 63 | --disable-daemon \ 64 | --disable-modules \ 65 | --disable-dnstap \ 66 | --disable-documentation 67 | RUN make -j"$(nproc)" 68 | RUN make install 69 | RUN file /usr/bin/kdig 70 | RUN file /usr/bin/khost 71 | RUN /usr/bin/kdig --version 72 | RUN /usr/bin/khost --version 73 | 74 | # Build LuaJIT 75 | ARG LUAJIT_TREEISH=538a82133ad6fddfd0ca64de167c4aca3bc1a2da 76 | ARG LUAJIT_REMOTE=https://github.com/LuaJIT/LuaJIT.git 77 | RUN mkdir /tmp/luajit/ 78 | WORKDIR /tmp/luajit/ 79 | RUN git clone "${LUAJIT_REMOTE:?}" ./ 80 | RUN git checkout "${LUAJIT_TREEISH:?}" 81 | RUN git submodule update --init --recursive 82 | RUN [ "$(getconf LONG_BIT)" != 32 ] || XCFLAGS='-DLUAJIT_USE_SYSMALLOC'; \ 83 | make -j"$(nproc)" amalg XCFLAGS="${XCFLAGS-}" 84 | RUN make install PREFIX=/usr 85 | RUN file /usr/bin/luajit-2.1.* 86 | RUN luajit -v 87 | 88 | # Build LuaRocks 89 | ARG LUAROCKS_TREEISH=v3.11.1 90 | ARG LUAROCKS_REMOTE=https://github.com/luarocks/luarocks.git 91 | RUN mkdir /tmp/luarocks/ 92 | WORKDIR /tmp/luarocks/ 93 | RUN git clone "${LUAROCKS_REMOTE:?}" ./ 94 | RUN git checkout "${LUAROCKS_TREEISH:?}" 95 | RUN git submodule update --init --recursive 96 | RUN ./configure \ 97 | --prefix=/usr \ 98 | --sysconfdir=/etc \ 99 | --rocks-tree=/usr/local \ 100 | --lua-version=5.1 \ 101 | --with-lua=/usr \ 102 | --with-lua-bin=/usr/bin \ 103 | --with-lua-lib=/usr/lib \ 104 | --with-lua-include=/usr/include/luajit-2.1 \ 105 | --with-lua-interpreter=luajit 106 | RUN make build -j"$(nproc)" 107 | RUN make install 108 | RUN file /usr/bin/luarocks 109 | RUN luarocks --version 110 | 111 | # Install LuaRocks packages 112 | RUN mkdir /tmp/rocks/ 113 | WORKDIR /tmp/rocks/ 114 | RUN luarocks init --lua-versions=5.1 metapackage 115 | RUN ROCKS=$(printf '["%s"]="%s",' \ 116 | basexx 0.4.1-1 \ 117 | binaryheap 0.4-1 \ 118 | bit32 5.3.5.1-1 \ 119 | compat53 0.14.4-1 \ 120 | cqueues 20200726.51-0 \ 121 | fifo 0.2-0 \ 122 | http 0.4-0 \ 123 | lpeg 1.1.0-2 \ 124 | lpeg_patterns 0.5-0 \ 125 | lua 5.1-1 \ 126 | lua-lru 1.0-1 \ 127 | luafilesystem 1.8.0-1 \ 128 | luaossl 20220711-0 \ 129 | mmdblua 0.2-0 \ 130 | psl 0.3-0 \ 131 | ) \ 132 | && printf 'return {dependencies = {%s}}' "${ROCKS:?}" > ./luarocks.lock \ 133 | && HOST_MULTIARCH=$(dpkg-architecture -qDEB_HOST_MULTIARCH) \ 134 | && LIBDIRS="${LIBDIRS-} CRYPTO_LIBDIR=/usr/lib/${HOST_MULTIARCH:?}" \ 135 | && LIBDIRS="${LIBDIRS-} OPENSSL_LIBDIR=/usr/lib/${HOST_MULTIARCH:?}" \ 136 | && luarocks install --tree=system --only-deps ./*.rockspec ${LIBDIRS:?} 137 | 138 | # Build Knot Resolver 139 | ARG KNOT_RESOLVER_TREEISH=v5.7.4 140 | ARG KNOT_RESOLVER_REMOTE=https://gitlab.nic.cz/knot/knot-resolver.git 141 | RUN mkdir /tmp/knot-resolver/ 142 | WORKDIR /tmp/knot-resolver/ 143 | RUN git clone "${KNOT_RESOLVER_REMOTE:?}" ./ 144 | RUN git checkout "${KNOT_RESOLVER_TREEISH:?}" 145 | RUN git submodule update --init --recursive 146 | RUN meson ./build/ \ 147 | --prefix=/usr \ 148 | --libdir=/usr/lib \ 149 | --sysconfdir=/etc \ 150 | --buildtype=release \ 151 | -D client=enabled \ 152 | -D dnstap=disabled \ 153 | -D doc=disabled \ 154 | -D managed_ta=disabled \ 155 | -D malloc=jemalloc \ 156 | -D root_hints=/usr/share/dns/root.hints \ 157 | -D keyfile_default=/usr/share/dns/root.key \ 158 | -D unit_tests=enabled \ 159 | -D config_tests=enabled \ 160 | -D extra_tests=disabled 161 | RUN ninja -C ./build/ 162 | RUN ninja -C ./build/ install 163 | RUN TESTS=$(meson test -C ./build/ --suite unit --suite config --no-suite snowflake --list 2>/dev/null \ 164 | | awk '{print($3)}' | grep -vE '^config\.(http|ta_bootstrap)$' \ 165 | ) \ 166 | && meson test -C ./build/ -t 8 --print-errorlogs ${TESTS:?} 167 | RUN setcap cap_net_bind_service=+ep /usr/sbin/kresd 168 | RUN file /usr/sbin/kresd 169 | RUN file /usr/sbin/kresc 170 | RUN /usr/sbin/kresd --version 171 | 172 | # Download hBlock 173 | ARG HBLOCK_TREEISH=v3.5.1 174 | ARG HBLOCK_REMOTE=https://github.com/hectorm/hblock.git 175 | RUN mkdir /tmp/hblock/ 176 | WORKDIR /tmp/hblock/ 177 | RUN git clone "${HBLOCK_REMOTE:?}" ./ 178 | RUN git checkout "${HBLOCK_TREEISH:?}" 179 | RUN git submodule update --init --recursive 180 | RUN make install prefix=/usr 181 | RUN /usr/bin/hblock --version 182 | 183 | ################################################## 184 | ## "base" stage 185 | ################################################## 186 | 187 | m4_ifdef([[CROSS_ARCH]], [[FROM docker.io/CROSS_ARCH/ubuntu:24.04]], [[FROM docker.io/ubuntu:24.04]]) AS base 188 | 189 | # Install system packages 190 | RUN export DEBIAN_FRONTEND=noninteractive \ 191 | && apt-get update \ 192 | && apt-get install -y --no-install-recommends \ 193 | ca-certificates \ 194 | catatonit \ 195 | curl \ 196 | dns-root-data \ 197 | gzip \ 198 | libcap-ng0 \ 199 | libedit2 \ 200 | libgcc1 \ 201 | libgeoip1t64 \ 202 | libgnutls30t64 \ 203 | libidn2-0 \ 204 | libjansson4 \ 205 | libjemalloc2 \ 206 | liblmdb0 \ 207 | libnghttp2-14 \ 208 | libpsl5t64 \ 209 | libssl3t64 \ 210 | libstdc++6 \ 211 | libsystemd0 \ 212 | libunistring5 \ 213 | liburcu8t64 \ 214 | libuv1t64 \ 215 | netcat-openbsd \ 216 | openssl \ 217 | rlfe \ 218 | runit \ 219 | snooze \ 220 | tzdata \ 221 | && apt-get clean \ 222 | && rm -rf \ 223 | /var/lib/apt/lists/* \ 224 | /var/cache/ldconfig/aux-cache \ 225 | /var/log/apt/* \ 226 | /var/log/alternatives.log \ 227 | /var/log/bootstrap.log \ 228 | /var/log/dpkg.log 229 | 230 | # Environment 231 | ENV SVDIR=/service/ 232 | ENV KRESD_CONF_DIR=/etc/knot-resolver/ 233 | ENV KRESD_DATA_DIR=/var/lib/knot-resolver/ 234 | ENV KRESD_CACHE_DIR=/var/cache/knot-resolver/ 235 | ENV KRESD_CACHE_SIZE=50 236 | ENV KRESD_DNS1_IP=1.1.1.1@853 237 | ENV KRESD_DNS1_HOSTNAME=cloudflare-dns.com 238 | ENV KRESD_DNS2_IP=1.0.0.1@853 239 | ENV KRESD_DNS2_HOSTNAME=cloudflare-dns.com 240 | ENV KRESD_INSTANCE_NUMBER=1 241 | ENV KRESD_RECENTLY_BLOCKED_NUMBER=100 242 | ENV KRESD_CERT_MANAGED=true 243 | ENV KRESD_NIC= 244 | ENV KRESD_LOG_LEVEL=notice 245 | 246 | # Create unprivileged user 247 | RUN userdel -rf "$(id -nu 1000)" && useradd -u 1000 -g 0 -s "$(command -v bash)" -Md "${KRESD_CACHE_DIR:?}" knot-resolver 248 | 249 | # Copy LuaJIT build 250 | COPY --from=build --chown=root:root /usr/lib/libluajit-* /usr/lib/ 251 | 252 | # Copy Lua packages 253 | COPY --from=build --chown=root:root /usr/local/lib/lua/ /usr/local/lib/lua/ 254 | COPY --from=build --chown=root:root /usr/local/share/lua/ /usr/local/share/lua/ 255 | 256 | # Copy Knot DNS build 257 | COPY --from=build --chown=root:root /usr/lib/libdnssec.* /usr/lib/ 258 | COPY --from=build --chown=root:root /usr/lib/libknot.* /usr/lib/ 259 | COPY --from=build --chown=root:root /usr/lib/libzscanner.* /usr/lib/ 260 | COPY --from=build --chown=root:root /usr/bin/kdig /usr/bin/kdig 261 | COPY --from=build --chown=root:root /usr/bin/khost /usr/bin/khost 262 | 263 | # Copy Knot Resolver build 264 | COPY --from=build --chown=root:root /usr/lib/libkres.* /usr/lib/ 265 | COPY --from=build --chown=root:root /usr/lib/knot-resolver/ /usr/lib/knot-resolver/ 266 | COPY --from=build --chown=root:root /usr/sbin/kresd /usr/sbin/kresd 267 | COPY --from=build --chown=root:root /usr/sbin/kresc /usr/sbin/kresc 268 | COPY --from=build --chown=root:root /usr/sbin/kres-cache-gc /usr/sbin/kres-cache-gc 269 | 270 | # Copy hBlock build 271 | COPY --from=build --chown=root:root /usr/bin/hblock /usr/bin/hblock 272 | 273 | # Create data and cache directories 274 | RUN mkdir "${KRESD_DATA_DIR:?}" "${KRESD_CACHE_DIR:?}" 275 | RUN chown knot-resolver:root "${KRESD_DATA_DIR:?}" "${KRESD_CACHE_DIR:?}" 276 | RUN chmod 0775 "${KRESD_DATA_DIR:?}" "${KRESD_CACHE_DIR:?}" 277 | 278 | # Copy kresd config 279 | COPY --chown=root:root ./config/knot-resolver/ /etc/knot-resolver/ 280 | RUN find /etc/knot-resolver/ -type d -not -perm 0755 -exec chmod 0755 '{}' ';' 281 | RUN find /etc/knot-resolver/ -type f -not -perm 0644 -exec chmod 0644 '{}' ';' 282 | 283 | # Copy hBlock config 284 | COPY --chown=root:root ./config/hblock/ /etc/hblock/ 285 | RUN find /etc/hblock/ -type d -not -perm 0755 -exec chmod 0755 '{}' ';' 286 | RUN find /etc/hblock/ -type f -not -perm 0644 -exec chmod 0644 '{}' ';' 287 | 288 | # Copy scripts 289 | COPY --chown=root:root ./scripts/bin/ /usr/local/bin/ 290 | RUN find /usr/local/bin/ -type d -not -perm 0755 -exec chmod 0755 '{}' ';' 291 | RUN find /usr/local/bin/ -type f -not -perm 0755 -exec chmod 0755 '{}' ';' 292 | 293 | # Copy services 294 | COPY --chown=root:root ./scripts/service/ /service/ 295 | RUN find /service/ -type d -not -perm 0775 -exec chmod 0775 '{}' ';' 296 | RUN find /service/ -type f -not -perm 0775 -exec chmod 0775 '{}' ';' 297 | 298 | # Drop root privileges 299 | USER knot-resolver:root 300 | 301 | # DNS endpoint 302 | EXPOSE 53/udp 53/tcp 303 | # DNS-over-HTTPS endpoint 304 | EXPOSE 443/tcp 305 | # DNS-over-TLS endpoint 306 | EXPOSE 853/tcp 307 | # Web management endpoint 308 | EXPOSE 8453/tcp 309 | 310 | HEALTHCHECK --start-period=30s --interval=10s --timeout=5s --retries=1 CMD ["/usr/local/bin/container-healthcheck"] 311 | ENTRYPOINT ["/usr/bin/catatonit", "--", "/usr/local/bin/container-init"] 312 | 313 | ################################################## 314 | ## "test" stage 315 | ################################################## 316 | 317 | FROM base AS test 318 | 319 | # Perform a test run 320 | RUN printf '%s\n' 'Starting services...' \ 321 | && export KRESD_INSTANCE_NUMBER=2 \ 322 | && export HBLOCK_PARALLEL=1 \ 323 | && (nohup container-init &) \ 324 | && TIMEOUT_DURATION=600s \ 325 | && TIMEOUT_COMMAND='until container-healthcheck; do sleep 1; done' \ 326 | && timeout "${TIMEOUT_DURATION:?}" sh -euc "${TIMEOUT_COMMAND:?}" 327 | 328 | ################################################## 329 | ## "main" stage 330 | ################################################## 331 | 332 | FROM base AS main 333 | 334 | # Dummy instruction so BuildKit does not skip the test stage 335 | RUN --mount=type=bind,from=test,source=/mnt/,target=/mnt/ 336 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © Héctor Molinero Fernández 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | SHELL := /bin/sh 4 | .SHELLFLAGS := -euc 5 | 6 | DOCKER := $(shell command -v docker 2>/dev/null) 7 | GIT := $(shell command -v git 2>/dev/null) 8 | M4 := $(shell command -v m4 2>/dev/null) 9 | 10 | DISTDIR := ./dist 11 | DOCKERFILE_TEMPLATE := ./Dockerfile.m4 12 | 13 | IMAGE_REGISTRY := docker.io 14 | IMAGE_NAMESPACE := hectorm 15 | IMAGE_PROJECT := hblock-resolver 16 | IMAGE_NAME := $(IMAGE_REGISTRY)/$(IMAGE_NAMESPACE)/$(IMAGE_PROJECT) 17 | ifeq ($(shell '$(GIT)' status --porcelain 2>/dev/null),) 18 | IMAGE_GIT_TAG := $(shell '$(GIT)' tag --list --contains HEAD 2>/dev/null) 19 | IMAGE_GIT_SHA := $(shell '$(GIT)' rev-parse --verify --short HEAD 2>/dev/null) 20 | IMAGE_VERSION := $(if $(IMAGE_GIT_TAG),$(IMAGE_GIT_TAG),$(if $(IMAGE_GIT_SHA),$(IMAGE_GIT_SHA),nil)) 21 | else 22 | IMAGE_GIT_BRANCH := $(shell '$(GIT)' symbolic-ref --short HEAD 2>/dev/null) 23 | IMAGE_VERSION := $(if $(IMAGE_GIT_BRANCH),$(IMAGE_GIT_BRANCH)-dirty,nil) 24 | endif 25 | 26 | IMAGE_BUILD_OPTS := 27 | 28 | IMAGE_NATIVE_DOCKERFILE := $(DISTDIR)/Dockerfile 29 | IMAGE_NATIVE_TARBALL := $(DISTDIR)/$(IMAGE_PROJECT).tzst 30 | IMAGE_AMD64_DOCKERFILE := $(DISTDIR)/Dockerfile.amd64 31 | IMAGE_AMD64_TARBALL := $(DISTDIR)/$(IMAGE_PROJECT).amd64.tzst 32 | IMAGE_ARM64V8_DOCKERFILE := $(DISTDIR)/Dockerfile.arm64v8 33 | IMAGE_ARM64V8_TARBALL := $(DISTDIR)/$(IMAGE_PROJECT).arm64v8.tzst 34 | 35 | export DOCKER_BUILDKIT := 1 36 | export BUILDKIT_PROGRESS := plain 37 | 38 | ################################################## 39 | ## "all" target 40 | ################################################## 41 | 42 | .PHONY: all 43 | all: save-native-image 44 | 45 | ################################################## 46 | ## "build-*" targets 47 | ################################################## 48 | 49 | .PHONY: build-native-image 50 | build-native-image: $(IMAGE_NATIVE_DOCKERFILE) 51 | 52 | $(IMAGE_NATIVE_DOCKERFILE): $(DOCKERFILE_TEMPLATE) 53 | mkdir -p '$(DISTDIR)' 54 | '$(M4)' \ 55 | --prefix-builtins \ 56 | '$(DOCKERFILE_TEMPLATE)' > '$@' 57 | '$(DOCKER)' build $(IMAGE_BUILD_OPTS) \ 58 | --tag '$(IMAGE_NAME):$(IMAGE_VERSION)' \ 59 | --tag '$(IMAGE_NAME):latest' \ 60 | --file '$@' ./ 61 | 62 | .PHONY: build-cross-images 63 | build-cross-images: build-amd64-image build-arm64v8-image 64 | 65 | .PHONY: build-amd64-image 66 | build-amd64-image: $(IMAGE_AMD64_DOCKERFILE) 67 | 68 | $(IMAGE_AMD64_DOCKERFILE): $(DOCKERFILE_TEMPLATE) 69 | mkdir -p '$(DISTDIR)' 70 | '$(M4)' \ 71 | --prefix-builtins \ 72 | --define=CROSS_ARCH=amd64 \ 73 | '$(DOCKERFILE_TEMPLATE)' > '$@' 74 | '$(DOCKER)' build $(IMAGE_BUILD_OPTS) \ 75 | --tag '$(IMAGE_NAME):$(IMAGE_VERSION)-amd64' \ 76 | --tag '$(IMAGE_NAME):latest-amd64' \ 77 | --platform linux/amd64 \ 78 | --file '$@' ./ 79 | 80 | .PHONY: build-arm64v8-image 81 | build-arm64v8-image: $(IMAGE_ARM64V8_DOCKERFILE) 82 | 83 | $(IMAGE_ARM64V8_DOCKERFILE): $(DOCKERFILE_TEMPLATE) 84 | mkdir -p '$(DISTDIR)' 85 | '$(M4)' \ 86 | --prefix-builtins \ 87 | --define=CROSS_ARCH=arm64v8 \ 88 | '$(DOCKERFILE_TEMPLATE)' > '$@' 89 | '$(DOCKER)' build $(IMAGE_BUILD_OPTS) \ 90 | --tag '$(IMAGE_NAME):$(IMAGE_VERSION)-arm64v8' \ 91 | --tag '$(IMAGE_NAME):latest-arm64v8' \ 92 | --platform linux/arm64/v8 \ 93 | --file '$@' ./ 94 | 95 | ################################################## 96 | ## "save-*" targets 97 | ################################################## 98 | 99 | define save_image 100 | '$(DOCKER)' save '$(1)' | zstd -T0 > '$(2)' 101 | endef 102 | 103 | .PHONY: save-native-image 104 | save-native-image: $(IMAGE_NATIVE_TARBALL) 105 | 106 | $(IMAGE_NATIVE_TARBALL): $(IMAGE_NATIVE_DOCKERFILE) 107 | $(call save_image,$(IMAGE_NAME):$(IMAGE_VERSION),$@) 108 | 109 | .PHONY: save-cross-images 110 | save-cross-images: save-amd64-image save-arm64v8-image 111 | 112 | .PHONY: save-amd64-image 113 | save-amd64-image: $(IMAGE_AMD64_TARBALL) 114 | 115 | $(IMAGE_AMD64_TARBALL): $(IMAGE_AMD64_DOCKERFILE) 116 | $(call save_image,$(IMAGE_NAME):$(IMAGE_VERSION)-amd64,$@) 117 | 118 | .PHONY: save-arm64v8-image 119 | save-arm64v8-image: $(IMAGE_ARM64V8_TARBALL) 120 | 121 | $(IMAGE_ARM64V8_TARBALL): $(IMAGE_ARM64V8_DOCKERFILE) 122 | $(call save_image,$(IMAGE_NAME):$(IMAGE_VERSION)-arm64v8,$@) 123 | 124 | ################################################## 125 | ## "load-*" targets 126 | ################################################## 127 | 128 | define load_image 129 | zstd -dc '$(1)' | '$(DOCKER)' load 130 | endef 131 | 132 | define tag_image 133 | '$(DOCKER)' tag '$(1)' '$(2)' 134 | endef 135 | 136 | .PHONY: load-native-image 137 | load-native-image: 138 | $(call load_image,$(IMAGE_NATIVE_TARBALL)) 139 | $(call tag_image,$(IMAGE_NAME):$(IMAGE_VERSION),$(IMAGE_NAME):latest) 140 | 141 | .PHONY: load-cross-images 142 | load-cross-images: load-amd64-image load-arm64v8-image 143 | 144 | .PHONY: load-amd64-image 145 | load-amd64-image: 146 | $(call load_image,$(IMAGE_AMD64_TARBALL)) 147 | $(call tag_image,$(IMAGE_NAME):$(IMAGE_VERSION)-amd64,$(IMAGE_NAME):latest-amd64) 148 | 149 | .PHONY: load-arm64v8-image 150 | load-arm64v8-image: 151 | $(call load_image,$(IMAGE_ARM64V8_TARBALL)) 152 | $(call tag_image,$(IMAGE_NAME):$(IMAGE_VERSION)-arm64v8,$(IMAGE_NAME):latest-arm64v8) 153 | 154 | ################################################## 155 | ## "push-*" targets 156 | ################################################## 157 | 158 | define push_image 159 | '$(DOCKER)' push '$(1)' 160 | endef 161 | 162 | define push_cross_manifest 163 | '$(DOCKER)' manifest create --amend '$(1)' '$(2)-amd64' '$(2)-arm64v8' 164 | '$(DOCKER)' manifest annotate '$(1)' '$(2)-amd64' --os linux --arch amd64 165 | '$(DOCKER)' manifest annotate '$(1)' '$(2)-arm64v8' --os linux --arch arm64 --variant v8 166 | '$(DOCKER)' manifest push --purge '$(1)' 167 | endef 168 | 169 | .PHONY: push-native-image 170 | push-native-image: 171 | @printf '%s\n' 'Unimplemented' 172 | 173 | .PHONY: push-cross-images 174 | push-cross-images: push-amd64-image push-arm64v8-image 175 | 176 | .PHONY: push-amd64-image 177 | push-amd64-image: 178 | $(call push_image,$(IMAGE_NAME):$(IMAGE_VERSION)-amd64) 179 | $(call push_image,$(IMAGE_NAME):latest-amd64) 180 | 181 | .PHONY: push-arm64v8-image 182 | push-arm64v8-image: 183 | $(call push_image,$(IMAGE_NAME):$(IMAGE_VERSION)-arm64v8) 184 | $(call push_image,$(IMAGE_NAME):latest-arm64v8) 185 | 186 | push-cross-manifest: 187 | $(call push_cross_manifest,$(IMAGE_NAME):$(IMAGE_VERSION),$(IMAGE_NAME):$(IMAGE_VERSION)) 188 | $(call push_cross_manifest,$(IMAGE_NAME):latest,$(IMAGE_NAME):latest) 189 | 190 | ################################################## 191 | ## "binfmt-*" targets 192 | ################################################## 193 | 194 | .PHONY: binfmt-register 195 | binfmt-register: 196 | '$(DOCKER)' run --rm --privileged docker.io/hectorm/qemu-user-static:latest --reset --persistent yes --credential yes 197 | 198 | ################################################## 199 | ## "version" target 200 | ################################################## 201 | 202 | .PHONY: version 203 | version: 204 | @LATEST_IMAGE_VERSION=$$('$(GIT)' describe --abbrev=0 2>/dev/null || printf 'v0'); \ 205 | if printf '%s' "$${LATEST_IMAGE_VERSION:?}" | grep -q '^v[0-9]\{1,\}$$'; then \ 206 | NEW_IMAGE_VERSION=$$(awk -v v="$${LATEST_IMAGE_VERSION:?}" 'BEGIN {printf("v%.0f", substr(v,2)+1)}'); \ 207 | '$(GIT)' commit --allow-empty -m "$${NEW_IMAGE_VERSION:?}"; \ 208 | '$(GIT)' tag -a "$${NEW_IMAGE_VERSION:?}" -m "$${NEW_IMAGE_VERSION:?}"; \ 209 | else \ 210 | >&2 printf 'Malformed version string: %s\n' "$${LATEST_IMAGE_VERSION:?}"; \ 211 | exit 1; \ 212 | fi 213 | 214 | ################################################## 215 | ## "clean" target 216 | ################################################## 217 | 218 | .PHONY: clean 219 | clean: 220 | rm -f '$(IMAGE_NATIVE_DOCKERFILE)' '$(IMAGE_AMD64_DOCKERFILE)' '$(IMAGE_ARM64V8_DOCKERFILE)' 221 | rm -f '$(IMAGE_NATIVE_TARBALL)' '$(IMAGE_AMD64_TARBALL)' '$(IMAGE_ARM64V8_TARBALL)' 222 | if [ -d '$(DISTDIR)' ] && [ -z "$$(ls -A '$(DISTDIR)')" ]; then rmdir '$(DISTDIR)'; fi 223 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hBlock Resolver 2 | 3 | A Docker image of [Knot Resolver](https://www.knot-resolver.cz) configured to automatically block ads, tracking and malware domains with 4 | [hBlock](https://github.com/hectorm/hblock). 5 | 6 | ## Start an instance 7 | 8 | ```sh 9 | docker run --detach \ 10 | --name hblock-resolver \ 11 | --publish 127.0.0.153:53:53/udp \ 12 | --publish 127.0.0.153:53:53/tcp \ 13 | --publish 127.0.0.153:443:443/tcp \ 14 | --publish 127.0.0.153:853:853/tcp \ 15 | --publish 127.0.0.153:8453:8453/tcp \ 16 | --mount type=volume,src=hblock-resolver-data,dst=/var/lib/knot-resolver/ \ 17 | docker.io/hectorm/hblock-resolver:latest 18 | ``` 19 | 20 | > **Warning:** do not expose this service to the open internet. An open DNS resolver represents a significant threat and it can be used in a number of 21 | > different attacks, such as [DNS amplification attacks](https://www.cloudflare.com/learning/ddos/dns-amplification-ddos-attack/). 22 | 23 | ## Environment variables 24 | 25 | #### `KRESD_CACHE_SIZE` (default: `50`) 26 | Maximum cache size in megabytes. 27 | 28 | #### `KRESD_DNS{1..4}_IP` (default: `1.1.1.1@853` and `1.0.0.1@853`) 29 | IP (and optionally port) of the DNS-over-TLS server to which the queries will be forwarded 30 | ([alternative DoT servers](https://dnsprivacy.org/wiki/display/DP/DNS+Privacy+Public+Resolvers#DNSPrivacyPublicResolvers-DNS-over-TLS(DoT))). 31 | 32 | #### `KRESD_DNS{1..4}_HOSTNAME` (default: `cloudflare-dns.com`) 33 | Hostname of the DNS-over-TLS server to which the queries will be forwarded 34 | ([CA+hostname authentication docs](https://knot-resolver.readthedocs.io/en/stable/modules-policy.html#ca-hostname-authentication)). 35 | 36 | #### `KRESD_DNS{1..4}_PIN_SHA256` (default: empty) 37 | Certificate hash of the DNS-over-TLS server to which the queries will be forwarded 38 | ([key-pinned authentication docs](https://knot-resolver.readthedocs.io/en/stable/modules-policy.html#key-pinned-authentication)). 39 | 40 | #### `KRESD_INSTANCE_NUMBER` (default: `1`) 41 | Number of instances to launch. 42 | 43 | #### `KRESD_RECENTLY_BLOCKED_NUMBER` (default: `100`) 44 | Number of recently blocked domains to store in memory for each instance. 45 | The `/recently_blocked` endpoint returns an aggregated list of all instances. 46 | 47 | #### `KRESD_CERT_MANAGED` (default: `true`) 48 | If equals `true`, a self-signed certificate will be generated. You can provide your own certificate with these options: 49 | ``` 50 | --env KRESD_CERT_MANAGED=false \ 51 | --mount type=bind,src=/path/to/server.key,dst=/var/lib/knot-resolver/ssl/server.key,ro \ 52 | --mount type=bind,src=/path/to/server.crt,dst=/var/lib/knot-resolver/ssl/server.crt,ro \ 53 | ``` 54 | > **Note:** for a more advanced setup, look at the [following example](examples/caddy) with [Let's Encrypt](https://letsencrypt.org) and 55 | [Caddy](https://caddyserver.com/). 56 | 57 | #### `KRESD_NIC` (default: empty) 58 | If defined, kresd will only listen on the specified interface. Some users observed a considerable, close to 100%, performance gain in Docker 59 | containers when they bound the daemon to a single interface:ip address pair 60 | ([dynamic configuration docs](https://knot-resolver.readthedocs.io/en/latest/daemon-scripting.html?highlight=docker#lua-scripts), 61 | [CZ-NIC/knot-resolver#32](https://github.com/CZ-NIC/knot-resolver/pull/32)). 62 | 63 | #### `KRESD_LOG_LEVEL` (default: `notice`) 64 | Set the global logging level. The possible values are: `crit`, `err`, `warning`, `notice`, `info` or `debug`. 65 | 66 | ## Additional configuration 67 | 68 | Main Knot DNS Resolver configuration is located in `/etc/knot-resolver/kresd.conf`. If you would like to add additional configuration, add one or more 69 | `*.conf` files under `/etc/knot-resolver/kresd.conf.d/`. 70 | 71 | ## License 72 | 73 | See the [license](LICENSE.md) file. 74 | -------------------------------------------------------------------------------- /config/hblock/environment: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shellcheck disable=SC2034 3 | 4 | if [ ! -e "${KRESD_DATA_DIR:?}"/hblock/ ]; then 5 | mkdir -p "${KRESD_DATA_DIR:?}"/hblock/ 6 | fi 7 | 8 | HBLOCK_OUTPUT_FILE="${KRESD_DATA_DIR:?}"/hblock/blocklist.rpz 9 | 10 | if [ -e "${KRESD_DATA_DIR:?}"/hblock/sources.list ]; then 11 | HBLOCK_SOURCES_FILE="${KRESD_DATA_DIR:?}"/hblock/sources.list 12 | fi 13 | 14 | if [ -e "${KRESD_DATA_DIR:?}"/hblock/allow.list ]; then 15 | HBLOCK_ALLOWLIST_FILE="${KRESD_DATA_DIR:?}"/hblock/allow.list 16 | fi 17 | 18 | if [ -e "${KRESD_DATA_DIR:?}"/hblock/deny.list ]; then 19 | HBLOCK_DENYLIST_FILE="${KRESD_DATA_DIR:?}"/hblock/deny.list 20 | fi 21 | 22 | HBLOCK_REDIRECTION='.' 23 | HBLOCK_TEMPLATE='%D CNAME %R\n*.%D CNAME %R' 24 | HBLOCK_COMMENT=';' 25 | HBLOCK_FILTER_SUBDOMAINS='true' 26 | -------------------------------------------------------------------------------- /config/hblock/footer: -------------------------------------------------------------------------------- 1 | hblock-check.molinero.dev CNAME . 2 | -------------------------------------------------------------------------------- /config/hblock/header: -------------------------------------------------------------------------------- 1 | $TTL 2h 2 | @ IN SOA localhost. root.localhost. (1 6h 1h 1w 2h) 3 | IN NS localhost. 4 | -------------------------------------------------------------------------------- /config/knot-resolver/kresd.conf: -------------------------------------------------------------------------------- 1 | -- Main configuration of Knot Resolver. 2 | -- Refer to manual: https://knot-resolver.readthedocs.io/en/latest/daemon.html#configuration 3 | 4 | -- Load configuration from kresd.conf.d/ directory 5 | 6 | local lfs = require('lfs') 7 | local conf_dir = env.KRESD_CONF_DIR .. '/kresd.conf.d' 8 | 9 | if lfs.attributes(conf_dir) ~= nil then 10 | local conf_files = {} 11 | for entry in lfs.dir(conf_dir) do 12 | if entry:sub(-5) == '.conf' then 13 | table.insert(conf_files, entry) 14 | end 15 | end 16 | table.sort(conf_files) 17 | for i = 1, #conf_files do 18 | dofile(conf_dir .. '/' .. conf_files[i]) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /config/knot-resolver/kresd.conf.d/000-utils.conf: -------------------------------------------------------------------------------- 1 | local notify = require('cqueues.notify') 2 | 3 | function exists_file(path) 4 | local file, err = io.open(path, 'r') 5 | if err ~= nil then 6 | return false 7 | end 8 | file:close() 9 | return true 10 | end 11 | 12 | function read_file(path) 13 | local file, err = io.open(path, 'r') 14 | if err ~= nil then 15 | io.stderr:write(err .. '\n') 16 | return nil 17 | end 18 | local content = file:read('*all') 19 | file:close() 20 | return content 21 | end 22 | 23 | function write_file(path, content) 24 | local file, err = io.open(path, 'w') 25 | if err ~= nil then 26 | io.stderr:write(err .. '\n') 27 | return false 28 | end 29 | file:write(content) 30 | file:close() 31 | return true 32 | end 33 | 34 | function delete_file(path) 35 | local ok, err = os.remove(path) 36 | if err ~= nil then 37 | io.stderr:write(err .. '\n') 38 | return false 39 | end 40 | return true 41 | end 42 | 43 | function watch_file(path, cb) 44 | local dirname = path:match('^(.+)/.+$') 45 | local basename = path:match('^.+/(.+)$') 46 | 47 | local watcher = notify.opendir(dirname) 48 | watcher:add(basename) 49 | 50 | worker.coroutine(function () 51 | for flags, name in watcher:changes() do 52 | if name == basename then 53 | cb(flags, name) 54 | end 55 | end 56 | end) 57 | end 58 | 59 | function start_prog(path) 60 | local pipe, err = io.popen('command ' .. path .. ' 2>&1; printf %s $?', 'r') 61 | if err ~= nil then 62 | io.stderr:write(err .. '\n') 63 | return '', 1 64 | end 65 | local out, exit = '', 1 66 | local next = pipe:lines() 67 | local curr_val = next() 68 | while curr_val ~= nil do 69 | local next_val = next() 70 | if next_val ~= nil then 71 | line = curr_val .. '\n' 72 | out = out .. line 73 | io.stdout:write(line) 74 | else 75 | exit = tonumber(curr_val) 76 | end 77 | curr_val = next_val 78 | end 79 | pipe:close() 80 | return out, exit 81 | end 82 | -------------------------------------------------------------------------------- /config/knot-resolver/kresd.conf.d/010-log.conf: -------------------------------------------------------------------------------- 1 | -- Set the global logging level 2 | 3 | log_level(env.KRESD_LOG_LEVEL) 4 | -------------------------------------------------------------------------------- /config/knot-resolver/kresd.conf.d/010-watcher.conf: -------------------------------------------------------------------------------- 1 | -- Watch some files 2 | 3 | while not exists_file(env.KRESD_DATA_DIR .. '/ssl/server.key') do 4 | io.stderr:write('Waiting for TLS private key availability...\n') 5 | worker.sleep(3) 6 | end 7 | 8 | while not exists_file(env.KRESD_DATA_DIR .. '/ssl/server.crt') do 9 | io.stderr:write('Waiting for TLS certificate availability...\n') 10 | worker.sleep(3) 11 | end 12 | 13 | while not exists_file(env.KRESD_DATA_DIR .. '/hblock/blocklist.rpz') do 14 | io.stderr:write('Waiting for blocklist availability...\n') 15 | worker.sleep(3) 16 | end 17 | 18 | watch_file(env.KRESD_DATA_DIR .. '/ssl/server.key', function () 19 | io.stdout:write('TLS private key changed, restarting kresd...\n') 20 | os.exit(0) 21 | end) 22 | 23 | watch_file(env.KRESD_DATA_DIR .. '/ssl/server.crt', function () 24 | io.stdout:write('TLS certificate changed, restarting kresd...\n') 25 | os.exit(0) 26 | end) 27 | -------------------------------------------------------------------------------- /config/knot-resolver/kresd.conf.d/020-cache.conf: -------------------------------------------------------------------------------- 1 | -- Define cache size 2 | 3 | cache.open(env.KRESD_CACHE_SIZE * MB, 'lmdb://' .. env.KRESD_CACHE_DIR) 4 | -------------------------------------------------------------------------------- /config/knot-resolver/kresd.conf.d/020-net.conf: -------------------------------------------------------------------------------- 1 | -- Listen on defined interfaces 2 | 3 | local nicname = env.KRESD_NIC 4 | net_interfaces = net.interfaces() 5 | net_addresses = nil 6 | 7 | if nicname == nil or nicname == '' then 8 | local nicnames = {}; for k, v in pairs(net_interfaces) do table.insert(nicnames, k) end 9 | io.stdout:write('Listening on all interfaces (' .. table.concat(nicnames, ', ') .. ')\n') 10 | net_addresses = net_interfaces 11 | elseif net_interfaces[nicname] ~= nil then 12 | io.stdout:write('Listening on ' .. nicname .. ' interface\n') 13 | net_addresses = net_interfaces[nicname] 14 | else 15 | io.stderr:write('Cannot find ' .. nicname .. ' interface\n') 16 | os.exit(1) 17 | end 18 | 19 | net.tls(env.KRESD_DATA_DIR .. '/ssl/server.crt', env.KRESD_DATA_DIR .. '/ssl/server.key') 20 | -------------------------------------------------------------------------------- /config/knot-resolver/kresd.conf.d/030-dns-over-https.conf: -------------------------------------------------------------------------------- 1 | net.listen(net_addresses, 443, { kind = 'doh2' }) 2 | -------------------------------------------------------------------------------- /config/knot-resolver/kresd.conf.d/030-dns-over-tls.conf: -------------------------------------------------------------------------------- 1 | net.listen(net_addresses, 853, { kind = 'tls' }) 2 | -------------------------------------------------------------------------------- /config/knot-resolver/kresd.conf.d/030-dns.conf: -------------------------------------------------------------------------------- 1 | net.listen(net_addresses, 53, { kind = 'dns' }) 2 | -------------------------------------------------------------------------------- /config/knot-resolver/kresd.conf.d/040-module-bogus-log.conf: -------------------------------------------------------------------------------- 1 | modules.load('bogus_log') 2 | -------------------------------------------------------------------------------- /config/knot-resolver/kresd.conf.d/040-module-hints.conf: -------------------------------------------------------------------------------- 1 | modules.load('hints > iterate') 2 | -------------------------------------------------------------------------------- /config/knot-resolver/kresd.conf.d/040-module-rebinding.conf: -------------------------------------------------------------------------------- 1 | modules.load('rebinding < iterate') 2 | -------------------------------------------------------------------------------- /config/knot-resolver/kresd.conf.d/040-module-stats.conf: -------------------------------------------------------------------------------- 1 | modules.load('stats') 2 | -------------------------------------------------------------------------------- /config/knot-resolver/kresd.conf.d/050-module-policy.conf: -------------------------------------------------------------------------------- 1 | modules.load('policy > hints') 2 | -------------------------------------------------------------------------------- /config/knot-resolver/kresd.conf.d/050-module-view.conf: -------------------------------------------------------------------------------- 1 | modules.load('view < cache') 2 | -------------------------------------------------------------------------------- /config/knot-resolver/kresd.conf.d/060-module-http.conf: -------------------------------------------------------------------------------- 1 | modules.load('http') 2 | 3 | -- Add healthcheck endpoint 4 | http.configs._builtin.webmgmt.endpoints['/health'] = { 'text/plain', 'OK' } 5 | 6 | -- Setup built-in web management endpoint 7 | http.config({ 8 | port = 8453, 9 | tls = true, 10 | ephemeral = false, 11 | key = env.KRESD_DATA_DIR .. '/ssl/server.key', 12 | cert = env.KRESD_DATA_DIR .. '/ssl/server.crt', 13 | endpoints = webmgmt_endpoints 14 | }, 'webmgmt') 15 | 16 | net.listen(net_addresses, 8453, { kind = 'webmgmt' }) 17 | -------------------------------------------------------------------------------- /config/knot-resolver/kresd.conf.d/070-policy-special.conf: -------------------------------------------------------------------------------- 1 | -- Add rules for special-use and locally-served domains 2 | -- https://www.iana.org/assignments/special-use-domain-names/ 3 | -- https://www.iana.org/assignments/locally-served-dns-zones/ 4 | for _, rule in ipairs(policy.special_names) do 5 | policy.add(rule.cb) 6 | end 7 | 8 | -- Disable DNS-over-HTTPS in applications 9 | -- https://support.mozilla.org/en-US/kb/canary-domain-use-application-dnsnet/ 10 | policy.add(policy.suffix( 11 | policy.DENY_MSG('This network is unsuitable for DNS-over-HTTPS'), 12 | {todname('use-application-dns.net.')} 13 | )) 14 | 15 | -- Resolve "*-dnsotls-ds.metric.gstatic.com" as it is necessary for DNS-over-TLS functionality on Android 16 | -- https://android.googlesource.com/platform/packages/modules/DnsResolver/+/bab3daa733894008bf917713f9a72a4ccbbd3b3a/DnsTlsTransport.cpp#150 17 | policy.add(policy.pattern( 18 | policy.ANSWER({ 19 | [kres.type.A] = { rdata = kres.str2ip('127.0.0.1'), ttl = 300 }, 20 | [kres.type.AAAA] = { rdata = kres.str2ip('::1'), ttl = 300 } 21 | }, true), 22 | '%w+%-dnsotls%-ds' .. todname('metric.gstatic.com.') .. '$' 23 | )) 24 | -------------------------------------------------------------------------------- /config/knot-resolver/kresd.conf.d/080-policy-blocklist.conf: -------------------------------------------------------------------------------- 1 | -- Add blocklist zone 2 | 3 | if not stats then modules.load('stats') end 4 | if not http then modules.load('http') end 5 | 6 | local lru = require('lru') 7 | local recently_blocked = lru.new(tonumber(env.KRESD_RECENTLY_BLOCKED_NUMBER)) 8 | 9 | local blocked_metric = 'answer.blocked' 10 | stats[blocked_metric] = 0 11 | 12 | local function deny_and_count(msg) 13 | local deny = policy.DENY_MSG(msg) 14 | local is_debug = log_level() == 'debug' 15 | return function (_, req) 16 | local qry = req:current() 17 | local qname = kres.dname2str(qry:name()):lower() 18 | local count = recently_blocked:get(qname) 19 | if count == nil then count = 0 end 20 | recently_blocked:set(qname, count + 1) 21 | stats[blocked_metric] = stats[blocked_metric] + 1 22 | if is_debug then 23 | io.stdout:write('[poli] blocked domain: ' .. qname .. '\n') 24 | end 25 | return deny(_, req) 26 | end 27 | end 28 | 29 | policy.add(policy.rpz( 30 | deny_and_count('Blocked domain'), 31 | env.KRESD_DATA_DIR .. '/hblock/blocklist.rpz', 32 | true 33 | )) 34 | 35 | function get_recently_blocked() 36 | local rb = {} 37 | for qname, count in recently_blocked:pairs() do 38 | rb[qname] = count 39 | end 40 | return rb 41 | end 42 | 43 | http.configs._builtin.webmgmt.endpoints['/recently_blocked'] = { 44 | 'application/json', 45 | function () 46 | local out = {} 47 | for _, result in pairs(map('get_recently_blocked()')) do 48 | if type(result) == 'table' then 49 | for qname, count in pairs(result) do 50 | out[qname] = (out[qname] or 0) + count 51 | end 52 | end 53 | end 54 | return tojson(out) 55 | end 56 | } 57 | -------------------------------------------------------------------------------- /config/knot-resolver/kresd.conf.d/090-policy-forward.conf: -------------------------------------------------------------------------------- 1 | -- DNS over TLS forwarding 2 | -- https://dnsprivacy.org/wiki/display/DP/DNS+Privacy+Public+Resolvers#DNSPrivacyPublicResolvers-DNS-over-TLS(DoT) 3 | 4 | local servers = {} 5 | 6 | for i = 1,4 do 7 | local ip = env['KRESD_DNS' .. i .. '_IP'] 8 | 9 | if ip ~= nil and ip ~= '' then 10 | local server = { ip } 11 | io.stdout:write('DoT server ' .. i .. ': ' .. ip) 12 | 13 | local hostname = env['KRESD_DNS' .. i .. '_HOSTNAME'] 14 | if hostname ~= nil and hostname ~= '' then 15 | server['hostname'] = hostname 16 | io.stdout:write(' (hostname = ' .. hostname .. ')') 17 | end 18 | 19 | local pin_sha256 = env['KRESD_DNS' .. i .. '_PIN_SHA256'] 20 | if pin_sha256 ~= nil and pin_sha256 ~= '' then 21 | server['pin_sha256'] = pin_sha256 22 | io.stdout:write(' (pin_sha256 = ' .. pin_sha256 .. ')') 23 | else 24 | server['ca_file'] = '/etc/ssl/certs/ca-certificates.crt' 25 | end 26 | 27 | table.insert(servers, server) 28 | io.stdout:write('\n') 29 | end 30 | end 31 | 32 | policy.add(policy.all(policy.TLS_FORWARD(servers))) 33 | -------------------------------------------------------------------------------- /config/knot-resolver/kresd.conf.d/100-http-blocklist.conf: -------------------------------------------------------------------------------- 1 | -- Expose blocklist zone API and web UI 2 | 3 | if not http then modules.load('http') end 4 | 5 | http.configs._builtin.webmgmt.endpoints['/hblock'] = { 6 | 'text/plain', 7 | function (h, stream) 8 | local method = h:get(':method') 9 | local option = h:get(':path'):match('^/[^/]*/([^/]*)') 10 | if option == 'config' then 11 | local name = h:get(':path'):match('^/[^/]*/config/([^/]*)') 12 | 13 | local path = nil 14 | if name == 'sources' then path = env.KRESD_DATA_DIR .. '/hblock/sources.list' 15 | elseif name == 'allowlist' then path = env.KRESD_DATA_DIR .. '/hblock/allow.list' 16 | elseif name == 'denylist' then path = env.KRESD_DATA_DIR .. '/hblock/deny.list' 17 | else return 400, '' end 18 | 19 | -- GET method 20 | if method == 'GET' then 21 | if exists_file(path) then 22 | local content = read_file(path) 23 | if content ~= nil then 24 | return 200, content 25 | else 26 | return 500, '' 27 | end 28 | end 29 | return 404, '' 30 | -- POST method 31 | elseif method == 'POST' then 32 | local content = stream:get_body_as_string() 33 | if write_file(path, content) then 34 | return 200, '' 35 | else 36 | return 500, '' 37 | end 38 | -- DELETE method 39 | elseif method == 'DELETE' then 40 | if not exists_file(path) or delete_file(path) then 41 | return 200, '' 42 | else 43 | return 500, '' 44 | end 45 | end 46 | elseif option == 'update' then 47 | -- POST method 48 | if method == 'POST' then 49 | local out, exit = start_prog('hblock') 50 | if exit == 0 then 51 | return 200, out 52 | else 53 | return 500, out 54 | end 55 | end 56 | end 57 | end 58 | } 59 | 60 | http.snippets['/hblock'] = { 61 | 'hBlock config', 62 | [[ 63 |