├── .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 |
64 |
65 | 69 | 70 |
71 |
72 | 76 | 77 |
78 |
79 | 83 | 84 |
85 |
86 | 87 | 88 |
89 |
90 | 109 | 201 | ]] 202 | } 203 | -------------------------------------------------------------------------------- /examples/traefik-grafana/.env.sample: -------------------------------------------------------------------------------- 1 | DOMAIN=hblock-resolver.localhost 2 | 3 | # The unhashed password is "password" 4 | TRAEFIK_BASIC_AUTH=admin:$$2y$$12$$Q80Ser2QXlWFnJuqj5nQjOt1gvYDXxpAZuaRUaq8vilEXTAPCOhfW 5 | #TRAEFIK_TLS_RESOLVER=acme-staging-http-01 6 | #TRAEFIK_TLS_RESOLVER=acme-production-http-01 7 | 8 | GRAFANA_ADMIN_PASSWORD=password 9 | GRAFANA_SECRET_KEY=H4!b5at+kWls-8yh4Guq 10 | -------------------------------------------------------------------------------- /examples/traefik-grafana/compose.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json 2 | name: "hblock-resolver" 3 | 4 | # https://hub.docker.com/_/traefik 5 | x-traefik-image: &traefik-image "docker.io/traefik:3.1" 6 | # https://hub.docker.com/r/hectorm/hblock-resolver 7 | x-hblock-resolver-image: &hblock-resolver-image "docker.io/hectorm/hblock-resolver:latest" 8 | # https://hub.docker.com/r/grafana/grafana 9 | x-grafana-image: &grafana-image "docker.io/grafana/grafana:11.0.1" 10 | # https://hub.docker.com/r/prom/prometheus 11 | x-prometheus-image: &prometheus-image "docker.io/prom/prometheus:v2.53.1" 12 | 13 | services: 14 | 15 | traefik: 16 | image: *traefik-image 17 | restart: "on-failure:3" 18 | hostname: "traefik" 19 | networks: 20 | - "hblock-resolver" 21 | ports: 22 | - "80:80/tcp" # HTTP. 23 | - "443:443/tcp" # HTTPS. 24 | - "443:443/udp" # HTTPS (QUIC). 25 | - "853:853/tcp" # DNS over TLS. 26 | volumes: 27 | - "./config/traefik/traefik.yml:/etc/traefik/traefik.yml:ro" 28 | - "./config/traefik/dynamic/:/etc/traefik/dynamic/:ro" 29 | - "traefik-acme:/etc/traefik/acme/" 30 | environment: 31 | TRAEFIK_BASIC_AUTH: "${TRAEFIK_BASIC_AUTH:-}" 32 | TRAEFIK_TLS_RESOLVER: "${TRAEFIK_TLS_RESOLVER:-}" 33 | DOMAIN: "${DOMAIN:?}" 34 | healthcheck: 35 | test: ["CMD", "traefik", "healthcheck"] 36 | start_period: "120s" 37 | start_interval: "5s" 38 | interval: "30s" 39 | timeout: "10s" 40 | retries: 2 41 | 42 | hblock-resolver: 43 | image: *hblock-resolver-image 44 | restart: "on-failure:3" 45 | hostname: "hblock-resolver" 46 | networks: 47 | - "hblock-resolver" 48 | ports: 49 | - "127.0.0.153:53:53/udp" # DNS over UDP. 50 | - "127.0.0.153:53:53/tcp" # DNS over TCP. 51 | volumes: 52 | - "hblock-resolver-data:/var/lib/knot-resolver/" 53 | environment: 54 | KRESD_INSTANCE_NUMBER: "${KRESD_INSTANCE_NUMBER:-4}" 55 | 56 | grafana: 57 | image: *grafana-image 58 | restart: "on-failure:3" 59 | hostname: "grafana" 60 | networks: 61 | - "hblock-resolver" 62 | volumes: 63 | - "./config/grafana/provisioning/:/etc/grafana/provisioning/:ro" 64 | - "grafana-data:/var/lib/grafana/" 65 | environment: 66 | GF_SERVER_ROOT_URL: "https://${DOMAIN:?}/grafana/" 67 | GF_SERVER_SERVE_FROM_SUB_PATH: "true" 68 | GF_SECURITY_ADMIN_USER: "admin" 69 | GF_SECURITY_ADMIN_PASSWORD: "${GRAFANA_ADMIN_PASSWORD:?}" 70 | GF_SECURITY_SECRET_KEY: "${GRAFANA_SECRET_KEY:?}" 71 | GF_SECURITY_COOKIE_SECURE: "true" 72 | GF_SECURITY_DISABLE_GRAVATAR: "true" 73 | GF_USERS_ALLOW_SIGN_UP: "false" 74 | GF_USERS_ALLOW_ORG_CREATE: "false" 75 | GF_AUTH_BASIC_ENABLED: "false" 76 | GF_ANALYTICS_REPORTING_ENABLED: "false" 77 | GF_ANALYTICS_CHECK_FOR_UPDATES: "false" 78 | healthcheck: 79 | test: ["CMD", "wget", "-qO", "/dev/null", "http://localhost:3000/api/health"] 80 | start_period: "120s" 81 | start_interval: "5s" 82 | interval: "30s" 83 | timeout: "10s" 84 | retries: 2 85 | 86 | prometheus: 87 | image: *prometheus-image 88 | restart: "on-failure:3" 89 | hostname: "prometheus" 90 | networks: 91 | - "hblock-resolver" 92 | volumes: 93 | - "./config/prometheus/:/etc/prometheus/:ro" 94 | - "prometheus-data:/prometheus/" 95 | command: 96 | - "--web.external-url=https://${DOMAIN:?}/prometheus/" 97 | - "--web.route-prefix=/prometheus/" 98 | - "--config.file=/etc/prometheus/prometheus.yml" 99 | - "--storage.tsdb.path=/prometheus/" 100 | - "--web.console.libraries=/usr/share/prometheus/console_libraries/" 101 | - "--web.console.templates=/usr/share/prometheus/consoles/" 102 | healthcheck: 103 | test: ["CMD", "wget", "-qO", "/dev/null", "http://localhost:9090/prometheus/-/healthy"] 104 | start_period: "120s" 105 | start_interval: "5s" 106 | interval: "30s" 107 | timeout: "10s" 108 | retries: 2 109 | 110 | volumes: 111 | 112 | traefik-acme: 113 | 114 | hblock-resolver-data: 115 | 116 | grafana-data: 117 | 118 | prometheus-data: 119 | 120 | networks: 121 | 122 | hblock-resolver: 123 | -------------------------------------------------------------------------------- /examples/traefik-grafana/config/grafana/provisioning/dashboards/dashboards.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: "dashboards" 5 | orgId: 1 6 | folder: "" 7 | type: "file" 8 | disableDeletion: true 9 | updateIntervalSeconds: 60 10 | allowUiUpdates: false 11 | options: 12 | path: "/etc/grafana/provisioning/dashboards/" 13 | -------------------------------------------------------------------------------- /examples/traefik-grafana/config/grafana/provisioning/dashboards/main.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": "-- Grafana --", 7 | "enable": true, 8 | "hide": true, 9 | "iconColor": "rgba(0, 211, 255, 1)", 10 | "name": "Annotations & Alerts", 11 | "target": { 12 | "limit": 100, 13 | "matchAny": false, 14 | "tags": [], 15 | "type": "dashboard" 16 | }, 17 | "type": "dashboard" 18 | } 19 | ] 20 | }, 21 | "editable": true, 22 | "fiscalYearStartMonth": 0, 23 | "graphTooltip": 0, 24 | "id": null, 25 | "links": [], 26 | "liveNow": false, 27 | "panels": [ 28 | { 29 | "datasource": "prometheus", 30 | "fieldConfig": { 31 | "defaults": { 32 | "color": { 33 | "fixedColor": "green", 34 | "mode": "fixed" 35 | }, 36 | "decimals": 0, 37 | "mappings": [], 38 | "min": 0, 39 | "thresholds": { 40 | "mode": "absolute", 41 | "steps": [ 42 | { 43 | "color": "green", 44 | "value": null 45 | } 46 | ] 47 | }, 48 | "unit": "none" 49 | }, 50 | "overrides": [] 51 | }, 52 | "gridPos": { 53 | "h": 4, 54 | "w": 8, 55 | "x": 0, 56 | "y": 0 57 | }, 58 | "id": 1, 59 | "options": { 60 | "colorMode": "value", 61 | "graphMode": "none", 62 | "justifyMode": "auto", 63 | "orientation": "auto", 64 | "reduceOptions": { 65 | "calcs": [ 66 | "last" 67 | ], 68 | "fields": "", 69 | "values": false 70 | }, 71 | "textMode": "value" 72 | }, 73 | "pluginVersion": "8.3.3", 74 | "targets": [ 75 | { 76 | "expr": "sum(increase(answer_total[$__range]))", 77 | "instant": true, 78 | "interval": "", 79 | "legendFormat": "", 80 | "refId": "A" 81 | } 82 | ], 83 | "title": "Total answers", 84 | "type": "stat" 85 | }, 86 | { 87 | "datasource": "prometheus", 88 | "fieldConfig": { 89 | "defaults": { 90 | "color": { 91 | "fixedColor": "red", 92 | "mode": "fixed" 93 | }, 94 | "decimals": 0, 95 | "mappings": [], 96 | "min": 0, 97 | "thresholds": { 98 | "mode": "absolute", 99 | "steps": [ 100 | { 101 | "color": "red", 102 | "value": null 103 | } 104 | ] 105 | }, 106 | "unit": "none" 107 | }, 108 | "overrides": [] 109 | }, 110 | "gridPos": { 111 | "h": 4, 112 | "w": 8, 113 | "x": 8, 114 | "y": 0 115 | }, 116 | "id": 2, 117 | "options": { 118 | "colorMode": "value", 119 | "graphMode": "none", 120 | "justifyMode": "auto", 121 | "orientation": "auto", 122 | "reduceOptions": { 123 | "calcs": [ 124 | "last" 125 | ], 126 | "fields": "", 127 | "values": false 128 | }, 129 | "textMode": "value" 130 | }, 131 | "pluginVersion": "8.3.3", 132 | "targets": [ 133 | { 134 | "expr": "sum(increase(answer_blocked[$__range]))", 135 | "instant": true, 136 | "interval": "", 137 | "legendFormat": "", 138 | "refId": "A" 139 | } 140 | ], 141 | "title": "Blocked answers", 142 | "type": "stat" 143 | }, 144 | { 145 | "datasource": "prometheus", 146 | "fieldConfig": { 147 | "defaults": { 148 | "color": { 149 | "mode": "continuous-GrYlRd" 150 | }, 151 | "decimals": 1, 152 | "mappings": [], 153 | "max": 1, 154 | "min": 0, 155 | "thresholds": { 156 | "mode": "absolute", 157 | "steps": [ 158 | { 159 | "color": "green", 160 | "value": null 161 | } 162 | ] 163 | }, 164 | "unit": "percentunit" 165 | }, 166 | "overrides": [] 167 | }, 168 | "gridPos": { 169 | "h": 4, 170 | "w": 8, 171 | "x": 16, 172 | "y": 0 173 | }, 174 | "id": 3, 175 | "options": { 176 | "colorMode": "value", 177 | "graphMode": "none", 178 | "justifyMode": "auto", 179 | "orientation": "auto", 180 | "reduceOptions": { 181 | "calcs": [ 182 | "last" 183 | ], 184 | "fields": "", 185 | "values": false 186 | }, 187 | "textMode": "value" 188 | }, 189 | "pluginVersion": "8.3.3", 190 | "targets": [ 191 | { 192 | "expr": "sum(increase(answer_blocked[$__range])) / sum(increase(answer_total[$__range]))", 193 | "instant": true, 194 | "interval": "", 195 | "legendFormat": "", 196 | "refId": "A" 197 | } 198 | ], 199 | "title": "Blocked answers (%)", 200 | "type": "stat" 201 | }, 202 | { 203 | "datasource": "prometheus", 204 | "fieldConfig": { 205 | "defaults": { 206 | "color": { 207 | "mode": "palette-classic" 208 | }, 209 | "custom": { 210 | "axisLabel": "", 211 | "axisPlacement": "auto", 212 | "barAlignment": 0, 213 | "drawStyle": "bars", 214 | "fillOpacity": 100, 215 | "gradientMode": "none", 216 | "hideFrom": { 217 | "legend": false, 218 | "tooltip": false, 219 | "viz": false 220 | }, 221 | "lineInterpolation": "linear", 222 | "lineWidth": 1, 223 | "pointSize": 5, 224 | "scaleDistribution": { 225 | "type": "linear" 226 | }, 227 | "showPoints": "never", 228 | "spanNulls": true, 229 | "stacking": { 230 | "group": "A", 231 | "mode": "normal" 232 | }, 233 | "thresholdsStyle": { 234 | "mode": "off" 235 | } 236 | }, 237 | "decimals": 0, 238 | "mappings": [], 239 | "min": 0, 240 | "thresholds": { 241 | "mode": "absolute", 242 | "steps": [ 243 | { 244 | "color": "green", 245 | "value": null 246 | }, 247 | { 248 | "color": "red", 249 | "value": 80 250 | } 251 | ] 252 | }, 253 | "unit": "short" 254 | }, 255 | "overrides": [] 256 | }, 257 | "gridPos": { 258 | "h": 9, 259 | "w": 24, 260 | "x": 0, 261 | "y": 4 262 | }, 263 | "id": 4, 264 | "options": { 265 | "legend": { 266 | "calcs": [ 267 | "sum" 268 | ], 269 | "displayMode": "list", 270 | "placement": "bottom" 271 | }, 272 | "tooltip": { 273 | "mode": "single" 274 | } 275 | }, 276 | "pluginVersion": "8.3.3", 277 | "targets": [ 278 | { 279 | "expr": "sum(irate(request_internal[$__rate_interval]) * $__interval_ms / 1000)", 280 | "instant": false, 281 | "interval": "", 282 | "legendFormat": "Internal", 283 | "refId": "A" 284 | }, 285 | { 286 | "expr": "sum(irate(request_udp[$__rate_interval]) * $__interval_ms / 1000)", 287 | "instant": false, 288 | "interval": "", 289 | "legendFormat": "UDP", 290 | "refId": "B" 291 | }, 292 | { 293 | "expr": "sum(irate(request_tcp[$__rate_interval]) * $__interval_ms / 1000)", 294 | "instant": false, 295 | "interval": "", 296 | "legendFormat": "TCP", 297 | "refId": "C" 298 | }, 299 | { 300 | "expr": "sum(irate(request_dot[$__rate_interval]) * $__interval_ms / 1000)", 301 | "instant": false, 302 | "interval": "", 303 | "legendFormat": "DoT", 304 | "refId": "D" 305 | }, 306 | { 307 | "expr": "sum(irate(request_doh[$__rate_interval]) * $__interval_ms / 1000)", 308 | "instant": false, 309 | "interval": "", 310 | "legendFormat": "DoH", 311 | "refId": "E" 312 | }, 313 | { 314 | "expr": "sum(irate(request_xdp[$__rate_interval]) * $__interval_ms / 1000)", 315 | "instant": false, 316 | "interval": "", 317 | "legendFormat": "XDP", 318 | "refId": "F" 319 | } 320 | ], 321 | "title": "Request sources", 322 | "type": "timeseries" 323 | }, 324 | { 325 | "datasource": "prometheus", 326 | "fieldConfig": { 327 | "defaults": { 328 | "color": { 329 | "mode": "palette-classic" 330 | }, 331 | "custom": { 332 | "axisLabel": "", 333 | "axisPlacement": "auto", 334 | "barAlignment": 0, 335 | "drawStyle": "bars", 336 | "fillOpacity": 100, 337 | "gradientMode": "none", 338 | "hideFrom": { 339 | "legend": false, 340 | "tooltip": false, 341 | "viz": false 342 | }, 343 | "lineInterpolation": "linear", 344 | "lineWidth": 1, 345 | "pointSize": 5, 346 | "scaleDistribution": { 347 | "type": "linear" 348 | }, 349 | "showPoints": "never", 350 | "spanNulls": true, 351 | "stacking": { 352 | "group": "A", 353 | "mode": "normal" 354 | }, 355 | "thresholdsStyle": { 356 | "mode": "off" 357 | } 358 | }, 359 | "decimals": 0, 360 | "mappings": [], 361 | "min": 0, 362 | "thresholds": { 363 | "mode": "absolute", 364 | "steps": [ 365 | { 366 | "color": "green", 367 | "value": null 368 | }, 369 | { 370 | "color": "red", 371 | "value": 80 372 | } 373 | ] 374 | }, 375 | "unit": "short" 376 | }, 377 | "overrides": [] 378 | }, 379 | "gridPos": { 380 | "h": 9, 381 | "w": 24, 382 | "x": 0, 383 | "y": 13 384 | }, 385 | "id": 5, 386 | "options": { 387 | "legend": { 388 | "calcs": [ 389 | "sum" 390 | ], 391 | "displayMode": "list", 392 | "placement": "bottom" 393 | }, 394 | "tooltip": { 395 | "mode": "single" 396 | } 397 | }, 398 | "pluginVersion": "8.3.3", 399 | "targets": [ 400 | { 401 | "expr": "sum(irate(answer_cached[$__rate_interval]) * $__interval_ms / 1000)", 402 | "instant": false, 403 | "interval": "", 404 | "legendFormat": "Cached", 405 | "refId": "A" 406 | }, 407 | { 408 | "expr": "sum(irate(answer_total[$__rate_interval]) * $__interval_ms / 1000) - sum(irate(answer_cached[$__rate_interval]) * $__interval_ms / 1000)", 409 | "instant": false, 410 | "interval": "", 411 | "legendFormat": "Uncached", 412 | "refId": "B" 413 | } 414 | ], 415 | "title": "Cached/Uncached answers", 416 | "type": "timeseries" 417 | }, 418 | { 419 | "datasource": "prometheus", 420 | "fieldConfig": { 421 | "defaults": { 422 | "color": { 423 | "mode": "palette-classic" 424 | }, 425 | "custom": { 426 | "axisLabel": "", 427 | "axisPlacement": "auto", 428 | "barAlignment": 0, 429 | "drawStyle": "bars", 430 | "fillOpacity": 100, 431 | "gradientMode": "none", 432 | "hideFrom": { 433 | "legend": false, 434 | "tooltip": false, 435 | "viz": false 436 | }, 437 | "lineInterpolation": "linear", 438 | "lineWidth": 1, 439 | "pointSize": 5, 440 | "scaleDistribution": { 441 | "type": "linear" 442 | }, 443 | "showPoints": "never", 444 | "spanNulls": true, 445 | "stacking": { 446 | "group": "A", 447 | "mode": "normal" 448 | }, 449 | "thresholdsStyle": { 450 | "mode": "off" 451 | } 452 | }, 453 | "decimals": 0, 454 | "mappings": [], 455 | "min": 0, 456 | "thresholds": { 457 | "mode": "absolute", 458 | "steps": [ 459 | { 460 | "color": "green", 461 | "value": null 462 | }, 463 | { 464 | "color": "red", 465 | "value": 80 466 | } 467 | ] 468 | }, 469 | "unit": "short" 470 | }, 471 | "overrides": [] 472 | }, 473 | "gridPos": { 474 | "h": 9, 475 | "w": 24, 476 | "x": 0, 477 | "y": 22 478 | }, 479 | "id": 6, 480 | "options": { 481 | "legend": { 482 | "calcs": [ 483 | "sum" 484 | ], 485 | "displayMode": "list", 486 | "placement": "bottom" 487 | }, 488 | "tooltip": { 489 | "mode": "single" 490 | } 491 | }, 492 | "pluginVersion": "8.3.3", 493 | "targets": [ 494 | { 495 | "expr": "sum(irate(answer_noerror[$__rate_interval]) * $__interval_ms / 1000)", 496 | "instant": false, 497 | "interval": "", 498 | "legendFormat": "NOERROR", 499 | "refId": "A" 500 | }, 501 | { 502 | "expr": "sum(irate(answer_nodata[$__rate_interval]) * $__interval_ms / 1000)", 503 | "instant": false, 504 | "interval": "", 505 | "legendFormat": "NODATA", 506 | "refId": "B" 507 | }, 508 | { 509 | "expr": "sum(irate(answer_nxdomain[$__rate_interval]) * $__interval_ms / 1000)", 510 | "instant": false, 511 | "interval": "", 512 | "legendFormat": "NXDOMAIN", 513 | "refId": "C" 514 | }, 515 | { 516 | "expr": "sum(irate(answer_servfail[$__rate_interval]) * $__interval_ms / 1000)", 517 | "instant": false, 518 | "interval": "", 519 | "legendFormat": "SERVFAIL", 520 | "refId": "D" 521 | } 522 | ], 523 | "title": "Answer RCODEs", 524 | "type": "timeseries" 525 | }, 526 | { 527 | "cards": {}, 528 | "color": { 529 | "cardColor": "#b4ff00", 530 | "colorScale": "sqrt", 531 | "colorScheme": "interpolateSpectral", 532 | "exponent": 0.5, 533 | "min": 0, 534 | "mode": "spectrum" 535 | }, 536 | "dataFormat": "tsbuckets", 537 | "datasource": "prometheus", 538 | "gridPos": { 539 | "h": 9, 540 | "w": 24, 541 | "x": 0, 542 | "y": 31 543 | }, 544 | "heatmap": {}, 545 | "hideZeroBuckets": false, 546 | "highlightCards": true, 547 | "id": 7, 548 | "legend": { 549 | "show": false 550 | }, 551 | "maxDataPoints": 25, 552 | "pluginVersion": "7.3.3", 553 | "reverseYBuckets": false, 554 | "targets": [ 555 | { 556 | "expr": "sum(increase(latency_bucket[$__rate_interval])) by (le)", 557 | "format": "heatmap", 558 | "instant": false, 559 | "interval": "", 560 | "legendFormat": "{{le}}", 561 | "refId": "A" 562 | } 563 | ], 564 | "title": "Answer latency", 565 | "tooltip": { 566 | "show": true, 567 | "showHistogram": false 568 | }, 569 | "type": "heatmap", 570 | "xAxis": { 571 | "show": true 572 | }, 573 | "yAxis": { 574 | "decimals": 0, 575 | "format": "ms", 576 | "logBase": 1, 577 | "show": true 578 | }, 579 | "yBucketBound": "auto" 580 | } 581 | ], 582 | "refresh": "15s", 583 | "schemaVersion": 34, 584 | "style": "dark", 585 | "tags": [], 586 | "templating": { 587 | "list": [] 588 | }, 589 | "time": { 590 | "from": "now-1h", 591 | "to": "now" 592 | }, 593 | "timepicker": {}, 594 | "timezone": "", 595 | "title": "hBlock Resolver metrics", 596 | "uid": null, 597 | "version": 1, 598 | "weekStart": "" 599 | } 600 | -------------------------------------------------------------------------------- /examples/traefik-grafana/config/grafana/provisioning/datasources/datasources.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | deleteDatasources: 4 | - name: "prometheus" 5 | orgId: 1 6 | 7 | datasources: 8 | - name: "prometheus" 9 | type: "prometheus" 10 | access: "proxy" 11 | orgId: 1 12 | url: "http://prometheus:9090/prometheus/" 13 | isDefault: true 14 | jsonData: 15 | timeInterval: "15s" 16 | queryTimeout: "15s" 17 | editable: false 18 | -------------------------------------------------------------------------------- /examples/traefik-grafana/config/grafana/provisioning/notifiers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorm/hblock-resolver/b8dbaa10dcdac0efafa861296c1ac617cbad3cc4/examples/traefik-grafana/config/grafana/provisioning/notifiers/.gitkeep -------------------------------------------------------------------------------- /examples/traefik-grafana/config/grafana/provisioning/plugins/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorm/hblock-resolver/b8dbaa10dcdac0efafa861296c1ac617cbad3cc4/examples/traefik-grafana/config/grafana/provisioning/plugins/.gitkeep -------------------------------------------------------------------------------- /examples/traefik-grafana/config/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: "15s" 3 | scrape_timeout: "15s" 4 | 5 | scrape_configs: 6 | - job_name: "hblock-resolver" 7 | metrics_path: "/metrics" 8 | scheme: "https" 9 | tls_config: 10 | insecure_skip_verify: true 11 | static_configs: 12 | - targets: ["hblock-resolver:8453"] 13 | -------------------------------------------------------------------------------- /examples/traefik-grafana/config/traefik/dynamic/ingress.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/traefik-v2-file-provider.json 2 | tls: 3 | options: 4 | default: 5 | minVersion: "VersionTLS13" 6 | hblock-resolver-dot: 7 | alpnProtocols: ["dot"] 8 | http: 9 | routers: 10 | https-redirect: 11 | rule: 'PathPrefix(`/`)' 12 | entryPoints: ["http"] 13 | middlewares: ["https-redirect"] 14 | service: "noop@internal" 15 | hblock-resolver-doh: 16 | rule: 'Host(`{{ env "DOMAIN" }}`) && Path(`/dns-query`)' 17 | entryPoints: ["https"] 18 | middlewares: ["security-headers"] 19 | service: "hblock-resolver-doh" 20 | tls: 21 | certResolver: '{{ env "TRAEFIK_TLS_RESOLVER" }}' 22 | # {{ if env "TRAEFIK_BASIC_AUTH" }} 23 | hblock-resolver-webmgmt: 24 | rule: 'Host(`{{ env "DOMAIN" }}`)' 25 | entryPoints: ["https"] 26 | middlewares: ["security-headers", "basic-auth"] 27 | service: "hblock-resolver-webmgmt" 28 | tls: 29 | certResolver: '{{ env "TRAEFIK_TLS_RESOLVER" }}' 30 | grafana: 31 | rule: 'Host(`{{ env "DOMAIN" }}`) && PathPrefix(`/grafana`)' 32 | entryPoints: ["https"] 33 | middlewares: ["security-headers", "basic-auth"] 34 | service: "grafana" 35 | tls: 36 | certResolver: '{{ env "TRAEFIK_TLS_RESOLVER" }}' 37 | prometheus: 38 | rule: 'Host(`{{ env "DOMAIN" }}`) && PathPrefix(`/prometheus`)' 39 | entryPoints: ["https"] 40 | middlewares: ["security-headers", "basic-auth"] 41 | service: "prometheus" 42 | tls: 43 | certResolver: '{{ env "TRAEFIK_TLS_RESOLVER" }}' 44 | # {{ end }} 45 | middlewares: 46 | https-redirect: 47 | redirectScheme: 48 | scheme: "https" 49 | permanent: true 50 | security-headers: 51 | headers: 52 | stsSeconds: 31536000 53 | stsIncludeSubdomains: true 54 | stsPreload: true 55 | referrerPolicy: "strict-origin" 56 | contentTypeNosniff: true 57 | contentSecurityPolicy: >- 58 | default-src 'self'; 59 | script-src 'self' 'unsafe-inline' 'unsafe-eval'; 60 | style-src 'self' 'unsafe-inline'; 61 | img-src 'self' blob: data: https://grafana.com; 62 | permissionsPolicy: >- 63 | accelerometer=(), 64 | camera=(), 65 | geolocation=(), 66 | gyroscope=(), 67 | magnetometer=(), 68 | microphone=(), 69 | midi=(), 70 | payment=(), 71 | usb=() 72 | customResponseHeaders: 73 | Server: "" 74 | X-Powered-By: "" 75 | # {{ if env "TRAEFIK_BASIC_AUTH" }} 76 | basic-auth: 77 | basicAuth: 78 | users: ['{{ env "TRAEFIK_BASIC_AUTH" }}'] 79 | # {{ end }} 80 | services: 81 | hblock-resolver-doh: 82 | loadBalancer: 83 | serversTransport: "hblock-resolver" 84 | servers: 85 | - url: "https://hblock-resolver:443" 86 | # {{ if env "TRAEFIK_BASIC_AUTH" }} 87 | hblock-resolver-webmgmt: 88 | loadBalancer: 89 | serversTransport: "hblock-resolver" 90 | servers: 91 | - url: "https://hblock-resolver:8453" 92 | grafana: 93 | loadBalancer: 94 | servers: 95 | - url: "http://grafana:3000" 96 | prometheus: 97 | loadBalancer: 98 | servers: 99 | - url: "http://prometheus:9090" 100 | # {{ end }} 101 | serversTransports: 102 | hblock-resolver: 103 | insecureSkipVerify: true 104 | tcp: 105 | routers: 106 | hblock-resolver-dot: 107 | rule: 'HostSNI(`{{ env "DOMAIN" }}`)' 108 | entryPoints: ["dot"] 109 | service: "hblock-resolver-dot" 110 | tls: 111 | certResolver: '{{ env "TRAEFIK_TLS_RESOLVER" }}' 112 | options: "hblock-resolver-dot" 113 | services: 114 | hblock-resolver-dot: 115 | loadBalancer: 116 | serversTransport: "hblock-resolver-dot" 117 | servers: 118 | - address: "hblock-resolver:853" 119 | tls: true 120 | serversTransports: 121 | hblock-resolver-dot: 122 | tls: 123 | insecureSkipVerify: true 124 | -------------------------------------------------------------------------------- /examples/traefik-grafana/config/traefik/traefik.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/traefik-v2.json 2 | global: 3 | checkNewVersion: false 4 | sendAnonymousUsage: false 5 | entryPoints: 6 | http: 7 | address: ":80/tcp" 8 | https: 9 | address: ":443/tcp" 10 | http3: { } 11 | dot: 12 | address: ":853/tcp" 13 | providers: 14 | file: 15 | directory: "/etc/traefik/dynamic/" 16 | certificatesResolvers: 17 | acme-staging-http-01: 18 | acme: 19 | storage: "/etc/traefik/acme/acme.json" 20 | caServer: "https://acme-staging-v02.api.letsencrypt.org/directory" 21 | httpChallenge: { entryPoint: "http" } 22 | acme-production-http-01: 23 | acme: 24 | storage: "/etc/traefik/acme/acme.json" 25 | caServer: "https://acme-v02.api.letsencrypt.org/directory" 26 | httpChallenge: { entryPoint: "http" } 27 | acme-staging-tls-alpn-01: 28 | acme: 29 | storage: "/etc/traefik/acme/acme.json" 30 | caServer: "https://acme-staging-v02.api.letsencrypt.org/directory" 31 | tlsChallenge: { } 32 | acme-production-tls-alpn-01: 33 | acme: 34 | storage: "/etc/traefik/acme/acme.json" 35 | caServer: "https://acme-v02.api.letsencrypt.org/directory" 36 | tlsChallenge: { } 37 | acme-staging-dns-01-cloudflare: 38 | acme: 39 | storage: "/etc/traefik/acme/acme.json" 40 | caServer: "https://acme-staging-v02.api.letsencrypt.org/directory" 41 | dnsChallenge: { provider: "cloudflare" } 42 | acme-production-dns-01-cloudflare: 43 | acme: 44 | storage: "/etc/traefik/acme/acme.json" 45 | caServer: "https://acme-v02.api.letsencrypt.org/directory" 46 | dnsChallenge: { provider: "cloudflare" } 47 | ping: { } 48 | -------------------------------------------------------------------------------- /examples/traefik/.env.sample: -------------------------------------------------------------------------------- 1 | DOMAIN=hblock-resolver.localhost 2 | 3 | # The unhashed password is "password" 4 | TRAEFIK_BASIC_AUTH=admin:$$2y$$12$$Q80Ser2QXlWFnJuqj5nQjOt1gvYDXxpAZuaRUaq8vilEXTAPCOhfW 5 | #TRAEFIK_TLS_RESOLVER=acme-staging-http-01 6 | #TRAEFIK_TLS_RESOLVER=acme-production-http-01 7 | -------------------------------------------------------------------------------- /examples/traefik/compose.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json 2 | name: "hblock-resolver" 3 | 4 | # https://hub.docker.com/_/traefik 5 | x-traefik-image: &traefik-image "docker.io/traefik:3.1" 6 | # https://hub.docker.com/r/hectorm/hblock-resolver 7 | x-hblock-resolver-image: &hblock-resolver-image "docker.io/hectorm/hblock-resolver:latest" 8 | 9 | services: 10 | 11 | traefik: 12 | image: *traefik-image 13 | restart: "on-failure:3" 14 | hostname: "traefik" 15 | networks: 16 | - "hblock-resolver" 17 | ports: 18 | - "80:80/tcp" # HTTP. 19 | - "443:443/tcp" # HTTPS. 20 | - "443:443/udp" # HTTPS (QUIC). 21 | - "853:853/tcp" # DNS over TLS. 22 | volumes: 23 | - "./config/traefik/traefik.yml:/etc/traefik/traefik.yml:ro" 24 | - "./config/traefik/dynamic/:/etc/traefik/dynamic/:ro" 25 | - "traefik-acme:/etc/traefik/acme/" 26 | environment: 27 | TRAEFIK_BASIC_AUTH: "${TRAEFIK_BASIC_AUTH:-}" 28 | TRAEFIK_TLS_RESOLVER: "${TRAEFIK_TLS_RESOLVER:-}" 29 | DOMAIN: "${DOMAIN:?}" 30 | healthcheck: 31 | test: ["CMD", "traefik", "healthcheck"] 32 | start_period: "120s" 33 | start_interval: "5s" 34 | interval: "30s" 35 | timeout: "10s" 36 | retries: 2 37 | 38 | hblock-resolver: 39 | image: *hblock-resolver-image 40 | restart: "on-failure:3" 41 | hostname: "hblock-resolver" 42 | networks: 43 | - "hblock-resolver" 44 | ports: 45 | - "127.0.0.153:53:53/udp" # DNS over UDP. 46 | - "127.0.0.153:53:53/tcp" # DNS over TCP. 47 | volumes: 48 | - "hblock-resolver-data:/var/lib/knot-resolver/" 49 | environment: 50 | KRESD_INSTANCE_NUMBER: "${KRESD_INSTANCE_NUMBER:-4}" 51 | 52 | volumes: 53 | 54 | traefik-acme: 55 | 56 | hblock-resolver-data: 57 | 58 | networks: 59 | 60 | hblock-resolver: 61 | -------------------------------------------------------------------------------- /examples/traefik/config/traefik/dynamic/ingress.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/traefik-v2-file-provider.json 2 | tls: 3 | options: 4 | default: 5 | minVersion: "VersionTLS13" 6 | hblock-resolver-dot: 7 | alpnProtocols: ["dot"] 8 | http: 9 | routers: 10 | https-redirect: 11 | rule: 'PathPrefix(`/`)' 12 | entryPoints: ["http"] 13 | middlewares: ["https-redirect"] 14 | service: "noop@internal" 15 | hblock-resolver-doh: 16 | rule: 'Host(`{{ env "DOMAIN" }}`) && Path(`/dns-query`)' 17 | entryPoints: ["https"] 18 | middlewares: ["security-headers"] 19 | service: "hblock-resolver-doh" 20 | tls: 21 | certResolver: '{{ env "TRAEFIK_TLS_RESOLVER" }}' 22 | # {{ if env "TRAEFIK_BASIC_AUTH" }} 23 | hblock-resolver-webmgmt: 24 | rule: 'Host(`{{ env "DOMAIN" }}`)' 25 | entryPoints: ["https"] 26 | middlewares: ["security-headers", "basic-auth"] 27 | service: "hblock-resolver-webmgmt" 28 | tls: 29 | certResolver: '{{ env "TRAEFIK_TLS_RESOLVER" }}' 30 | # {{ end }} 31 | middlewares: 32 | https-redirect: 33 | redirectScheme: 34 | scheme: "https" 35 | permanent: true 36 | security-headers: 37 | headers: 38 | stsSeconds: 31536000 39 | stsIncludeSubdomains: true 40 | stsPreload: true 41 | referrerPolicy: "strict-origin" 42 | contentTypeNosniff: true 43 | contentSecurityPolicy: >- 44 | default-src 'self'; 45 | script-src 'self' 'unsafe-inline'; 46 | style-src 'self' 'unsafe-inline'; 47 | img-src 'self' blob: data:; 48 | permissionsPolicy: >- 49 | accelerometer=(), 50 | camera=(), 51 | geolocation=(), 52 | gyroscope=(), 53 | magnetometer=(), 54 | microphone=(), 55 | midi=(), 56 | payment=(), 57 | usb=() 58 | customResponseHeaders: 59 | Server: "" 60 | X-Powered-By: "" 61 | # {{ if env "TRAEFIK_BASIC_AUTH" }} 62 | basic-auth: 63 | basicAuth: 64 | users: ['{{ env "TRAEFIK_BASIC_AUTH" }}'] 65 | # {{ end }} 66 | services: 67 | hblock-resolver-doh: 68 | loadBalancer: 69 | serversTransport: "hblock-resolver" 70 | servers: 71 | - url: "https://hblock-resolver:443" 72 | # {{ if env "TRAEFIK_BASIC_AUTH" }} 73 | hblock-resolver-webmgmt: 74 | loadBalancer: 75 | serversTransport: "hblock-resolver" 76 | servers: 77 | - url: "https://hblock-resolver:8453" 78 | # {{ end }} 79 | serversTransports: 80 | hblock-resolver: 81 | insecureSkipVerify: true 82 | tcp: 83 | routers: 84 | hblock-resolver-dot: 85 | rule: 'HostSNI(`{{ env "DOMAIN" }}`)' 86 | entryPoints: ["dot"] 87 | service: "hblock-resolver-dot" 88 | tls: 89 | certResolver: '{{ env "TRAEFIK_TLS_RESOLVER" }}' 90 | options: "hblock-resolver-dot" 91 | services: 92 | hblock-resolver-dot: 93 | loadBalancer: 94 | serversTransport: "hblock-resolver-dot" 95 | servers: 96 | - address: "hblock-resolver:853" 97 | tls: true 98 | serversTransports: 99 | hblock-resolver-dot: 100 | tls: 101 | insecureSkipVerify: true 102 | -------------------------------------------------------------------------------- /examples/traefik/config/traefik/traefik.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/traefik-v2.json 2 | global: 3 | checkNewVersion: false 4 | sendAnonymousUsage: false 5 | entryPoints: 6 | http: 7 | address: ":80/tcp" 8 | https: 9 | address: ":443/tcp" 10 | http3: { } 11 | dot: 12 | address: ":853/tcp" 13 | providers: 14 | file: 15 | directory: "/etc/traefik/dynamic/" 16 | certificatesResolvers: 17 | acme-staging-http-01: 18 | acme: 19 | storage: "/etc/traefik/acme/acme.json" 20 | caServer: "https://acme-staging-v02.api.letsencrypt.org/directory" 21 | httpChallenge: { entryPoint: "http" } 22 | acme-production-http-01: 23 | acme: 24 | storage: "/etc/traefik/acme/acme.json" 25 | caServer: "https://acme-v02.api.letsencrypt.org/directory" 26 | httpChallenge: { entryPoint: "http" } 27 | acme-staging-tls-alpn-01: 28 | acme: 29 | storage: "/etc/traefik/acme/acme.json" 30 | caServer: "https://acme-staging-v02.api.letsencrypt.org/directory" 31 | tlsChallenge: { } 32 | acme-production-tls-alpn-01: 33 | acme: 34 | storage: "/etc/traefik/acme/acme.json" 35 | caServer: "https://acme-v02.api.letsencrypt.org/directory" 36 | tlsChallenge: { } 37 | acme-staging-dns-01-cloudflare: 38 | acme: 39 | storage: "/etc/traefik/acme/acme.json" 40 | caServer: "https://acme-staging-v02.api.letsencrypt.org/directory" 41 | dnsChallenge: { provider: "cloudflare" } 42 | acme-production-dns-01-cloudflare: 43 | acme: 44 | storage: "/etc/traefik/acme/acme.json" 45 | caServer: "https://acme-v02.api.letsencrypt.org/directory" 46 | dnsChallenge: { provider: "cloudflare" } 47 | ping: { } 48 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | export LC_ALL=C 5 | 6 | DOCKER=$(command -v docker 2>/dev/null) 7 | 8 | IMAGE_REGISTRY=docker.io 9 | IMAGE_NAMESPACE=hectorm 10 | IMAGE_PROJECT=hblock-resolver 11 | IMAGE_TAG=latest 12 | IMAGE_NAME=${IMAGE_REGISTRY:?}/${IMAGE_NAMESPACE:?}/${IMAGE_PROJECT:?}:${IMAGE_TAG:?} 13 | CONTAINER_NAME=${IMAGE_PROJECT:?} 14 | 15 | imageExists() { [ -n "$("${DOCKER:?}" images -q "${1:?}")" ]; } 16 | containerExists() { "${DOCKER:?}" ps -af name="${1:?}" --format '{{.Names}}' | grep -Fxq "${1:?}"; } 17 | containerIsRunning() { "${DOCKER:?}" ps -f name="${1:?}" --format '{{.Names}}' | grep -Fxq "${1:?}"; } 18 | 19 | if ! imageExists "${IMAGE_NAME:?}" && ! imageExists "${IMAGE_NAME#docker.io/}"; then 20 | >&2 printf '%s\n' "\"${IMAGE_NAME:?}\" image doesn't exist!" 21 | exit 1 22 | fi 23 | 24 | if containerIsRunning "${CONTAINER_NAME:?}"; then 25 | printf '%s\n' "Stopping \"${CONTAINER_NAME:?}\" container..." 26 | "${DOCKER:?}" stop "${CONTAINER_NAME:?}" >/dev/null 27 | fi 28 | 29 | if containerExists "${CONTAINER_NAME:?}"; then 30 | printf '%s\n' "Removing \"${CONTAINER_NAME:?}\" container..." 31 | "${DOCKER:?}" rm "${CONTAINER_NAME:?}" >/dev/null 32 | fi 33 | 34 | printf '%s\n' "Creating \"${CONTAINER_NAME:?}\" container..." 35 | "${DOCKER:?}" run --detach \ 36 | --name "${CONTAINER_NAME:?}" \ 37 | --hostname "${CONTAINER_NAME:?}" \ 38 | --restart on-failure:3 \ 39 | --log-opt max-size=32m \ 40 | --user "$(shuf -i100000-200000 -n1)" \ 41 | --publish '127.0.0.153:53:53/udp' \ 42 | --publish '127.0.0.153:53:53/tcp' \ 43 | --publish '127.0.0.153:443:443/tcp' \ 44 | --publish '127.0.0.153:853:853/tcp' \ 45 | --publish '127.0.0.153:8453:8453/tcp' \ 46 | --mount type=volume,src="${CONTAINER_NAME:?}-data",dst='/var/lib/knot-resolver/' \ 47 | --mount type=volume,src="${CONTAINER_NAME:?}-cache",dst='/var/cache/knot-resolver/' \ 48 | --env KRESD_INSTANCE_NUMBER=4 \ 49 | "${IMAGE_NAME:?}" "$@" >/dev/null 50 | 51 | printf '%s\n\n' 'Done!' 52 | exec "${DOCKER:?}" logs -f "${CONTAINER_NAME:?}" 53 | -------------------------------------------------------------------------------- /scripts/bin/container-healthcheck: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | umask 0002 5 | 6 | # kresd services must be running 7 | id=0; while [ "${id:?}" -lt "${KRESD_INSTANCE_NUMBER:?}" ]; do 8 | if ! is-sv-status "kresd${id:?}" run; then 9 | >&2 printf '%s\n' "kresd${id:?} service is not running" 10 | exit 1 11 | fi 12 | id=$((id + 1)) 13 | done 14 | 15 | # kres-cache-gc service must be running 16 | if ! is-sv-status kres-cache-gc run; then 17 | >&2 printf '%s\n' 'kres-cache-gc service is not running' 18 | exit 1 19 | fi 20 | 21 | # kres-cert-updater service must be running 22 | if ! is-sv-status kres-cert-updater run; then 23 | >&2 printf '%s\n' 'kres-cert-updater service is not running' 24 | exit 1 25 | fi 26 | 27 | # hblock service must be running 28 | if ! is-sv-status hblock run; then 29 | >&2 printf '%s\n' 'hblock service is not running' 30 | exit 1 31 | fi 32 | 33 | # DNS server must resolve localhost A record 34 | if [ "$(kdig @127.0.0.1 -p 53 +short +timeout=1 +retry=0 localhost A)" != '127.0.0.1' ]; then 35 | >&2 printf '%s\n' 'DNS server returned an unexpected result' 36 | exit 1 37 | fi 38 | 39 | # DNS (over TLS) server must resolve localhost A record 40 | if [ "$(kdig @127.0.0.1 -p 853 +tls +short +timeout=1 +retry=0 localhost A)" != '127.0.0.1' ]; then 41 | >&2 printf '%s\n' 'DNS (over TLS) server returned an unexpected result' 42 | exit 1 43 | fi 44 | 45 | # DNS (over HTTPS) server must resolve localhost A record 46 | if [ "$(kdig @127.0.0.1 -p 443 +https +short +timeout=1 +retry=0 localhost A)" != '127.0.0.1' ]; then 47 | >&2 printf '%s\n' 'DNS (over HTTPS) server returned an unexpected result' 48 | exit 1 49 | fi 50 | 51 | # HTTP server must return "OK" 52 | if [ "$(curl -kfs https://localhost:8453/health)" != 'OK' ]; then 53 | >&2 printf '%s\n' 'HTTP server returned an unexpected result' 54 | exit 1 55 | fi 56 | -------------------------------------------------------------------------------- /scripts/bin/container-init: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | umask 0002 5 | 6 | # Remove leftover files 7 | for d in "${SVDIR:?}"/*/supervise/; do 8 | [ -d "${d:?}" ] && rm -rf "${d:?}" 9 | done 10 | for s in "${KRESD_CACHE_DIR:?}"/control/*; do 11 | [ -S "${s:?}" ] && rm -f "${s:?}" 12 | done 13 | 14 | # Generate certificate if it does not exist 15 | kres-cert-updater 16 | 17 | # Generate blocklist zone if it does not exist 18 | if [ ! -e "${KRESD_DATA_DIR:?}"/hblock/blocklist.rpz ]; then 19 | hblock 20 | fi 21 | 22 | # Create a service for each kresd instance 23 | if [ "${KRESD_INSTANCE_NUMBER:?}" -gt 1 ]; then 24 | id=1; while [ "${id:?}" -lt "${KRESD_INSTANCE_NUMBER:?}" ]; do 25 | if [ ! -d "${SVDIR:?}"/kresd"${id:?}"/ ]; then 26 | cp -a "${SVDIR:?}"/kresd0/ "${SVDIR:?}"/kresd"${id:?}"/ 27 | fi 28 | id=$((id + 1)) 29 | done 30 | fi 31 | 32 | # Start all services 33 | exec runsvdir -P "${SVDIR:?}" 34 | -------------------------------------------------------------------------------- /scripts/bin/is-sv-status: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | umask 0002 5 | 6 | SERVICE=${1:?} 7 | STATUS=${2:?} 8 | STATUS_FILE=${SVDIR:?}/${SERVICE:?}/supervise/stat 9 | 10 | [ -f "${STATUS_FILE:?}" ] && [ "$(cat "${STATUS_FILE:?}")" = "${STATUS:?}" ] 11 | -------------------------------------------------------------------------------- /scripts/bin/kres-cert-updater: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | umask 0007 5 | 6 | # Do nothing if the certificate is not managed 7 | if [ "${KRESD_CERT_MANAGED:?}" != 'true' ]; then 8 | exit 0 9 | fi 10 | 11 | { 12 | set -a 13 | 14 | CERTS_DIR="${KRESD_DATA_DIR:?}"/ssl/ 15 | 16 | CA_KEY="${CERTS_DIR:?}"/ca.key 17 | CA_CSR="${CERTS_DIR:?}"/ca.csr 18 | CA_SRL="${CERTS_DIR:?}"/ca.srl 19 | CA_CRT="${CERTS_DIR:?}"/ca.crt 20 | CA_CRT_CNF="${CERTS_DIR:?}"/ca.cnf 21 | CA_CRT_SUBJ='/CN=hBlock Resolver CA' 22 | CA_CRT_VALIDITY_DAYS='7300' 23 | CA_CRT_RENOVATION_DAYS='365' 24 | CA_RENEW_PREHOOK='' 25 | CA_RENEW_POSTHOOK='' 26 | 27 | SERVER_KEY="${CERTS_DIR:?}"/server.key 28 | SERVER_CSR="${CERTS_DIR:?}"/server.csr 29 | SERVER_CRT="${CERTS_DIR:?}"/server.crt 30 | SERVER_CRT_CNF="${CERTS_DIR:?}"/server.cnf 31 | SERVER_CRT_SUBJ="/CN=$(hostname -f)" 32 | SERVER_CRT_SAN=$(printf '%s\n' \ 33 | "DNS:$(hostname -f)" \ 34 | 'DNS:localhost' \ 35 | 'IP:127.0.0.1' \ 36 | 'IP:::1' \ 37 | | paste -sd, -) 38 | SERVER_CRT_VALIDITY_DAYS='90' 39 | SERVER_CRT_RENOVATION_DAYS='30' 40 | SERVER_RENEW_PREHOOK='' 41 | SERVER_RENEW_POSTHOOK='' 42 | 43 | set +a 44 | } 45 | 46 | if [ ! -e "${CERTS_DIR:?}" ]; then 47 | mkdir "${CERTS_DIR:?}" 48 | fi 49 | 50 | # Generate CA private key if it does not exist 51 | if [ ! -e "${CA_KEY:?}" ] \ 52 | || ! openssl ecparam -check -in "${CA_KEY:?}" -noout >/dev/null 2>&1 53 | then 54 | printf '%s\n' 'Generating CA private key...' 55 | openssl ecparam -genkey -name prime256v1 > "${CA_KEY:?}" 56 | fi 57 | 58 | # Generate CA certificate if it does not exist or will expire soon 59 | if [ ! -e "${CA_CRT:?}" ] \ 60 | || [ "$(openssl x509 -pubkey -in "${CA_CRT:?}" -noout 2>/dev/null)" != "$(openssl pkey -pubout -in "${CA_KEY:?}" -outform PEM 2>/dev/null)" ] \ 61 | || ! openssl x509 -checkend "$((60*60*24*CA_CRT_RENOVATION_DAYS))" -in "${CA_CRT:?}" -noout >/dev/null 2>&1 62 | then 63 | if [ -n "${CA_RENEW_PREHOOK?}" ]; then 64 | sh -euc "${CA_RENEW_PREHOOK:?}" 65 | fi 66 | 67 | printf '%s\n' 'Generating CA certificate...' 68 | openssl req -new \ 69 | -key "${CA_KEY:?}" \ 70 | -out "${CA_CSR:?}" \ 71 | -subj "${CA_CRT_SUBJ:?}" 72 | cat > "${CA_CRT_CNF:?}" <<-EOF 73 | [ x509_exts ] 74 | subjectKeyIdentifier = hash 75 | authorityKeyIdentifier = keyid:always,issuer:always 76 | basicConstraints = critical,CA:TRUE,pathlen:0 77 | keyUsage = critical,keyCertSign,cRLSign 78 | EOF 79 | openssl x509 -req \ 80 | -in "${CA_CSR:?}" \ 81 | -out "${CA_CRT:?}" \ 82 | -signkey "${CA_KEY:?}" \ 83 | -days "${CA_CRT_VALIDITY_DAYS:?}" \ 84 | -sha256 \ 85 | -extfile "${CA_CRT_CNF:?}" \ 86 | -extensions x509_exts 87 | openssl x509 -in "${CA_CRT:?}" -fingerprint -noout 88 | 89 | if [ -n "${CA_RENEW_POSTHOOK?}" ]; then 90 | sh -euc "${CA_RENEW_POSTHOOK:?}" 91 | fi 92 | fi 93 | 94 | # Generate server private key if it does not exist 95 | if [ ! -e "${SERVER_KEY:?}" ] \ 96 | || ! openssl ecparam -check -in "${SERVER_KEY:?}" -noout >/dev/null 2>&1 97 | then 98 | printf '%s\n' 'Generating server private key...' 99 | openssl ecparam -genkey -name prime256v1 > "${SERVER_KEY:?}" 100 | fi 101 | 102 | # Generate server certificate if it does not exist or will expire soon 103 | if [ ! -e "${SERVER_CRT:?}" ] \ 104 | || [ "$(openssl x509 -pubkey -in "${SERVER_CRT:?}" -noout 2>/dev/null)" != "$(openssl pkey -pubout -in "${SERVER_KEY:?}" -outform PEM 2>/dev/null)" ] \ 105 | || ! openssl verify -CAfile "${CA_CRT:?}" "${SERVER_CRT:?}" >/dev/null 2>&1 \ 106 | || ! openssl x509 -checkend "$((60*60*24*SERVER_CRT_RENOVATION_DAYS))" -in "${SERVER_CRT:?}" -noout >/dev/null 2>&1 107 | then 108 | if [ -n "${SERVER_RENEW_PREHOOK?}" ]; then 109 | sh -euc "${SERVER_RENEW_PREHOOK:?}" 110 | fi 111 | 112 | printf '%s\n' 'Generating server certificate...' 113 | openssl req -new \ 114 | -key "${SERVER_KEY:?}" \ 115 | -out "${SERVER_CSR:?}" \ 116 | -subj "${SERVER_CRT_SUBJ:?}" 117 | cat > "${SERVER_CRT_CNF:?}" <<-EOF 118 | [ x509_exts ] 119 | subjectAltName = ${SERVER_CRT_SAN:?} 120 | basicConstraints = critical,CA:FALSE 121 | keyUsage = critical,digitalSignature 122 | extendedKeyUsage = critical,serverAuth 123 | EOF 124 | openssl x509 -req \ 125 | -in "${SERVER_CSR:?}" \ 126 | -out "${SERVER_CRT:?}" \ 127 | -CA "${CA_CRT:?}" \ 128 | -CAkey "${CA_KEY:?}" \ 129 | -CAserial "${CA_SRL:?}" -CAcreateserial \ 130 | -days "${SERVER_CRT_VALIDITY_DAYS:?}" \ 131 | -sha256 \ 132 | -extfile "${SERVER_CRT_CNF:?}" \ 133 | -extensions x509_exts 134 | cat "${CA_CRT:?}" >> "${SERVER_CRT:?}" 135 | openssl x509 -in "${SERVER_CRT:?}" -fingerprint -noout 136 | 137 | if [ -n "${SERVER_RENEW_POSTHOOK?}" ]; then 138 | sh -euc "${SERVER_RENEW_POSTHOOK:?}" 139 | fi 140 | fi 141 | -------------------------------------------------------------------------------- /scripts/bin/kres-console: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | umask 0002 5 | 6 | id=0; while [ "${id:?}" -lt "${KRESD_INSTANCE_NUMBER:?}" ]; do 7 | if is-sv-status "kresd${id:?}" run; then 8 | KRESD_PID=$(cat "${SVDIR:?}"/"kresd${id:?}"/supervise/pid) 9 | KRESD_SOCKET=${KRESD_CACHE_DIR:?}/control/${KRESD_PID:?} 10 | if [ -t 0 ] || [ -t 1 ]; then 11 | sleep 0.1 && exec rlfe nc -U "${KRESD_SOCKET:?}" 12 | else 13 | exec nc -U "${KRESD_SOCKET:?}" 14 | fi 15 | fi 16 | id=$((id + 1)) 17 | done 18 | 19 | >&2 printf '%s\n' 'kresd is not running' 20 | exit 1 21 | -------------------------------------------------------------------------------- /scripts/service/hblock/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | cd "${KRESD_CACHE_DIR:?}" 6 | 7 | exec 2>&1 8 | exec snooze -v -H '5' -M '0' -R '3600' -- hblock 9 | -------------------------------------------------------------------------------- /scripts/service/kres-cache-gc/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | cd "${KRESD_CACHE_DIR:?}" 6 | 7 | # Wait until "data.mdb" exists before starting the garbage collector 8 | until [ -e "${PWD:?}"/data.mdb ]; do sleep 1; done 9 | 10 | exec 2>&1 11 | exec kres-cache-gc -c "${PWD:?}" -d 1000 12 | -------------------------------------------------------------------------------- /scripts/service/kres-cert-updater/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | cd "${KRESD_CACHE_DIR:?}" 6 | 7 | exec 2>&1 8 | exec snooze -v -H '4' -M '30' -R '0' -- kres-cert-updater 9 | -------------------------------------------------------------------------------- /scripts/service/kresd0/finish: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | # Remove unused sockets 6 | for s in "${KRESD_CACHE_DIR:?}"/control/*; do 7 | [ -S "${s:?}" ] && { nc -zU "${s:?}" 2>/dev/null || rm -f "${s:?}"; } 8 | done 9 | -------------------------------------------------------------------------------- /scripts/service/kresd0/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | cd "${KRESD_CACHE_DIR:?}" 6 | 7 | exec 2>&1 8 | exec kresd --noninteractive --config="${KRESD_CONF_DIR:?}"/kresd.conf "${PWD:?}" 9 | --------------------------------------------------------------------------------