## File: README.md

[](https://github.com/0xERR0R/blocky/actions/workflows/makefile.yml)
[](https://github.com/0xERR0R/blocky/actions/workflows/release.yml)
[](https://github.com/0xERR0R/blocky/releases)
[](https://github.com/0xERR0R/blocky/releases)
[](#)
[](https://hub.docker.com/r/spx01/blocky)
[](https://hub.docker.com/r/spx01/blocky)
[](https://codecov.io/gh/0xERR0R/blocky)
[](#)
[](https://goreportcard.com/report/github.com/0xERR0R/blocky)
[](https://ko-fi.com/0xerr0r)
[](https://liberapay.com/spx01)



# Blocky

Blocky is a DNS proxy and ad-blocker for the local network written in Go with following features:

## Features

- **Blocking** - Blocking of DNS queries with external lists (Ad-block, malware) and allowlisting

  - Definition of allow/denylists per client group (Kids, Smart home devices, etc.)
  - Periodical reload of external allow/denylists
  - Regex support
  - Blocking of request domain, response CNAME (deep CNAME inspection) and response IP addresses (against IP lists)

- **Advanced DNS configuration** - not just an ad-blocker

  - Custom DNS resolution for certain domain names
  - Conditional forwarding to external DNS server
  - Upstream resolvers can be defined per client group

- **Performance** - Improves speed and performance in your network

  - Customizable caching of DNS answers for queries -> improves DNS resolution speed and reduces amount of external DNS
    queries
  - Prefetching and caching of often used queries
  - Using multiple external resolver simultaneously
  - Low memory footprint

- **Various Protocols** - Supports modern DNS protocols

  - DNS over UDP and TCP
  - DNS over HTTPS (aka DoH)
  - DNS over TLS (aka DoT)
  - DNS over QUIC (aka DoQ, RFC 9250)
  - DNS over HTTPS/3 (aka DoH3, RFC 9114)

- **Security and Privacy** - Secure communication

  - Supports modern DNS extensions: DNSSEC, eDNS, ...
  - DNSSEC validation of upstream resolvers
  - Free configurable blocking lists - no hidden filtering etc.
  - Provides DoH Endpoint
  - Uses random upstream resolvers from the configuration - increases your privacy through the distribution of your DNS
    traffic over multiple provider
  - Blocky does **NOT** collect any user data, telemetry, statistics etc.

- **Integration** - various integration

  - [Prometheus](https://prometheus.io/) metrics
  - Prepared [Grafana](https://grafana.com/) dashboards (Prometheus and database)
  - Logging of DNS queries per day / per client in CSV format or MySQL/MariaDB/PostgreSQL/Timescale database - easy to
    analyze
  - Various REST API endpoints
  - CLI tool

- **Simple configuration** - single or multiple configuration files in YAML format

  - Simple to maintain
  - Simple to backup

- **Simple installation/configuration** - blocky was designed for simple installation

  - Stateless (no database, no temporary files)
  - Docker image with Multi-arch support
  - Single binary
  - Supports x86-64, ARM, and MIPS architectures -> runs fine on Raspberry PI and OpenWrt routers
  - Community supported Helm chart for k8s deployment

## ❤️ Support Blocky

Blocky is **free, open source, and built entirely in my spare time** — with no telemetry, no data
collection, and no hidden filtering. It keeps the DNS for thousands of home networks, homelabs, and
businesses clean and private, and it always will.

Maintaining a project this size — fixing bugs, reviewing pull requests, shipping new features, and
keeping up with security — takes a lot of time. **Thousands of people run Blocky; only a handful
support it.** If Blocky is useful to you, please consider chipping in. Your support directly funds
ongoing development and helps keep Blocky independent and ad-free, forever.

Even a small recurring contribution makes a real difference and is hugely appreciated. 🙏

[](https://github.com/sponsors/0xERR0R)
[](https://thanks.dev/u/gh/0xERR0R)
[](https://liberapay.com/spx01)
[](https://ko-fi.com/0xerr0r)
[](https://paypal.me/spx01)

### 🥇 Gold sponsors

[](https://github.com/GeoffreyvanOostveen)

### Sponsors

[](https://github.com/Qapf)[](https://github.com/V-Mann-Nick)[](https://github.com/bioxz)

Thank you to everyone who supports Blocky! ❤️

## Quick start

You can jump to [Installation](https://0xerr0r.github.io/blocky/latest/installation/) chapter in the documentation.

## Full documentation

You can find full documentation and configuration examples
at: [https://0xERR0R.github.io/blocky/](https://0xERR0R.github.io/blocky/)

## Contribution

Issues, feature suggestions and pull requests are welcome!

[](https://ko-fi.com/G2G25XZQG)

---

## File: docs/includes/abbreviations.md

*[DNS]: Domain Name System
*[k8s]: Kubernetes
*[UDP]: User Datagram Protocol
*[TCP]: Transmission Control Protocol
*[HTTP]: Hypertext Transfer Protocol
*[HTTPS]: Hypertext Transfer Protocol Secure
*[DoH]: DNS-over-HTTPS
*[DoT]: DNS-over-TLS
*[DNSSEC]: Domain Name System Security Extensions
*[eDNS]: Extended DNS
*[REST]: Representational State Transfer
*[API]: Application Programming Interface
*[CLI]: Command Line Interface
*[YAML]: YAML Ain't Markup Language
*[Helm]: package manager for Kubernetes
*[CNAME]: Canonical Name
*[CIDR]: Classless Inter-Domain Routing
*[NXDOMAIN]: Non-Existence Domain
*[TTL]: Time-To-Live
*[rDNS]: Reverse DNS
*[SSL]: Secure Sockets Layer
*[CSV]: Comma-separated values
*[SAMBA]: Server Message Block Protocol (Windows Network File System)
*[DHCP]: Dynamic Host Configuration Protocol
*[duration format]: Example: "300ms", "1.5h" or "2h45m". Valid time units are "ns", "us", "ms", "s", "m", "h".
*[regex]: Regular expression

---

## File: docs/configuration.md

# Configuration

This chapter describes all configuration options in `config.yaml`. You can download a reference file with all
configuration properties as [YAML](config.yml). A matching [JSON schema](config.schema.json) is also available for
editor autocomplete and validation (see [Editor support and validation](#editor-support-and-validation)).

??? example "reference configuration file"

    ```yaml
    --8<-- "config.yml"
    ```

## Editor support and validation

Blocky ships a [JSON schema](https://raw.githubusercontent.com/0xERR0R/blocky/main/docs/config.schema.json)
generated from its configuration types. Editors with the
[YAML Language Server](https://github.com/redhat-developer/yaml-language-server)
(for example VS Code with the *YAML* extension) use it for autocomplete,
inline documentation and validation. Add this modeline to the top of your
`config.yml`:

```yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/0xERR0R/blocky/main/docs/config.schema.json
```

The same schema is used by `blocky validate`, which reports unknown keys and
invalid values with their field path. Schema validation is a structural
first pass; blocky still performs its full semantic checks when loading the
configuration.

## Multiple configuration files

Instead of a single file, `--config` can point to a **folder**. Blocky then loads every `*.yml` and `*.yaml`
file in that folder (subfolders included) and merges them into one configuration:

- Files are applied in **lexical (alphabetical) path order** — use number prefixes like `00_base.yml`,
  `10_local.yml` to make the order explicit. Blocky logs the merge order at startup.
- **Mappings merge recursively**: keys from later files are added; keys present on both sides merge field by
  field.
- **Everything else is replaced**: when several files set the same scalar or list, the last file wins,
  wholesale. Lists are never concatenated.
- Duplicate keys *within one file* are still an error, and each file must be valid YAML on its own
  (YAML anchors and aliases work within a file, but not across files). A file may contain multiple
  `---`-separated documents; they merge in document order, like separate files.

!!! example

    `config/00_base.yml` — checked into your repo:

    ```yaml
    upstreams:
      groups:
        default:
          - 9.9.9.9
      strategy: parallel_best
    ```

    `config/10_local.yml` — host-specific overlay:

    ```yaml
    upstreams:
      groups:
        192.168.0.0/16:
          - 1.1.1.1
    ```

    Effective configuration:

    ```yaml
    upstreams:
      groups:
        default:
          - 9.9.9.9
        192.168.0.0/16:
          - 1.1.1.1
      strategy: parallel_best
    ```

## Basic configuration

| Parameter          | Type                | Mandatory | Default value | Description                                                                                                |
| ------------------ | ------------------- | --------- | ------------- | ---------------------------------------------------------------------------------------------------------- |
| certFile           | path                | no        |               | Path to cert and key file for SSL encryption (DoH and DoT); if empty, self-signed certificate is generated |
| keyFile            | path                | no        |               | Path to cert and key file for SSL encryption (DoH and DoT); if empty, self-signed certificate is generated |
| minTlsServeVersion | string              | no        | 1.2           | Minimum TLS version that the DoT and DoH server use to serve those encrypted DNS requests                  |
| connectIPVersion   | enum (dual, v4, v6) | no        | dual          | IP version to use for outgoing connections (dual, v4, v6)                                                  |

!!! example

    ```yaml
    minTlsServeVersion: 1.1
    connectIPVersion: v4
    ```

## Ports & addresses configuration

All values in this section are optional.

| Parameter     | Type                  | Default value | Description                                                                                                                                       |
|---------------|-----------------------|---------------|---------------------------------------------------------------------------------------------------------------------------------------------------|
| ports.dns     | One or more [IP]:Port | 53            | Listen address for DNS (TCP and UDP). Example: `53`, `:53`, `192.168.0.1:53`, `[53, "[::1]:53"]`                                                  |
| ports.tls     | One or more [IP]:Port |               | Listen address for DoT (DNS-over-TLS). Example: `83`, `:853`, `192.168.0.1:853`, `[853, "[::1]:853"]`                                             |
| ports.http    | One or more [IP]:Port |               | Listen address for HTTP used for prometheus metrics, pprof, REST API, DoH... Example: `4000`, `:4000`, `192.168.0.1:4000`, `[4000, "[::1]:4000"]` |
| ports.https   | One or more [IP]:Port |               | Listen address for HTTPS used for prometheus metrics, pprof, REST API, DoH... Example: `443`, `:443`, `192.168.0.1:443`, `[443, "[::1]:443"]`     |
| ports.dohPath | string                | /dns-query    | URL path for DoH queries.                                                                                                                         |
| ports.freeBind | bool                 | false         | Allow binding the DNS/DoT listeners to addresses not yet assigned to an interface (Linux only, via `IP_FREEBIND`; e.g. Tailscale/WireGuard/VRRP). No effect on wildcard binds; ignored with a warning on non-Linux. |
| ports.proxyProtocol | list | _empty_ | TCP listener families (any of `dns`, `http`, `https`, `tls`) that must require a HAProxy PROXY protocol header. Enable only when the listener is reachable only through a trusted proxy. |

!!! example

    ```yaml
    ports:
      dns: 53
      tls: [853, "[::1]:853"]
      http:
        - 80
        - 4000
      https: 443
      dohPath: /my-custom-dns-query
      proxyProtocol:
        - https
        - tls
    ```

## Logging configuration

All logging options are optional.

| Parameter     | Type                                   | Default value | Description                                                                                                                                       |
| ------------- | -------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| log.level     | enum (trace, debug, info, warn, error) | info          | Log level                                                                                                                                         |
| log.format    | enum (text, json)                      | text          | Log format (text or json).                                                                                                                        |
| log.timestamp | bool                                   | true          | Log timestamps (true or false).                                                                                                                   |
| log.privacy   | bool                                   | false         | Obfuscate log output (replace all alphanumeric characters with \*) for user sensitive data like request domains or responses to increase privacy. |

!!! example

    ```yaml
    log:
      level: debug
      format: json
      timestamp: false
      privacy: true
    ```

## Init Strategy

A couple of features use an "init/loading strategy" which configures behavior at Blocky startup.
This applies to all of them. The default strategy is blocking.

| strategy    | Description                                                                                                                                                     |
| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| blocking    | Initialization happens before DNS resolution starts. Any errors are logged, but Blocky continues running if possible.                                           |
| failOnError | Like blocking but Blocky will exit with an error if initialization fails.                                                                                       |
| fast        | Blocky starts serving DNS immediately and initialization happens in the background. The feature requiring initialization will enable later on (if it succeeds). |

## Upstreams configuration

| Parameter               | Type                                 | Mandatory | Default value | Description                                    |
| ----------------------- | ------------------------------------ | --------- | ------------- | ---------------------------------------------- |
| upstreams.groups        | map of name to upstream              | yes       |               | Upstream DNS servers to use, in groups.        |
| upstreams.init.strategy | enum (blocking, failOnError, fast)   | no        | blocking      | See [Init Strategy](#init-strategy) and below. |
| upstreams.strategy      | enum (parallel_best, random, strict) | no        | parallel_best | Upstream server usage strategy.                |
| upstreams.timeout              | duration                             | no        | 2s            | Upstream connection timeout.                          |
| upstreams.userAgent            | string                               | no        |               | HTTP User Agent when connecting to upstreams.         |
| upstreams.quic.maxIdleTimeout  | duration                             | no        | 30s           | QUIC maximum idle timeout before closing connection.  |
| upstreams.quic.keepAlivePeriod | duration                             | no        | 15s           | QUIC keep-alive interval to maintain connection.      |

For `init.strategy`, the "init" is testing the given resolvers for each group. The potentially fatal error, depending on the strategy, is if a group has no functional resolvers.

### Upstream Groups

To resolve a DNS query, blocky needs external public or private DNS resolvers. Blocky supports DNS resolvers with
following network protocols (net part of the resolver URL):

- tcp+udp (UDP and TCP, dependent on query type)
- https (aka DoH)
- tcp-tls (aka DoT)
- quic (aka DoQ, DNS-over-QUIC per RFC 9250)

!!! hint

    You can (and should!) configure multiple DNS resolvers.
    Per default blocky uses the `parallel_best` upstream strategy where blocky picks 2 random resolvers from the list for each query and
    returns the answer from the fastest one.

Each resolver must be defined as a string in following format: `[net:]host:[port][/path][#commonName]`.

| Parameter  | Type                             | Mandatory | Default value                                     |
| ---------- | -------------------------------- | --------- | ------------------------------------------------- |
| net        | enum (tcp+udp, tcp-tls, https or quic) | no        | tcp+udp                                                          |
| host       | IP or hostname                         | yes       |                                                                  |
| port       | int (1 - 65535)                        | no        | 53 for udp/tcp, 853 for tcp-tls and quic, 443 for https         |
| commonName | string                           | no        | the host value                                    |

The `commonName` parameter overrides the expected certificate common name value used for verification.

#### DNS Stamp Format

As an alternative to the traditional format, Blocky supports **DNS Stamps** (`sdns://...`), which encode all connection parameters in a compact, shareable format.

DNS Stamps are standardized URIs (following IETF draft-denis-dns-stamps) that include protocol, address, hostname, path, and optional certificate hashes in a single string.

**Format:** `sdns://base64url(payload)`

**Supported Protocols:**

- Plain DNS (`sdns://AA...`)
- DNS-over-HTTPS (`sdns://Ag...`)
- DNS-over-TLS (`sdns://Aw...`)
- DNS-over-QUIC (`sdns://BA...`)

**Benefits:**

- **Compact**: Single string encodes all parameters
- **Portable**: Easy to share and copy
- **Secure**: Includes certificate hashes for automatic certificate pinning
- **Standardized**: Following IETF specification

**Examples:**

```yaml
upstreams:
  groups:
    default:
      # Traditional format
      - 8.8.8.8
      - https://dns.google/dns-query
      - tcp-tls:1.1.1.1:853
      # DNS-over-QUIC (also accepts quic://dns.adguard.com for AdGuard compatibility)
      - quic:dns.adguard.com

      # DNS Stamp format (equivalent servers)
      - sdns://AAcAAAAAAAAABzguOC44Ljg  # Google DNS
      - sdns://AgcAAAAAAAAADTE4NS45NS4yMTguNDKg9_WvKIAeh31986K-KP4UnzJ0p-0p8Tb9UDzjmMuoCw2g9XV9eG8XP2q1MZfsnCKQisBtKuTqoqW29Y678AOXod-gs14FlQz72CWfk3W6EBhwuMPEwOxaUpdX5jFn6d4mqeig7PBiC_ww1ETe9Xetxlw3UFpzui0v2eb_QcPtOtDWWg-gouMnQ2N22OdkBavp_siMtB3i1NA1DU6CkqUxGMS431ugkHjSGlbA4IkdkGIhtpkjMb3RcXLrDzPdoH1cZcbhTDmg1q1wTTQXThIIyVKqq5g16xwGaw3No7Ta1A9FE-jhglqgsl8vNpZ_c5TwmTeIWzM_qjVtcZ_qzzjM6fA1UADz4XSg5kS6aWPjNf52XLmXaxKxDrVClLQkd3ZMyzo6zKOssvygKq4_t78F5MgcQZTcpEUR1PmvMEeG7BrnIYQJz2Kgg1Wg1h2W6_s_oZCW4sAC42A79_2q77InBNqGaAYrvobtS5MgnUgyu0yNdyB7Jd4XWSaC78PKRfV53GZKDtXySPXJM5gcZG5zLmRpZ2l0YWxlLWdlc2VsbHNjaGFmdC5jaAovZG5zLXF1ZXJ5  # Digitale Gesellschaft DoH
```

**Certificate Pinning:**

DNS stamps can include SHA256 certificate hashes for enhanced security. When present, Blocky automatically validates the server's certificate against these hashes, preventing MITM attacks even if a Certificate Authority is compromised.

This is transparent to the user - if a DNS stamp includes certificate hashes, pinning is automatically enabled.

**Creating DNS Stamps:**

Use the [DNS Stamp Calculator](https://dnscrypt.info/stamps/) to create stamps for your DNS servers.

!!! note
    - DNSCrypt protocol is not yet supported
    - Traditional and DNS stamp formats can be mixed in the same configuration
    - DNS stamps with certificate hashes provide additional security through automatic certificate pinning

!!! note
    Blocky needs at least the configuration of the **default** group with at least one upstream DNS server. This group will be used as a fallback, if no client
    specific resolver configuration is available.

    See [List of public DNS servers](additional_information.md#list-of-public-dns-servers) if you need some ideas, which public free DNS server you could use.

You can specify multiple upstream groups (additional to the `default` group) to use different upstream servers for different clients, based on client name (see [Client name lookup](#client-name-lookup)), client IP address or client subnet (as CIDR).

!!! tip

    You can use `*` as wildcard for the sequence of any character or `[0-9]` as number range

!!! example

    ```yaml
    upstreams:
      groups:
        default:
          - 5.9.164.112
          - 1.1.1.1
          - tcp-tls:fdns1.dismail.de:853
          - https://dns.digitale-gesellschaft.ch/dns-query
        laptop*:
          - 123.123.123.123
        10.43.8.67/28:
          - 1.1.1.1
          - 9.9.9.9
    ```

The above example results in:

- `123.123.123.123` as the only upstream DNS resolver for clients with a name starting with "laptop"
- `1.1.1.1` and `9.9.9.9` for all clients in the subnet `10.43.8.67/28`
- 4 resolvers (default) for all others clients.

The logic determining what group a client belongs to follows a strict order: IP, client name, CIDR

If a client matches multiple client name or CIDR groups, a warning is logged and the first found group is used.

### Upstream connection timeout

Blocky will wait 2 seconds (default value) for the response from the external upstream DNS server. You can change this
value by setting the `timeout` configuration parameter (in **duration format**).

!!! example

    ```yaml
    upstreams:
      timeout: 5s
      groups:
        default:
          - 46.182.19.48
          - 80.241.218.68
    ```

### Upstream strategy

Blocky supports different upstream strategies (default `parallel_best`) that determine how and to which upstream DNS servers requests are forwarded.

Currently available strategies:

- `parallel_best`: blocky picks 2 random (weighted) resolvers from the upstream group for each query and returns the answer from the fastest one.
  If an upstream failed to answer within the last hour, it is less likely to be chosen for the race.
  This improves your network speed and increases your privacy - your DNS traffic will be distributed over multiple providers.
  (When using 10 upstream servers, each upstream will get on average 20% of the DNS requests)
- `random`: blocky picks one random (weighted) resolver from the upstream group for each query and if successful, returns its response.
  If the selected resolver fails to respond, a second one is picked to which the query is sent.
  The weighting is identical to the `parallel_best` strategy.
  Although the `random` strategy might be slower than the `parallel_best` strategy, it offers more privacy since each request is sent to a single upstream.
- `strict`: blocky forwards the request in a strict order. If the first upstream does not respond, the second is asked, and so on.

!!! example

    ```yaml
    upstreams:
      strategy: strict
      groups:
        default:
          - 1.2.3.4
          - 9.8.7.6
    ```

## Bootstrap DNS configuration

These DNS servers are used to resolve upstream DoH and DoT servers that are specified as host names, and list domains.
It is useful if no system DNS resolver is configured, and/or to encrypt the bootstrap queries.

| Parameter  | Type                 | Mandatory                   | Default value | Description                                                                  |
| ---------- | -------------------- | --------------------------- | ------------- | ---------------------------------------------------------------------------- |
| upstream   | Upstream (see above) | no                          |               |                                                                              |
| ips        | List of IPs          | yes, if upstream is DoT/DoH |               | Only valid if upstream is DoH or DoT                                         |
| resolvFile | string (file path)   | no                          |               | Read nameservers from a `resolv.conf`(5) file and use them as bootstrap DNS. Cannot be combined with `upstream`/`ips` in the same entry |

When using an upstream specified by IP, and not by hostname, you can write only the upstream and skip `ips`.

If `bootstrapDns` is not set, blocky uses the operating system's resolver (`/etc/resolv.conf` on Linux). On systems where the
DHCP-provided resolvers live elsewhere (for example OpenWrt writes them to `/tmp/resolv.conf.auto`), use a `resolvFile` entry to
point blocky at the right file. The file is parsed once at startup; its `nameserver` entries are used as plain-DNS bootstrap servers.

!!! note

    Works only on Linux/\*nix OS due to golang limitations under Windows.

!!! example

    ```yaml
        bootstrapDns:
          - upstream: tcp-tls:dns.example.com
            ips:
            - 123.123.123.123
          - upstream: https://234.234.234.234/dns-query
          # read the DHCP-provided resolvers from a file (e.g. OpenWrt)
          - resolvFile: /tmp/resolv.conf.auto
    ```

## Filtering

Under certain circumstances, it may be useful to filter some types of DNS queries. You can define one or more DNS query
types, all queries with these types will be dropped (empty answer will be returned).

!!! example

    ```yaml
    filtering:
      queryTypes:
        - AAAA
    ```

This configuration will drop all 'AAAA' (IPv6) queries.

When `AAAA` is filtered, Blocky additionally strips the `ipv6hint` SvcParam from `HTTPS` and `SVCB`
(RFC 9460) answers. Without this, clients could still discover and connect to IPv6 endpoints
advertised in those records, bypassing the `AAAA` filter. Other SvcParams (e.g. `alpn`, `ipv4hint`)
are left untouched. Because stripping a hint changes the record, any DNSSEC signature on the
affected `HTTPS`/`SVCB` record is removed and the `AD` (authenticated data) flag is cleared for
that response.

## DNS rebinding protection

In a DNS rebinding attack, an attacker-controlled domain first resolves to a public IP, then
re-resolves to a private address, letting a victim's browser reach devices on the local network
under the attacker's origin. When this protection is enabled, blocky drops any answer from the
general upstream resolvers that contains a non-public IP address — in `A`/`AAAA` records or in
`ipv4hint`/`ipv6hint` SvcParams of `HTTPS`/`SVCB` records, in any section of the response
(answer, authority or additional) — and returns an empty `NOERROR` response instead (visible as
response type `REBIND` with reason `REBIND (rebinding protection)` in query logs and metrics;
the offending IP is logged at debug level). Rebinding hits count as **blocked** in the
[statistics](#statistics), and are reported to clients as Extended DNS Error `15 (Blocked)`.
**Disabled by default.**

!!! note "Upgrading"

    Rebinding hits previously used the `FILTERED` response type. Query log rows written before
    the upgrade keep the old value, so a dashboard grouping by response type shows `FILTERED` and
    `REBIND` side by side for the retention period. The same applies to Prometheus counters,
    which are labelled per response type.

The following ranges are considered non-public:

| Range                                           | Description             |
| ----------------------------------------------- | ----------------------- |
| `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16` | IPv4 private (RFC 1918) |
| `fc00::/7`                                      | IPv6 unique local       |
| `127.0.0.0/8`, `::1`                            | loopback                |
| `169.254.0.0/16`, `fe80::/10`                   | link-local              |
| `0.0.0.0`, `::`                                 | unspecified             |

Answers from [conditional upstreams](#conditional-dns-resolution), custom DNS and the hosts file
are never inspected, so internal zones served by trusted internal resolvers keep working without
any extra configuration. One exception: a `customDNS` entry of type `CNAME` resolves its target
through the regular chain (see [CNAME resolution](#cname-resolution)), so if the target resolves
to a private IP via the general upstreams (e.g. a DDNS name pointing into the LAN), that lookup
is inspected and the entry silently loses its address records — add the CNAME **target** name to
the allowlist to keep it working. Note that all `upstreams.groups` — **including client-specific
groups** — count as general upstream resolvers and are inspected; to serve an internal zone from
an internal resolver, use a conditional mapping instead of a client-keyed group. Lookups blocky
performs for its own operation (e.g. resolving `blocking.clientGroupsBlock` FQDN client
identifiers) are exempt from the protection.

For split-horizon domains that legitimately resolve to private IPs via the public upstreams, add
them to the allowlist. Entries match the domain itself and all of its subdomains; matching is
done on the name of the inspected query, so a `CNAME` inside an upstream answer pointing at an
allowlisted name does not bypass the protection (for `customDNS` `CNAME` entries the inspected
query is the lookup of the CNAME target, so allowlist the **target** name, not the entry's
name). If `customDNS.rewrite` rules apply to the query, matching uses the rewritten name —
allowlist the rewritten form (`conditional.rewrite` rules do not affect matching). Entries must
be plain domain names: wildcards (`*.example.com`), regexes and whitespace are rejected at
startup, and internationalized domains must be given in punycode (`xn--…`) form.

!!! example

    ```yaml
    rebindingProtection:
      enable: true
      allowedDomains:
        - intranet.example.com
    ```

!!! note

    The protection runs above the cache: the upstream answer is cached unchanged for its
    regular TTL, and every cache hit is re-inspected, so repeat queries for a blocked domain
    keep showing `REBIND`. This also covers cache entries synchronized from other instances
    through [redis](#redis). Answers from trusted local sources (conditional upstreams,
    special-use domains) are never written to the cache, so they cannot resurface as cached
    upstream answers and become subject to inspection.

    The protection does not cover NAT64/DNS64 setups: an IPv6 answer embedding a private IPv4
    address under a NAT64 prefix (e.g. `64:ff9b::192.168.1.1`) is not detected, and answers
    synthesized by blocky's own [DNS64](#dns64) resolver are not inspected at all (the internal
    `A` lookup they are derived from enters the resolver chain below the protection). On
    networks with a NAT64 gateway, DNS rebinding via synthesized IPv6 answers is therefore not
    prevented.

## Rate limiting per client IP

Blocky can enforce a per-client query rate limit at the head of the resolver chain. **Disabled by default.** This is _not_ a DDoS defense — for that, run [fail2ban](https://www.fail2ban.org/) or [crowdsec](https://www.crowdsec.net/) at the firewall, which can drop traffic before it reaches Blocky. The limiter is intended for misbehaving clients, runaway scripts, and the kind of low-volume amplification that can enroll a public Blocky instance in someone else's DNS reflection attack ([issue #1135](https://github.com/0xERR0R/blocky/issues/1135)).

When a client exceeds its bucket, the query is **dropped silently** (no UDP response, no DNS error) so that amplification ratio against a spoofed-source attacker is zero. A WARN log line is emitted (throttled to ≤ 1 line per bucket per second) so fail2ban can match against it.

### Configuration

```yaml
rateLimit:
  enable: true
  rate: 50           # avg queries per second per client
  burst: 100         # optional, default rate × 2; must be >= rate
  ipv4Prefix: 32     # default: aggregate by /32 (one IP = one client)
  ipv6Prefix: 64     # default: aggregate by /64 (one subscriber prefix)
  allowlist:
    - 127.0.0.1/32   # localhost (recommended if /api/query is used)
    - ::1/128
    - 10.0.0.0/8
    - 192.168.0.0/16
```

The limiter is a **token bucket** per client. Tokens refill at `rate` per second, capped at `burst`. Each query consumes one token. When the bucket is empty, the query is dropped.

- `rate` is the long-run sustained limit in queries/second.
- `burst` is the bucket capacity — how many queries an idle client may spend at once before being throttled to `rate`. Optional; defaults to `rate × 2` if omitted or set to 0.
- `ipv6Prefix` defaults to `/64` because residential IPv6 typically allocates a `/64` per subscriber; aggregating by `/64` keeps an attacker from cheaply rotating source addresses within their own subnet.

**Example — `rate: 100, burst: 200`:**

| Time | Bucket | Client sends | Result |
|------|--------|--------------|--------|
| 0.00 s | 200 (full) | 200 queries instantly | all 200 allowed, bucket drained |
| 0.00 s | 0 | 1 more query | **dropped** |
| 0.50 s | 50 (refilled at 100/s) | 50 queries | all 50 allowed |
| 0.50 s | 0 | 1 more query | **dropped** |
| 2.50 s | 200 (capped) | 200 queries instantly | all 200 allowed (burst recovered after 2 s idle) |

Pick `burst == rate` for a strict steady-state limiter with no spike tolerance. Pick `burst` 2–10× `rate` to let typical browser page loads through (a single page can trigger 30–80 DNS lookups in well under a second) while still capping long-running abusers at `rate` qps.

### Sample fail2ban filter

`/etc/fail2ban/filter.d/blocky-ratelimit.conf`:

```ini
[Definition]
failregex = ^.*rate-limiting: dropped query.*client_ip=<HOST>.*$
ignoreregex =
```

This filter assumes `logFormat: text` (the default); with `logFormat: json` the field becomes `"client_ip":"<HOST>"` and the regex must be adjusted accordingly.

### Metrics

- `blocky_rate_limit_drops_total{protocol="TCP|UDP"}` — counter, incremented on every drop (label cardinality is bounded).
- `blocky_rate_limit_cap_exhausted_total` — counter, incremented when a new client is dropped because the in-memory bucket store has reached its hard cap. A non-zero value means the store is under sustained pressure from many distinct clients.
- `blocky_rate_limit_active_buckets` — gauge, current number of in-memory token buckets.

Per-client IP is intentionally not a Prometheus label; it lives in the log line. Per-IP analysis is better served by log aggregation (Loki, ELK) than by Prometheus.

## FQDN only

In domain environments, it may be useful to only respond to FQDN requests. If this option is enabled blocky will respond immediately
with NXDOMAIN if the request is not a valid FQDN. The request is therefore not processed further by other options like custom or conditional.
Please be aware that by enabling this your resolution will break unless every query is for a fully qualified domain name.

!!! example

    ```yaml
    fqdnOnly:
      enable: true
    ```

## Custom DNS

You can define your own domain name mappings for local DNS resolution. This is useful for creating user-friendly names for network devices, defining domain names for local services, or creating your own DNS zone.

Custom DNS supports multiple record types (A, AAAA, CNAME, TXT, SRV) and provides automatic reverse DNS lookups for defined IP addresses.

| Parameter           | Type                                                   | Mandatory | Default value | Description                                                                                |
| ------------------- | ------------------------------------------------------ | --------- | ------------- | ------------------------------------------------------------------------------------------ |
| customTTL           | duration used for simple mappings (no unit is minutes) | no        | 1h            | Time-to-live for DNS records defined in the mapping section                                |
| rewrite             | string: string (domain: domain)                        | no        |               | Domain rewriting rules applied before DNS resolution                                       |
| mapping             | string: string (hostname: address list)                | no        |               | Simple domain to IP mappings                                                               |
| zone                | string containing a DNS Zone                           | no        |               | DNS zone file content for more complex configurations                                      |
| filterUnmappedTypes | boolean                                                | no        | true          | Whether to filter query types that aren't defined for a domain or forward them to upstream |

### Simple Mapping

The `mapping` parameter allows you to define simple domain-to-IP mappings. You can specify multiple IP addresses for a single domain by separating them with commas.

!!! example

    ```yaml
    customDNS:
      customTTL: 1h
      mapping:
        printer.lan: 192.168.178.3
        otherdevice.lan: 192.168.178.15,2001:0db8:85a3:08d3:1319:8a2e:0370:7344
    ```

This configuration will resolve:
- `printer.lan` to IPv4 address `192.168.178.3`
- `otherdevice.lan` to both IPv4 address `192.168.178.15` and IPv6 address `2001:0db8:85a3:08d3:1319:8a2e:0370:7344`

### Subdomain Resolution

Custom DNS automatically resolves subdomains of defined domains. For example, with the above configuration, queries for `my.printer.lan` or `any.subdomain.of.printer.lan` will also resolve to `192.168.178.3`.

### Domain Rewriting

With the optional `rewrite` parameter, you can replace part of a domain query with another string before resolution is performed:

!!! example

    ```yaml
    customDNS:
      rewrite:
        home: lan
        example.com: example-rewrite.com
      mapping:
        printer.lan: 192.168.178.3
        example-rewrite.com: 1.2.3.4
    ```

With this configuration:
- A query for `printer.home` will be rewritten to `printer.lan` and return `192.168.178.3`
- A query for `sub.example.com` will be rewritten to `sub.example-rewrite.com` and return `1.2.3.4`

### Zone File

For more complex configurations, you can use the `zone` parameter to define a DNS zone file:

!!! example

    ```yaml
    customDNS:
      zone: |
        $ORIGIN example.com.
        www 3600 A 1.2.3.4
        www 3600 AAAA 2001:db8:85a3::8a2e:370:7334
        @ 3600 CNAME www
    ```

The zone file supports standard DNS zone file syntax including:
- `$ORIGIN` - sets the origin for relative domain names
- `$TTL` - sets the default TTL for records in the zone
- `$INCLUDE` - includes another zone file relative to the blocky executable
- `$GENERATE` - generates a range of records

For records defined using the `zone` parameter, the `customTTL` parameter is unused. Instead, the TTL is defined in the zone directly.

### CNAME Resolution

When a CNAME record is defined and a query matches that record, blocky will:
1. Return the CNAME record in the answer
2. Additionally resolve the target of the CNAME and include those records in the answer
3. Protect against CNAME loops (where CNAMEs point to each other in a loop)

The target resolution in step 2 goes through the regular resolver chain. If
[rebinding protection](#dns-rebinding-protection) is enabled and the target resolves to a
private IP via the general upstreams, add the target name to its allowlist.

### Reverse DNS

Blocky automatically creates reverse DNS (PTR) records for all defined A and AAAA records. This allows reverse lookups from IP addresses to domain names.

### Filtering Unmapped Types

With `filterUnmappedTypes = true` (default), blocky will filter all queries with unmapped types. For example, if you only define an A record for `printer.lan`, an AAAA query for the same domain will return an empty result.

With `filterUnmappedTypes = false`, unmapped type queries will be forwarded to the upstream DNS server. For example, an AAAA query for `printer.lan` (when only an A record is defined) will be sent to the upstream resolver.

## Conditional DNS resolution

You can define, which DNS resolver(s) should be used for queries for the particular domain (with all subdomains). This
is for example useful, if you want to reach devices in your local network by the name. Since only your router know which
hostname belongs to which IP address, all DNS queries for the local network should be redirected to the router.

The optional parameter `rewrite` behaves the same as with custom DNS.

The optional parameter `fallbackUpstream`, if false (default), return empty result if after rewrite, the mapped resolver returned an empty answer. If true, the original query will be sent to the upstream resolver.

**Usage:** One usecase when having split DNS for internal and external (internet facing) users, but not all subdomains are listed in the internal domain

!!! example

    ```yaml
    conditional:
      fallbackUpstream: false
      rewrite:
        example.com: fritz.box
        replace-me.com: with-this.com
      mapping:
        fritz.box: 192.168.178.1
        lan.net: 192.170.1.2,192.170.1.3
        # for reverse DNS lookups of local devices
        178.168.192.in-addr.arpa: 192.168.178.1
        # for all unqualified hostnames
        .: 168.168.0.1
    ```

!!! tip

    You can use `.` as wildcard for all non full qualified domains (domains without dot)

In this example, a DNS query "client.fritz.box" will be redirected to the router's DNS server at 192.168.178.1 and client.lan.net to 192.170.1.2 and 192.170.1.3.
The query "client.example.com" will be rewritten to "client.fritz.box" and also redirected to the resolver at 192.168.178.1.

If not found and if `fallbackUpstream` was set to `true`, the original query "blog.example.com" will be sent upstream.

All unqualified host names (e.g. "test") will be redirected to the DNS server at 168.168.0.1.

One usecase for `fallbackUpstream` is when having split DNS for internal and external (internet facing) users, but not all subdomains are listed in the internal domain.

## Client name lookup

Blocky can try to resolve a user-friendly client name from the IP address or server URL (DoT and DoH). This is useful
for defining of blocking groups, since IP address can change dynamically.

### Resolving client name from URL/Host

If DoT or DoH is enabled, you can use a subdomain prefixed with `id-` to provide a client name (wildcard ssl certificate
recommended).

Example: domain `example.com`

DoT Host: `id-bob.example.com` -> request's client name is `bob`
DoH URL: `https://id-bob.example.com/dns-query` -> request's client name is `bob`

For DoH you can also pass the client name as url parameter:

DoH URL: `https://blocky.example.com/dns-query/alice` -> request's client name is `alice`

### Resolving client name from IP address

Blocky resolves a client's name from its IP address using the first of the following sources that yields a match:

1. **Custom client name mapping** – names defined manually via `clientLookup.clients` (see below).
2. **Local in-memory sources** – reverse (IP → name) entries already known to Blocky from your
   [custom DNS](#custom-dns) records and [hosts files](#hosts-file). This works automatically, requires no
   `clientLookup.upstream`, and performs no network lookup. For example, a hosts file entry `192.168.1.11 unifi`
   makes the client name of `192.168.1.11` resolve to `unifi`.
3. **rDNS upstream** – a reverse DNS lookup against the DNS server configured in `clientLookup.upstream`
   (typically your router).

If none of these returns a name, the IP address is used as the client name.

!!! note

    Because client names are also used for [blocking groups](#blocking-and-allowlisting) and
    [client-specific upstream groups](#upstreams-configuration), enabling local sources (custom DNS / hosts files) can change which
    group a client matches if a group is keyed on a name now resolved from those sources.

#### Single name order

Some routers return multiple names for the client (host name and user defined name). With
parameter `clientLookup.singleNameOrder` you can specify, which of retrieved names should be used.

#### Custom client name mapping

You can also map a particular client name to one (or more) IP (ipv4/ipv6) addresses. Parameter `clientLookup.clients`
contains a map of client name and multiple IP addresses.

!!! example

    ```yaml
    clientLookup:
      upstream: 192.168.178.1
      singleNameOrder:
        - 2
        - 1
      clients:
        laptop:
          - 192.168.178.29
    ```

    Use `192.168.178.1` for rDNS lookup. Take second name if present, if not take first name. IP address `192.168.178.29` is mapped to `laptop` as client name.

## Blocking and allowlisting

Blocky can use lists of domains and IPs to block (e.g. advertisement, malware,
trackers, adult sites). You can group several list sources together and define the blocking behavior per client.
Blocking uses the [DNS sinkhole](https://en.wikipedia.org/wiki/DNS_sinkhole) approach. For each DNS query, the domain name from
the request, IP address from the response, and any CNAME records will be checked to determine whether to block the query or not.

To avoid over-blocking, you can use allowlists.

You can also activate list groups only during configured time windows by using schedules.

### Definition allow/denylists

Lists are defined in groups. This allows using different sets of lists for different clients.

Each list in a group is a "source" and can be downloaded, read from a file, or inlined in the config. See [Sources](#sources) for details and configuring how those are loaded and reloaded/refreshed.

The supported list formats are:

1. the well-known [Hosts format](<https://en.wikipedia.org/wiki/Hosts_(file)>)
2. one domain per line (plain domain list)
3. one wildcard per line
4. one regex per line

!!! example

    ```yaml
    blocking:
      denylists:
        ads:
          - https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt
          - https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
          - |
            # inline definition using YAML literal block scalar style
            # content is in plain domain list format
            someadsdomain.com
            anotheradsdomain.com
            *.wildcard.example.com # blocks wildcard.example.com and all subdomains
          - |
            # inline definition with a regex
            /^banners?[_.-]/
        special:
          - https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews/hosts
      allowlists:
        ads:
          - allowlist.txt
          - /path/to/file.txt
          - |
            # inline definition with YAML literal block scalar style
            allowlistdomain.com
    ```

    In this example you can see 2 groups: **ads** and **special** with one list. The **ads** group includes 2 inline lists.

!!! warning

    If the same group has **both** allow/denylists, allowlists take precedence. Meaning if a domain is both blocked and allowed, it will be allowed.
    If a group has **only allowlist** entries, only domains from this list are allowed, and all others be blocked.

!!! warning
    You must also define a client group mapping, otherwise the allow/denylist definitions will have no effect.

#### Wildcard support

You can use wildcards to block a domain and all its subdomains.
Example: `*.example.com` will block `example.com` and `any.subdomains.example.com`.

#### Regex support

You can use regex to define patterns to block. A regex entry must start and end with the slash character (`/`). Some
Examples:

- `/baddomain/` will block `www.baddomain.com`, `baddomain.com`, but also `mybaddomain-sometext.com`
- `/^baddomain/` will block `baddomain.com`, but not `www.baddomain.com`
- `/^apple\.(de|com)$/` will only block `apple.de` and `apple.com`

!!! warning
    Regexes use more a lot more memory and are much slower than wildcards, you should use them as a last resort.

### Client groups

In this configuration section, you can define, which blocking group(s) should be used for which client in your network.
Example: All clients should use the **ads** group, which blocks advertisement and kids devices should use the **adult**
group, which blocky adult sites.

Clients without an explicit group assignment will use the **default** group.

You can use the client name (see [Client name lookup](#client-name-lookup)), client's IP address, client's full-qualified domain name
or a client subnet as CIDR notation.

If full-qualified domain name is used (for example "myclient.ddns.org"), blocky will try to resolve the IP address (A and AAAA records) of this domain.
If client's IP address matches with the result, the defined group will be used.

!!! example

    ```yaml
    blocking:
      clientGroupsBlock:
      # default will be used, if no special definition for a client name exists
        default:
          - ads
          - special
        laptop*:
          - ads
        192.168.178.1/24:
          - special
        kid-laptop:
          - ads
          - adult
    ```

    All queries from network clients, whose device name starts with `laptop`, will be filtered against the **ads** group's lists. All devices from the subnet `192.168.178.1/24` against the **special** group and `kid-laptop` against **ads** and **adult**. All other clients: **ads** and **special**.

!!! tip

    You can use `*` as wildcard for the sequence of any character or `[0-9]` as number range

### Schedule-based blocking

You can define named schedules and assign them to list groups. A list group is active only when at least one assigned schedule is active.

Rules:

1. If a list group has no schedule mapping, it is always active.
2. If a list group has multiple schedules, they are combined with OR logic (any active schedule enables the list).
3. Schedules use local server time. During daylight-saving transitions, only the specific skipped minutes are unobservable (a window overlapping the gap fires for its non-skipped portion); windows in the repeated hour fire twice.
4. Scheduling an allowlist-only group (a group that has allowlist entries but no denylist entries) time-gates that group's allowlist enforcement: outside the schedule, that allowlist is not consulted. Other active groups for the client (denylists or other allowlist-only groups) are still evaluated normally.

Each schedule supports:

- `weekdays`: required, list of `mon`, `tue`, `wed`, `thu`, `fri`, `sat`, `sun`
- `start` and `end`: optional `HH:MM` times

Time behavior:

1. Omit both `start` and `end` for full-day schedule on selected weekdays.
2. Set both `start` and `end` for a bounded window (`start <= now < end`).
3. If `start > end`, the schedule is overnight (for example `22:00` to `06:00`).
4. Do not set only one of `start` or `end`.

!!! example

    ```yaml
    blocking:
      denylists:
        ads:
          - https://example.org/ads.txt
        social:
          - https://example.org/social.txt

      schedules:
        workhours:
          weekdays: [mon, tue, wed, thu, fri]
          start: "08:00"
          end: "18:00"
        nights:
          weekdays: [sun, mon, tue, wed, thu]
          start: "22:00"
          end: "06:00"
        weekend-all-day:
          weekdays: [sat, sun]

      listSchedules:
        social: [workhours, nights]

      clientGroupsBlock:
        default:
          - ads
          - social
    ```

    In this example, `ads` is always active (no schedule mapping), while `social` is active during work hours or night window.

### Block type

You can configure, which response should be sent to the client, if a requested query is blocked (only for A and AAAA
queries, NXDOMAIN for other types):

| blockType  | Example                                                 | Description                                                                                                                                                                            |
| ---------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| zeroIP     | zeroIP                                                  | This is the default block type. Server returns 0.0.0.0 (or :: for IPv6) as result for A and AAAA queries                                                                               |
| nxDomain   | nxDomain                                                | return NXDOMAIN as return code                                                                                                                                                         |
| custom IPs | 192.100.100.15, 2001:0db8:85a3:08d3:1319:8a2e:0370:7344 | comma separated list of destination IP addresses. Should contain ipv4 and ipv6 to cover all query types. Useful with running web server on this address to display the "blocked" page. |

!!! example

    ```yaml
    blocking:
      blockType: nxDomain
    ```

### Block TTL

TTL for answers to blocked domains can be set to customize the time (in **duration format**) clients ask for those
domains again. Default Block TTL is **6 hours**. This setting applies to all blocking modes and will affect how much
time it could take for a client to be able to see the real IP address for a domain after receiving the blocked response.

**For `zeroIP` and custom IP modes:** The TTL is applied to the returned A/AAAA records in the answer section.

**For `nxDomain` mode:** The TTL is applied to the SOA record in the authority section. Per [RFC 2308](https://www.rfc-editor.org/rfc/rfc2308),
Blocky includes an SOA record in NXDOMAIN responses to enable proper negative caching by stub resolvers.
The blockTTL value is used for both the SOA's TTL and its MINIMUM field, ensuring clients cache the
NXDOMAIN response for the configured duration.

!!! example

    ```yaml
    blocking:
      blockType: 192.100.100.15, 2001:0db8:85a3:08d3:1319:8a2e:0370:7344
      blockTTL: 10s
    ```

### Lists Loading

See [Sources Loading](#sources-loading).

## Caching

Each DNS response has a TTL (Time-to-live) value. This value defines, how long is the record valid in seconds. The
values are maintained by domain owners, server administrators etc. Blocky caches the answers from all resolved queries
in own cache in order to avoid repeated requests. This reduces the DNS traffic and increases the network speed, since
blocky can serve the result immediately from the cache.

With following parameters you can tune the caching behavior:

!!! warning

    Wrong values can significantly increase external DNS traffic or memory consumption.

| Parameter                     | Type            | Mandatory | Default value | Description                                                                                                                                                                                                                                                                                                                                                                                                    |
| ----------------------------- | --------------- | --------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| caching.minTime               | duration format | no        | 0 (use TTL)   | How long a response must be cached (min value). If <=0, use response's TTL, if >0 use this value, if TTL is smaller                                                                                                                                                                                                                                                                                            |
| caching.maxTime               | duration format | no        | 0 (use TTL)   | How long a response must be cached (max value). If <0, do not cache responses. If 0, use TTL. If > 0, use this value, if TTL is greater                                                                                                                                                                                                                                                                        |
| caching.maxItemsCount         | int             | no        | 0 (unlimited) | Max number of cache entries (responses) to be kept in cache (soft limit). Default (0): unlimited. Useful on systems with limited amount of RAM.                                                                                                                                                                                                                                                                |
| caching.prefetching           | bool            | no        | false         | if true, blocky will preload DNS results for often used queries (default: names queried more than 5 times in a 2 hour time window). Results in cache will be loaded again on their expire (TTL). This improves the response time for often used queries, but significantly increases external traffic. It is recommended to increase "minTime" to reduce the number of prefetch queries to external resolvers. |
| caching.prefetchExpires       | duration format | no        | 2h            | Prefetch track time window                                                                                                                                                                                                                                                                                                                                                                                     |
| caching.prefetchThreshold     | int             | no        | 5             | Name queries threshold for prefetch                                                                                                                                                                                                                                                                                                                                                                            |
| caching.prefetchMaxItemsCount | int             | no        | 0 (unlimited) | Max number of domains to be kept in cache for prefetching (soft limit). Default (0): unlimited. Useful on systems with limited amount of RAM.                                                                                                                                                                                                                                                                  |
| caching.cacheTimeNegative     | duration format | no        | 30m           | Time how long negative results (NXDOMAIN response or empty result) are cached. A value of -1 will disable caching for negative results.                                                                                                                                                                                                                                                                        |
| caching.exclude               | Regex list      | no        |               | Exclusions rules as regex expressions of domains that won't be cached at all. Such as: /lan$/ or /^.*\.host\.com$/                                                                                                                                                                                                                                                                                                 |

!!! example

    ```yaml
    caching:
      minTime: 5m
      maxTime: 30m
      prefetching: true
      exclude:
        - /.*\.lan$/
        - /.*\.local$/
        - /.*\.host\.com\.(jp|fr)$/
    ```

## Redis

Blocky can synchronize its cache and blocking state between multiple instances through redis.
Synchronization is disabled if no address is configured.

| Parameter                | Type            | Mandatory | Default value | Description                                                         |
| ------------------------ | --------------- | --------- | ------------- | ------------------------------------------------------------------- |
| redis.address            | string          | no        |               | Server address and port, a unix socket path (starting with `/`), or master name if sentinel is used |
| redis.username           | string          | no        |               | Username if necessary                                               |
| redis.password           | string          | no        |               | Password if necessary (supports `file:` — see tip below)            |
| redis.database           | int             | no        | 0             | Database                                                            |
| redis.required           | bool            | no        | false         | Connection is required for blocky to start                          |
| redis.connectionAttempts | int             | no        | 3             | Max connection attempts                                             |
| redis.connectionCooldown | duration format | no        | 1s            | Time between the connection attempts                                |
| redis.sentinelUsername   | string          | no        |               | Sentinel username if necessary                                      |
| redis.sentinelPassword   | string          | no        |               | Sentinel password if necessary (supports `file:` — see tip below)   |
| redis.sentinelAddresses  | string[]        | no        |               | Sentinel host list (Sentinel is activated if addresses are defined) |

!!! tip "Loading secrets from files"

    Any sensitive value — `redis.password`, `redis.sentinelPassword`, and
    `queryLog.target` — can be loaded from a file instead of being written
    inline. Set the value to `file:` followed by an absolute path:

    ```yaml
    redis:
      password: file:/run/secrets/redis_password
    ```

    The file's contents become the value (a single trailing newline is
    stripped). This suits Docker/Kubernetes secrets and lets you restrict
    access to each secret with file permissions.

!!! example

    ```yaml
    redis:
      address: redismaster
      username: usrname
      password: passwd
      database: 2
      required: true
      connectionAttempts: 10
      connectionCooldown: 3s
      sentinelUsername: sentUsrname
      sentinelPassword: sentPasswd
      sentinelAddresses:
        - redis-sentinel1:26379
        - redis-sentinel2:26379
        - redis-sentinel3:26379
    ```

## Prometheus

Blocky can expose various metrics for prometheus. To use the prometheus feature, the HTTP listener must be enabled (
see [Basic Configuration](#basic-configuration)).

| Parameter         | Mandatory | Default value | Description                         |
| ----------------- | --------- | ------------- | ----------------------------------- |
| prometheus.enable | no        | false         | If true, enables prometheus metrics |
| prometheus.path   | no        | /metrics      | URL path to the metrics endpoint    |

!!! example

    ```yaml
    prometheus:
      enable: true
      path: /metrics
    ```

## Statistics

Blocky can collect in-memory statistics over a rolling 24h window and serve them as JSON at
`GET /api/stats`, independent of Prometheus. To
use this feature, the HTTP listener must be enabled (see [Basic Configuration](#basic-configuration)).

The endpoint returns curated totals (`summary`), raw per-type breakdowns (response type, query type
and response code), a per-hour time series, top-20 domains / blocked-domains / clients, and current
list and cache gauges. Disabled by default; when disabled,
`GET /api/stats` returns HTTP 503.

| Parameter         | Mandatory | Default value | Description                                                       |
| ----------------- | --------- | ------------- | ----------------------------------------------------------------- |
| statistics.enable | no        | false         | If true, enables in-memory statistics and the /api/stats endpoint |

!!! example

    ```yaml
    statistics:
      enable: true
    ```

!!! note

    Clients are identified by their resolved name (see [client name lookup](#client-name-lookup)) and fall
    back to their IP when no name is available. Queries dropped by the
    [rate limiter](#rate-limiting-per-client-ip) are always attributed to the client IP: the limiter
    runs before the client name lookup, so that its bucket key stays the connection's source IP.

## HTTP/3 (DoH3) {#http3}

Serve DNS-over-HTTPS over HTTP/3 (RFC 9114). When enabled, Blocky
listens on UDP at the same addresses as `ports.https` and answers the
same endpoints as the HTTPS listener (DoH `/dns-query`, REST API,
Prometheus metrics, web UI).

| Parameter    | Type    | Mandatory | Default value | Description                                                   |
| ------------ | ------- | --------- | ------------- | ------------------------------------------------------------- |
| http3.enable | boolean | no        | false         | Enable the HTTP/3 listener. Requires `ports.https` to be set. |

**Notes:**

- The HTTP/3 listener uses TLS 1.3 (mandated by RFC 9001). The
  `minTlsServeVersion` config does not affect it.
- If `http3.enable` is true but `ports.https` is empty, Blocky logs a
  warning at startup and does not open any UDP listeners.
- If `ports.proxyProtocol` includes `https`, HTTP/3 is disabled because
  QUIC/UDP cannot carry a PROXY protocol header.
- When HTTP/3 is enabled, HTTPS responses include an `Alt-Svc: h3=...`
  header so capable clients can switch transports automatically.

**Example:**

```yaml
ports:
  https: 443
http3:
  enable: true
```

## Query logging

You can enable the logging of DNS queries (question, answer, client, duration etc.) to a daily CSV file (can be opened
in Excel or OpenOffice Calc) or MySQL/MariaDB database.

!!! warning

    Query file/database contains sensitive information. Please ensure to inform users, if you log their queries.

### Query log types

You can select one of following query log types:

- `mysql`: log each query in the external MySQL/MariaDB database
- `postgresql`: log each query in the external PostgreSQL database
- `timescale`: log each query in the external Timescale database
- `sqlite`: log each query in a local SQLite database file (no external DB needed)
- `csv`: log into CSV file (one per day)
- `csv-client`: log into CSV file (one per day and per client)
- `console`: log into console output
- `dnstap`: export client DNS query/response events via [dnstap](https://dnstap.info/) (Frame Streams over Unix socket or TCP)
- `none`: do not log any queries

#### SQLite query log

The `sqlite` target stores the query log in a single local file (set via `queryLog.target`, e.g. `/var/lib/blocky/querylog.db`) — no external database is required. Blocky creates the file and its parent directory automatically.

!!! note
    The `sqlite` target is not available on every platform. It relies on a pure-Go SQLite driver that does not support all CPU architectures, so it is **not** compiled into the official builds for **`linux/mips`, `linux/mipsle`, `netbsd/arm`, `netbsd/arm64` and `openbsd/arm`**. On those builds, selecting `sqlite` fails at startup with a clear error message — use the `csv`, `mysql` or `postgresql` query log target instead. All other targets (including `linux/amd64`, `linux/arm`, `linux/arm64`, `windows/amd64` and `darwin`) support `sqlite`.

Set `queryLog.target` to a **plain filesystem path**. Do **not** prefix it with `file:` — for query-log targets that prefix means "read the target value from this file" (see the [Redis tip](#redis)), so `file:/var/lib/blocky/querylog.db` would be treated as a file to read the path *from*, not as the database itself.

Blocky opens the database in **WAL (Write-Ahead Logging) mode** automatically; you do not need to configure this. As a result the database is written as three files next to each other: `querylog.db`, `querylog.db-wal` and `querylog.db-shm`. When running in Docker, mount the **directory** (not just the `.db` file) as a volume so all three files persist, and include all three in any backup.

WAL requires a normal local filesystem. It is **not** supported on network filesystems (NFS, SMB/CIFS); placing the database on a network share can cause lock errors or corruption, so use local/block storage.

Retention (`logRetentionDays`) works the same as for the other database targets: entries older than the limit are deleted. Note that SQLite does not hand freed space back to the operating system on its own, so the `.db` file does not shrink after a deletion — the space is reused for new entries. Run `VACUUM` manually if you need to reclaim disk space.

##### Reading the SQLite query log from external tools

You can read the database with the `sqlite3` CLI, DB Browser for SQLite, a Grafana SQLite data source, or your own script:

- Reading **while Blocky is running** is safe — WAL lets a reader run concurrently with Blocky's writes without blocking. Open the database **read-only** (ideally with a `busy_timeout`) so your tool never interferes with Blocky.
- Open the **live database file in place**, with the `-wal`/`-shm` sidecar files present. The newest entries may still be in the `-wal` file before they are checkpointed into the main `.db`, so a WAL-aware open is required to see them.
- **Do not copy only `querylog.db`** for offline reading — you would miss un-checkpointed rows. Instead copy all three files together, run `PRAGMA wal_checkpoint(TRUNCATE);` first, or use `sqlite3 querylog.db ".backup backup.db"` (or `VACUUM INTO`).
- Use a tool with WAL support (a modern `sqlite3` CLI does).

#### dnstap query log

The `dnstap` target streams structured DNS events to an external collector (e.g. `dnstap-read`, SIEM pipelines). Each resolved client query is sent as a dnstap `CLIENT_RESPONSE` frame containing wire-format query and response messages.

Set `queryLog.target` to:

- `unix:/var/run/dnstap.sock` — Unix domain socket
- `tcp://127.0.0.1:6000` — TCP listener
- `/var/run/dnstap.sock` — bare path, treated as Unix socket

`queryLog.fields` and `queryLog.logRetentionDays` do not apply (streaming export, not stored rows/files). `queryLog.flushInterval` controls socket write batching.

!!! note
    dnstap exports full DNS messages. `log.privacy` obfuscation does not apply to dnstap payloads.

!!! note
    sustained non-zero values mean export loss.

### Query log fields

You can choose which information from processed DNS request and response should be logged in the target system. You can define one or more of following fields:

- `clientIP`: origin IP address from the request
- `clientName`: resolved client name(s) from the origins request
- `responseReason`: reason for the response (e.g. from which upstream resolver), response type and code. For blocked
  queries the reason also names the matched rule per group, e.g. `BLOCKED (ads: *.docler.com)`, so you can tell which
  denylist entry caused the block
- `responseAnswer`: returned DNS answer
- `question`: DNS question from the request
- `duration`: request processing time in milliseconds

!!! hint
    If not defined, blocky will log all available information. The `fields` setting does not apply to the `dnstap` target.

Configuration parameters:

| Parameter                 | Type                                                                                 | Mandatory | Default value | Description                                                                                   |
| ------------------------- | ------------------------------------------------------------------------------------ | --------- | ------------- | --------------------------------------------------------------------------------------------- |
| queryLog.type             | enum (mysql, postgresql, timescale, sqlite, csv, csv-client, console, dnstap, none (see above)) | no        |               | Type of logging target. Console if empty                                                      |
| queryLog.target           | string                                                                               | no        |               | directory for writing the logs (for csv), database file path (for sqlite), database url (for mysql, postgresql or timescale), or dnstap socket address (`unix:…`, `tcp://…`, or bare `/path`); supports `file:` for database URLs — see [Redis tip](#redis) |
| queryLog.logRetentionDays | int                                                                                  | no        | 0             | if > 0, deletes log files/database entries which are older than ... days; not used for dnstap   |
| queryLog.creationAttempts | int                                                                                  | no        | 3             | Max attempts to create specific query log writer                                              |
| queryLog.creationCooldown | duration format                                                                      | no        | 2s            | Time between the creation attempts                                                            |
| queryLog.fields           | list enum (clientIP, clientName, responseReason, responseAnswer, question, duration) | no        | all           | which information should be logged; ignored for dnstap                                        |
| queryLog.flushInterval    | duration format                                                                      | no        | 30s           | Interval to write buffered entries in bulk to the database (mysql/postgresql/timescale/sqlite) or dnstap socket batching |
| queryLog.ignore.sudn      | bool                                                                                 | no        | false         | if true, queries answered as Special Use Domain Names (SUDN) are not logged                   |
| queryLog.ignore.domains   | list of string                                                                       | no        |               | domains excluded from the query log; each entry is matched against the query name as an exact domain, a `*.wildcard`, or a `/regex/` |

!!! hint

    Please ensure, that the log directory is writable or database exists. If you use docker, please ensure, that the directory is properly
    mounted (e.g. volume)

### Database URLs

To connect to a database, you must provide a URL like value for `target`. The exact format and supported parameters depends on the DB type.
Parsing is handled not by Blocky, but third-party libraries, therefore the full documentation is external.

| Database   | Full docs                                                                                       | Format                                                                                   | Example                                                              |
| ---------- | ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| MySQL      | [Go MySQL driver Data Source Name](https://github.com/go-sql-driver/mysql#dsn-data-source-name) | `[username[:password]@][protocol[(host[:port])]]/dbname[?param1=value1[&paramN=valueN]]` | `username:password@tcp(localhost:3306)/blocky_query_log?timeout=15s` |
| PostgreSQL | [pgx.ParseConfig](https://pkg.go.dev/github.com/jackc/pgx/v5/pgconn#ParseConfig)                | `postgres://[username[:password]@][host[:port]]/dbname[?param1=value1[&paramN=valueN]]`  | `postgres://username@localhost:5432/blocky_query_log`                |
| Timescale  | See PostgreSQL above                                                                            |                                                                                          |                                                                      |

!!! note

    For increased security, it is recommended to configure the password for a PostgreSQL/Timescale connection via the `PGPASSFILE` environment variable.

### Examples

!!! example
**CSV format with limited logging information**

    ```yaml
    queryLog:
      type: csv
      target: /logs
      logRetentionDays: 7
      fields:
      - clientIP
      - duration
      flushInterval: 30s
    ```

!!! example
    **MySQL Database**

    ```yaml
    queryLog:
      type: mysql
      target: 'username:password@tcp(localhost:3306)/blocky_query_log?charset=utf8mb4&parseTime=True&loc=Local&timeout=15s'
      logRetentionDays: 7
    ```

!!! example
    **dnstap export**

    ```yaml
    queryLog:
      type: dnstap
      target: unix:/var/run/dnstap.sock
      flushInterval: 30s
      ignore:
        sudn: true
    ```

!!! example
    **Ignore noisy domains**

    ```yaml
    queryLog:
      type: postgresql
      target: postgres://user:password@db:5432/blocky_query_log
      ignore:
        sudn: true
        domains:
          - "*._dns-sd._udp.home"
          - "db._dns-sd._udp.home"
          - "/\\.in-addr\\.arpa$/"
          - "apple.com"
    ```

## Hosts file

You can enable resolving of entries, located in local hosts file.

Configuration parameters:

| Parameter                | Type                           | Mandatory | Default value | Description                                     |
| ------------------------ | ------------------------------ | --------- | ------------- | ----------------------------------------------- |
| hostsFile.sources        | list of string                 | no        |               | Host files (e.g. /etc/hosts on Linux)           |
| hostsFile.hostsTTL       | duration (no units is minutes) | no        | 1h            | TTL                                             |
| hostsFile.filterLoopback | bool                           | no        | false         | Filter loopback addresses (127.0.0.0/8 and ::1) |
| hostsFile.loading        |                                | no        |               | See [Sources Loading](#sources-loading)         |

!!! example

    ```yaml
    hostsFile:
      sources:
        - /etc/hosts
      hostsTTL: 1h
      loading:
        refreshPeriod: 30m
        strategy: fast
    ```

## Deliver EDE codes as EDNS0 option

DNS responses can be extended with EDE codes according to [RFC8914](https://datatracker.ietf.org/doc/rfc8914/).

For blocked queries the EDE extra text carries the same reason as the query log, including the matched group and rule
(e.g. `BLOCKED CNAME (ads: *.docler.com)`), so a client that requests EDE information learns which denylist entry caused
the block.

Configuration parameters:

| Parameter  | Type | Mandatory | Default value | Description                                         |
| ---------- | ---- | --------- | ------------- | --------------------------------------------------- |
| ede.enable | bool | no        | false         | If true, DNS responses are delivered with EDE codes |

!!! example

    ```yaml
    ede:
      enable: true
    ```

## EDNS Client Subnet options

EDNS Client Subnet (ECS) configuration parameters:

| Parameter       | Type | Mandatory | Default value | Description                                                                                   |
| --------------- | ---- | --------- | ------------- | --------------------------------------------------------------------------------------------- |
| ecs.useAsClient | bool | no        | false         | Use ECS information if it is present with a netmask is 32 for IPv4 or 128 for IPv6 as CientIP |
| ecs.forward     | bool | no        | false         | Forward ECS option to upstream                                                                |
| ecs.ipv4Mask    | int  | no        | 0             | Add ECS option for IPv4 requests if mask is greater than zero (max value 32)                  |
| ecs.ipv6Mask    | int  | no        | 0             | Add ECS option for IPv6 requests if mask is greater than zero (max value 128)                 |

!!! example

    ```yaml
    ecs:
      ipv4Mask: 32
      ipv6Mask: 128
    ```

## Special Use Domain Names

SUDN (Special Use Domain Names) are always enabled by default as they are required by various RFCs.
Some RFCs have optional recommendations, which are configurable as described below.
However, you can completely deactivate the blocking of SUDN by setting enable to false.
Warning! You should only disable this if your upstream DNS server is local, as it shouldn't be disabled for remote upstreams.

This also covers [RFC 9462 (Discovery of Designated Resolvers)](https://www.rfc-editor.org/rfc/rfc9462): queries
for any name in `resolver.arpa.` (e.g. `_dns.resolver.arpa.`) are answered locally with NODATA and never
forwarded upstream. Without this, stub resolvers (notably recent iOS/Android) can pick up a DDR answer from the
upstream and silently upgrade to that upstream, bypassing blocky.

Configuration parameters:

| Parameter                           | Type | Mandatory | Default value | Description                                                                                   |
| ----------------------------------- | ---- | --------- | ------------- | --------------------------------------------------------------------------------------------- |
| specialUseDomains.rfc6762-appendixG | bool | no        | true          | Block TLDs listed in [RFC 6762 Appendix G](https://www.rfc-editor.org/rfc/rfc6762#appendix-G) |
| enable                              | bool | no        | true          | completely disable or enable SUDN blocking                                                    |

!!! example

    ```yaml
    specialUseDomains:
      rfc6762-appendixG: true
    ```

!!! example

    ```yaml
    specialUseDomains:
      enable: false
    ```

## DNSSEC validation

DNSSEC (Domain Name System Security Extensions) provides cryptographic authentication of DNS data to protect against cache poisoning, man-in-the-middle attacks, and DNS spoofing. When enabled, Blocky validates DNSSEC signatures on DNS responses and ensures the chain of trust from the root zone to the queried domain.

### Overview

When DNSSEC validation is enabled, Blocky will:

- Set the DNSSEC OK (DO) bit on all upstream queries to request DNSSEC records
- Validate cryptographic signatures (RRSIG records) in DNS responses
- Verify the chain of trust from root to the queried domain using DNSKEY and DS records
- Validate authenticated denial of existence using NSEC/NSEC3 records for NXDOMAIN and NODATA responses
- Validate wildcard expansions according to RFC 4035 §5.3.4
- Return SERVFAIL for responses with invalid DNSSEC signatures (Bogus)
- Set the Authenticated Data (AD) flag only after successful validation
- Add Extended DNS Error (EDE) codes per RFC 8914 when validation fails

Independently of this setting, Blocky normalizes every response against the DNSSEC-related bits of the query it answers, because the client's DO bit is not necessarily the one used towards the upstream:

- The DNSSEC records (RRSIG, DNSKEY, DS, NSEC, NSEC3) are stripped from the response of a client that did not set the DO bit, unless that client explicitly queried for one of those types or sent an ANY query (RFC 4035 §3.2.1, RFC 3225 §3). Validation is unaffected: the DO bit is still set towards the upstream
- The DO bit of the query is copied into the response (RFC 3225 §3)
- The AD flag is only reported to clients that asked for the validation result by setting the DO or AD bit (RFC 6840 §5.8)

### RFC Compliance

Blocky's DNSSEC implementation complies with the following RFCs:

- **RFC 4033/4034/4035** (DNSSEC core specifications): Fully implemented
- **RFC 5155** (NSEC3): Implemented with configurable iteration limits
- **RFC 6840** (DNSSEC clarifications): Implemented including algorithm downgrade protection
- **RFC 8624** (Algorithm implementation requirements): Supports recommended algorithms
- **RFC 8914** (Extended DNS Errors): Provides detailed error information

### Security Benefits

DNSSEC validation protects against:

- **Cache poisoning attacks**: Prevents attackers from injecting false DNS records into the cache
- **Man-in-the-middle attacks**: Ensures DNS responses haven't been tampered with in transit
- **DNS spoofing**: Verifies that DNS responses come from authoritative sources
- **Forged denial-of-existence**: Validates that NXDOMAIN responses are authentic

### Configuration Parameters

| Parameter                 | Type          | Mandatory | Default value | Description                                                                                       |
| ------------------------- | ------------- | --------- | ------------- | ------------------------------------------------------------------------------------------------- |
| dnssec.validate           | bool          | no        | false         | Enable DNSSEC validation                                                                          |
| dnssec.maxChainDepth      | uint          | no        | 10            | Maximum domain label depth for chain of trust validation (DoS protection)                         |
| dnssec.cacheExpirationHours | uint        | no        | 1             | How long to cache validation results in hours                                                     |
| dnssec.maxUpstreamQueries | uint          | no        | 30            | Maximum upstream DNS queries per validation request (DoS protection)                              |
| dnssec.maxNSEC3Iterations | uint          | no        | 150           | Maximum NSEC3 hash iterations allowed (DoS protection, per RFC 5155 §10.3)                        |
| dnssec.clockSkewToleranceSec | uint       | no        | 3600          | Clock skew tolerance in seconds for signature validation (per RFC 6781 §4.1.2)                    |
| dnssec.trustAnchors       | string array  | no        | []            | Custom trust anchors (DNSKEY or DS records). Empty array uses built-in root trust anchors         |

### DoS Protection

DNSSEC validation can be computationally expensive and requires additional DNS queries. Blocky includes several protections against DoS attacks:

- **maxChainDepth**: Limits validation depth for deeply nested domains (e.g., prevents validation of `a.b.c.d.e.f.g.h.i.j.example.com`)
- **maxUpstreamQueries**: Limits the number of upstream queries (DNSKEY/DS lookups) per validation to prevent query amplification
- **maxNSEC3Iterations**: Limits NSEC3 hash iterations to prevent CPU exhaustion attacks
- **Validation caching**: Caches validation results to avoid repeated expensive validations

### Clock Skew Tolerance

DNSSEC signatures have inception and expiration timestamps. Blocky validates that the current time falls within this validity window. However, in real-world deployments, system clocks may not be perfectly synchronized, which can cause valid signatures to be rejected.

The `clockSkewToleranceSec` parameter (default: 3600 seconds = 1 hour) allows validation to succeed even if the system clock is off by the specified amount. This matches the behavior of industry-standard validators like Unbound and BIND.

**Common scenarios where clock skew tolerance is important:**

- **Virtual machines**: Clock drift after suspend/resume or snapshot restore operations
- **Containers**: Clock synchronization issues with the host system
- **IoT/embedded devices**: Systems without NTP or with battery-backed RTCs that lose time
- **Systems during NTP sync**: Time adjustment in progress when DNSSEC validation is needed
- **Raspberry Pi and similar SBCs**: Devices that lose time on power loss

**Recommended values:**

- **300 seconds (5 minutes)**: Conservative setting, handles typical NTP drift
- **3600 seconds (1 hour)**: Default, handles timezone/DST issues and matches Unbound/BIND
- **7200 seconds (2 hours)**: Lenient setting for systems with significant clock problems

!!! warning "Security vs. Robustness Tradeoff"
    Larger clock skew tolerance values increase the risk of accepting signatures that are actually expired or not yet valid. Smaller values increase the risk of false negatives from clock drift. For production deployments, ensure NTP or systemd-timesyncd is properly configured and use the default 1-hour tolerance.

### Validation Results

Blocky classifies DNSSEC validation results into four categories:

| Result          | Description                                                          | Response Handling                        |
| --------------- | -------------------------------------------------------------------- | ---------------------------------------- |
| **Secure**      | Valid DNSSEC signatures and complete chain of trust                  | AD flag set, response returned           |
| **Insecure**    | Domain is not signed with DNSSEC (no RRSIG records)                  | AD flag cleared, response returned       |
| **Bogus**       | Invalid DNSSEC signatures or broken chain of trust                   | SERVFAIL returned with EDE code `6 (DNSSEC Bogus)`, response type `BOGUS` |
| **Indeterminate** | Validation could not be completed (e.g., network errors, budget exceeded) | AD flag cleared, response returned |

A `BOGUS` result means blocky could not obtain a trustworthy answer, so it counts as an **error**
in the [statistics](#statistics) — not as a block. A domain whose operator has misconfigured DNSSEC
is not something blocky blocked, and never appears in `topBlockedDomains`.

!!! note "Upgrading"

    Validation failures previously used the `BLOCKED` response type, which counted them as blocks
    and reported them to clients as Extended DNS Error `15 (Blocked)` instead of `6 (DNSSEC
    Bogus)`. Query log rows written before the upgrade keep the old value, so a dashboard grouping
    by response type shows `BLOCKED` and `BOGUS` side by side for the retention period.

### Trust Anchors

By default, Blocky uses the latest IANA root trust anchors (KSK-2017 and KSK-2024) for DNSSEC validation. These are the same trust anchors used by major DNS resolvers and are embedded in the Blocky binary.

#### Custom Trust Anchors

You can override the default trust anchors for:

- Testing DNSSEC in lab environments
- Validating private/internal DNSSEC-signed zones
- Using specific trust anchors for organizational requirements

Trust anchors can be specified in two formats:

1. **DNSKEY format**: Full DNSKEY record in zone file format
2. **DS format**: DS record pointing to a DNSKEY in the child zone

!!! warning
    Specifying custom trust anchors **replaces** the default root trust anchors. If you need to validate both public domains and private zones, you must include both the root trust anchors and your custom anchors.

### Supported Algorithms

Blocky supports the following DNSSEC algorithms per RFC 8624:

| Algorithm | ID | Status | Security Level |
| --------- | -- | ------ | -------------- |
| RSASHA1   | 5  | Deprecated (still supported) | Weak |
| RSASHA256 | 8  | Recommended | Moderate |
| RSASHA512 | 10 | Recommended | Moderate |
| ECDSAP256SHA256 | 13 | Recommended | Strong |
| ECDSAP384SHA384 | 14 | Recommended | Strong |
| ED25519   | 15 | Recommended | Very Strong |
| ED448     | 16 | Recommended | Strongest |

Blocky automatically selects the strongest available algorithm when multiple signatures are present, providing protection against algorithm downgrade attacks per RFC 6840 §5.11.

### Performance Considerations

DNSSEC validation adds overhead to DNS resolution:

- **Additional queries**: Each validation may require 2-4 additional queries for DNSKEY and DS records
- **Cryptographic operations**: Signature verification requires CPU time
- **Caching helps significantly**: The validation cache reduces overhead for frequently queried domains

Recommendations:

- Start with `cacheExpirationHours: 1` and increase if validation overhead is acceptable
- Monitor upstream query count with the `maxUpstreamQueries` metric
- Use `prefetching` in the caching resolver to pre-validate popular domains
- Consider disabling for high-volume recursive resolvers if performance is critical

### Examples

!!! example "Basic DNSSEC validation"

    ```yaml
    dnssec:
      validate: true
    ```

    This enables DNSSEC validation with all default settings. Blocky will validate all DNSSEC-signed domains using the built-in root trust anchors.

!!! example "DNSSEC validation with custom DoS protection"

    ```yaml
    dnssec:
      validate: true
      maxChainDepth: 15
      maxUpstreamQueries: 50
      maxNSEC3Iterations: 100
      cacheExpirationHours: 2
    ```

    This configuration increases the allowed chain depth and upstream queries for complex delegation scenarios, while reducing NSEC3 iterations for better DoS protection and doubling the cache duration.

!!! example "DNSSEC validation with clock skew tolerance for embedded systems"

    ```yaml
    dnssec:
      validate: true
      clockSkewToleranceSec: 7200  # 2 hours tolerance for systems with clock drift
    ```

    This configuration is suitable for embedded devices, Raspberry Pi, or systems without reliable NTP. The 2-hour tolerance allows validation to succeed even with significant clock drift, while still providing DNSSEC security.

!!! example "DNSSEC validation with custom trust anchor for private zone"

    ```yaml
    dnssec:
      validate: true
      trustAnchors:
        # Root KSK-2024 (required for public domains)
        - ". 172800 IN DNSKEY 257 3 8 AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3..."
        # Custom DNSKEY for internal zone
        - "corp.internal. 86400 IN DNSKEY 257 3 13 mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAeF..."
    ```

    This configuration validates both public internet domains (using the root trust anchor) and a private `corp.internal` zone using a custom trust anchor.

!!! example "Testing DNSSEC validation"

    ```yaml
    dnssec:
      validate: true
    log:
      level: debug
    ```

    Enable debug logging to see detailed DNSSEC validation information for troubleshooting.

### Monitoring

Blocky exposes Prometheus metrics for DNSSEC validation:

- `blocky_dnssec_validation_total{result="secure|insecure|bogus|indeterminate"}`: Total validations by result
- `blocky_dnssec_cache_hits_total`: Number of validation cache hits
- `blocky_dnssec_validation_duration_seconds{result="..."}`: Validation duration histogram

### Troubleshooting

**Problem**: All DNSSEC-signed domains return SERVFAIL

- Check that your upstream DNS servers support DNSSEC and return DNSSEC records
- Verify the DO bit is being set on upstream queries (check logs with `log.level: debug`)
- Ensure your upstream servers are not stripping DNSSEC records

**Problem**: Specific domain fails DNSSEC validation

- Check if the domain's DNSSEC is properly configured using online validators like [DNSViz](https://dnsviz.net/) or [Verisign DNSSEC Debugger](https://dnssec-debugger.verisignlabs.com/)
- Enable debug logging to see detailed validation failure reasons
- Check for expired RRSIG records or missing DNSKEY records

**Problem**: High CPU usage after enabling DNSSEC

- Increase `cacheExpirationHours` to reduce validation frequency
- Lower `maxNSEC3Iterations` if validating many NSEC3-signed zones
- Consider disabling DNSSEC validation for internal-only resolvers

**Problem**: Validation fails with "budget exhausted"

- Increase `maxUpstreamQueries` for domains with complex delegation chains
- Check if upstream servers are slow to respond (may require multiple retries)
- Increase `maxChainDepth` for deeply nested subdomains

**Problem**: Signatures rejected as "expired" or "not yet valid" despite being valid

- Check your system clock is synchronized with NTP (`timedatectl status` on Linux)
- Increase `clockSkewToleranceSec` if your system has known clock drift issues (VMs, containers, embedded devices)
- Verify RRSIG inception/expiration times are reasonable using online validators
- For systems without NTP, consider using a larger tolerance (e.g., 7200 seconds = 2 hours)

## DNS64

DNS64 (RFC 6147) enables IPv6-only clients to access IPv4-only services by synthesizing AAAA (IPv6) records from A (IPv4) records. This technology is critical for IPv6-only networks such as cellular networks, IoT deployments, modern datacenters, and enterprise networks transitioning to IPv6.

### Overview

When DNS64 is enabled, Blocky will:

- Intercept AAAA queries and check if native IPv6 records exist
- If no native AAAA records are found, query for A records
- Synthesize AAAA records by embedding IPv4 addresses into configured IPv6 prefixes
- Return synthesized AAAA records with proper TTL and DNSSEC handling
- Preserve CNAME/DNAME chains in synthesized responses
- Skip synthesis if AAAA records already exist (unless they're in the exclusion set)

!!! warning "NAT64 Required"
    DNS64 **requires** a NAT64 gateway in your network. DNS64 alone won't enable IPv6-only clients to reach IPv4 destinations.
    The DNS64 prefix must match your NAT64 gateway's configuration.

### RFC Compliance

Blocky's DNS64 implementation complies with:

- **RFC 6147** (DNS64 specification): Non-validating mode implementation
- **RFC 6052** (IPv4-to-IPv6 address embedding): All six prefix lengths supported (/32, /40, /48, /56, /64, /96)
- **RFC 8914** (Extended DNS Errors): Synthesized responses include EDE code 4 (Forged Answer)

### Configuration Parameters

| Parameter          | Type           | Mandatory | Default value                                        | Description                                                                                           |
| ------------------ | -------------- | --------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| dns64.enable       | bool           | no        | false                                                | Enable DNS64 synthesis                                                                                |
| dns64.prefixes     | array of CIDR  | no        | [64:ff9b::/96]                                       | IPv6 prefixes for synthesis. Valid lengths: /32, /40, /48, /56, /64, /96 (per RFC 6052)              |
| dns64.exclusionSet | array of CIDR  | no        | [::ffff:0:0/96, ::1/128, fe80::/10] + prefixes      | Custom exclusion set. Advanced use only - overrides default exclusion behavior              |

### DNS64 Prefixes

DNS64 uses IPv6 prefixes to embed IPv4 addresses. RFC 6052 defines six valid prefix lengths, each with different bit layouts:

| Prefix Length | Bits Used | IPv4 Position | Example Prefix     | Example Synthesis                              |
| ------------- | --------- | ------------- | ------------------ | ---------------------------------------------- |
| /96           | 96        | bits 96-127   | `64:ff9b::/96`     | `192.0.2.1` → `64:ff9b::192.0.2.1`             |
| /64           | 64        | bits 72-103   | `2001:db8::/64`    | `192.0.2.1` → `2001:db8::c000:201`             |
| /56           | 56        | bits 56-63, 72-95 | `2001:db8::/56` | `192.0.2.1` → `2001:db8:c0:0:201::`            |
| /48           | 48        | bits 48-63, 72-87 | `2001:db8::/48` | `192.0.2.1` → `2001:db8:0:c000:2:100::`        |
| /40           | 40        | bits 40-63, 72-79 | `2001:db8::/40` | `192.0.2.1` → `2001:db8:c0:2:1::`              |
| /32           | 32        | bits 32-63    | `2001:db8::/32`    | `192.0.2.1` → `2001:db8:c000:201::`            |

**Well-known prefix (default)**: `64:ff9b::/96` is the RFC 6052 well-known prefix, recommended for most deployments.

**Multiple prefixes**: You can configure multiple prefixes for load balancing across multiple NAT64 gateways. DNS64 will synthesize one AAAA record per prefix per A record.

!!! warning "Prefix Overlap Validation"
    Blocky validates that configured prefixes do not overlap. Overlapping prefixes will cause startup failure.

### Exclusion Set

DNS64 will **not** synthesize AAAA records if the response already contains AAAA records that are **not** in the exclusion set. This prevents synthesizing for domains that already have native IPv6 support.

#### Default Exclusion Set (Recommended)

If `dns64.exclusionSet` is not configured, Blocky uses the default exclusion set:

- **IPv4-mapped IPv6 addresses** (`::ffff:0:0/96`) - RFC 6147 requirement
- **Loopback address** (`::1/128`) - RFC 6147 recommendation
- **Link-local addresses** (`fe80::/10`) - RFC 6147 recommendation
- **Unspecified address** (`::/128`) - Special case
- **Configured DNS64 prefixes** - Automatically added to prevent synthesis loops

**Example**: If a domain returns AAAA record `2001:db8::1` (not in exclusion set), DNS64 will **not** synthesize. If a domain returns only `::1` (in exclusion set), DNS64 **will** synthesize from A records.

#### Custom Exclusion Set (Advanced)

!!! warning "Advanced Configuration"
    Configuring a custom exclusion set is an **advanced feature** and should only be used if you understand DNS64 synthesis behavior. Most users should use the default exclusion set.

You can configure a custom exclusion set using the `dns64.exclusionSet` parameter:

!!! example
    ```yaml
    dns64:
      enable: true
      prefixes:
        - 64:ff9b::/96
      exclusionSet:
        - ::ffff:0:0/96  # IPv4-mapped (REQUIRED by RFC)
        - ::1/128        # Loopback (recommended)
        - fe80::/10      # Link-local (recommended)
        - 64:ff9b::/96   # Your configured prefix (recommended to prevent loops)
        - 2001:db8::/32  # Custom exclusion range
    ```

!!! danger "Important: No Automatic Prefix Addition"
    When using a custom exclusion set, **configured DNS64 prefixes are NOT automatically added**. You must explicitly include your configured prefixes in the exclusion set to prevent synthesis loops.

    **Bad configuration** (will cause synthesis loops):
    ```yaml
    dns64:
      prefixes:
        - 64:ff9b::/96
      exclusionSet:
        - ::ffff:0:0/96  # Missing configured prefix!
    ```

    **Good configuration**:
    ```yaml
    dns64:
      prefixes:
        - 64:ff9b::/96
      exclusionSet:
        - ::ffff:0:0/96
        - 64:ff9b::/96   # Includes configured prefix
    ```

### DNSSEC Handling (Non-Validating Mode)

DNS64 operates in **non-validating mode** and handles DNSSEC as follows:

- **DO bit**: Copied from original AAAA query to A query (preserves client's DNSSEC request)
- **CD bit**: Copied from original AAAA query to A query (preserves checking-disabled flag)
- **AD bit**: Always **cleared** in synthesized responses (synthesis breaks authentication)
- **Extended DNS Error**: Synthesized responses include EDE code 4 (Forged Answer)

!!! info "Future: DNSSEC-Aware DNS64"
    Future enhancement may implement RFC 6147 Section 3 (DNSSEC-aware operation), where DNS64 can validate DNSSEC signatures and set the AD bit appropriately for synthesized responses. The current non-validating mode is suitable for most deployments.

### Performance Considerations

DNS64 adds overhead to DNS resolution:

- **Additional A query**: Every AAAA query that requires synthesis triggers an additional A query
- **Caching is critical**: Enable caching to minimize upstream query load
- **Cache hit rate**: With caching, most AAAA queries hit the cache and avoid synthesis overhead

!!! warning "Enable Caching for DNS64"
    DNS64 without caching will **double** upstream query load (one AAAA + one A per query). Blocky will log a warning if DNS64 is enabled without caching.

## Healthcheck

Blocky responds to a dedicated `healthcheck.blocky` DNS query that is
designed for container liveness probes. The handler is registered on
the DNS listeners created by Blocky (UDP, TCP, DoT) and responds to any
query type with `NOERROR` and an empty answer section. It does **not**
go through the resolver chain, so it stays cheap and is unaffected by
upstream health.

!!! example

    ```sh
    dig @ -p <dns-port> healthcheck.blocky A +short
    ```

    Returns no answer records but `status: NOERROR` — probes should
    test for `status: NOERROR`, not for any record content.

!!! tip "Docker / Kubernetes liveness probe"

    A liveness probe should treat a `NOERROR` response as healthy.
    Example for Docker Compose:

    ```yaml
    healthcheck:
      test: ["CMD", "dig", "@127.0.0.1", "-p", "53", "healthcheck.blocky", "+short", "+tries=1"]
      interval: 30s
      timeout: 5s
      retries: 3
    ```

## SSL certificate configuration (DoH / TLS listener)

See [Wiki - Configuration of HTTPS](https://github.com/0xERR0R/blocky/wiki/Configuration-of-HTTPS-for-DoH-and-Rest-API)
for detailed information, how to create and configure SSL certificates.

DoH url: `https://host:port/dns-query`

## Sources

Sources are a concept shared by the blocking and hosts file resolvers. They represent where to load the files for each resolver.

The supported source types are:

- HTTP(S) URL (any source starting with `http`)
- inline configuration (any source containing a newline)
- local file path (any source not matching the above rules)

!!! note

    The format/content of the sources depends on the context: lists and hosts files have different, but overlapping, supported formats.

!!! example

    ```yaml
    - https://example.com/a/source # blocky will download and parse the file
    - /a/file/path # blocky will read the local file
    - | # blocky will parse the content of this multi-line string
      # inline configuration
    ```

### Sources Loading

This sections covers `loading` configuration that applies to both the blocking and hosts file resolvers.
These settings apply only to the resolver under which they are nested.

!!! example

    ```yaml
    blocking:
      loading:
        # only applies to allow/denylists

    hostsFile:
      loading:
        # only applies to hostsFile sources
    ```

#### Refresh / Reload

To keep source contents up-to-date, blocky can periodically refresh and reparse them. Default period is
**4 hours**. You can configure this by setting the `refreshPeriod` parameter to a value in **duration format**.
A value of zero or less will disable this feature.

!!! example

    ```yaml
    loading:
      refreshPeriod: 1h
    ```

    Refresh every hour.

### Downloads

Configures how HTTP(S) sources are downloaded:

| Parameter    | Type     | Mandatory | Default value | Description                                    |
| ------------ | -------- | --------- | ------------- | ---------------------------------------------- |
| timeout      | duration | no        | 5s            | Download attempt timeout                       |
| writeTimeout | duration | no        | 20s           | File write attempt timeout                     |
| readTimeout  | duration | no        | 20s           | Download request read timeout                  |
| readHeaderTimeout | duration | no        | 20s           | Download request header read timeout           |
| attempts     | int      | no        | 3             | How many download attempts should be performed |
| cooldown     | duration | no        | 500ms         | Time between the download attempts             |
| cachePath    | path     | no        | (empty)       | Directory for an on-disk cache of downloaded sources (see below) |

`cachePath` (default: empty): directory for an on-disk cache of downloaded sources. When set, blocky sends HTTP
conditional requests (`If-None-Match` / `If-Modified-Since`) and skips re-downloading unchanged lists, serves a source
from disk when its host is temporarily unreachable, and — with `loading.strategy: fast` — seeds lists from disk on
startup so it can answer queries immediately. Validators are kept in memory and reset on restart, so the first refresh
after a restart re-downloads each source. When unset, downloads are fully stateless (nothing is written to disk).
Mount this directory on a persistent volume in containerized setups.
Note: when you remove or change a source URL, its previous cache file is left on disk — blocky does not delete orphaned cache files automatically. They are harmless and bounded; clear the cache directory manually if you want to reclaim the space.

!!! warning "The directory must be writable by blocky"

    If blocky cannot write to `cachePath`, it logs
    `cannot create temp cache file in , serving without caching` and keeps
    resolving normally — only the on-disk cache is disabled, so this degrades
    performance rather than breaking DNS.

    In Docker, blocky runs as **UID 100**. Use the pre-created `/app/cache` directory
    with a named volume, or make your own path writable by UID 100. See
    [writable mounts and file permissions](installation.md#run-blocky).

!!! example

    ```yaml
    loading:
      downloads:
        timeout: 4m
        attempts: 5
        cooldown: 10s
        cachePath: /var/cache/blocky/lists
    ```

    In a container, prefer the pre-created cache directory and mount a named volume
    over it (`-v blocky_cache:/app/cache`):

    ```yaml
    loading:
      downloads:
        cachePath: /app/cache
    ```

### Strategy

See [Init Strategy](#init-strategy).
In this context, "init" is loading and parsing each source, and an error is a single source failing to load/parse.

!!! example

    ```yaml
    loading:
      strategy: failOnError
    ```

### Max Errors per Source

Number of errors allowed when parsing a source before it is considered invalid and parsing stops.
A value of -1 disables the limit.

!!! example

    ```yaml
    loading:
      maxErrorsPerSource: 10
    ```

### Concurrency

Blocky downloads and processes sources concurrently. This allows limiting how many can be processed in the same time.
Larger values can reduce the overall list refresh time at the cost of using more RAM. Please consider reducing this value on systems with limited memory.
Default value is 4.

!!! example

    ```yaml
    loading:
      concurrency: 10
    ```

!!! note

    As with other settings under `loading`, the limit applies to the blocking and hosts file resolvers separately.
    The total number of concurrent sources concurrently processed can reach the sum of both values.
    For example if blocking has a limit set to 8 and hosts file's is 4, there could be up to 12 concurrent jobs.

---

## File: docs/index.md

# Blocky

<figure>
  
</figure>

Blocky is a DNS proxy and ad-blocker for the local network written in Go with following features:

```mermaid
flowchart LR
    C[Client devices] -->|DNS query| B(Blocky)
    B -->|allowed query| U[Upstream resolvers]
    U -->|answer| B
    B -->|blocked domain| X[Blocked response]
    B -->|answer| C
```

## Features

- **Blocking** - :no_entry: Blocking of DNS queries with external lists (Ad-block, malware) and allowlisting

    * Definition of allow/denylists per client group (Kids, Smart home devices, etc.)
    * Periodical reload of external allow/denylists
    * Regex support
    * Blocking of request domain, response CNAME (deep CNAME inspection) and response IP addresses (against IP lists)

- **Advanced DNS configuration** - :nerd: not just an ad-blocker

    * Custom DNS resolution for certain domain names
    * Conditional forwarding to external DNS server
    * Upstream resolvers can be defined per client group

- **Performance** - :rocket: Improves speed and performance in your network

    * Customizable caching of DNS answers for queries -> improves DNS resolution speed and reduces amount of external DNS
      queries
    * Prefetching and caching of often used queries
    * Using multiple external resolver simultaneously
    * Low memory footprint

- **Various Protocols** - :computer: Supports modern DNS protocols

    * DNS over UDP and TCP
    * DNS over HTTPS (aka DoH)
    * DNS over TLS (aka DoT)
    * DNS over QUIC (aka DoQ, RFC 9250)
    * DNS over HTTPS/3 (aka DoH3, RFC 9114)

- **Security and Privacy** - :dark_sunglasses: Secure communication

    * Supports modern DNS extensions: DNSSEC, eDNS, ...
    * DNSSEC validation of upstream resolvers
    * Free configurable blocking lists - no hidden filtering etc.
    * Provides DoH Endpoint
    * Uses random upstream resolvers from the configuration - increases your privacy through the distribution of your DNS
      traffic over multiple provider
    * Open source development
    * Blocky does **NOT** collect any user data, telemetry, statistics etc.

- **Integration** - :notebook_with_decorative_cover: various integration

    * [Prometheus](https://prometheus.io/) metrics
    * Prepared [Grafana](https://grafana.com/) dashboards (Prometheus and database)
    * Logging of DNS queries per day / per client in CSV format or MySQL/MariaDB/PostgreSQL/Timescale database - easy to
      analyze
    * Various REST API endpoints
    * CLI tool

- **Simple configuration** - :baby: single or multiple configuration files in YAML format

    * Simple to maintain
    * Simple to backup

- **Simple installation/configuration** - :cloud: blocky was designed for simple installation

    * Stateless (no database, no temporary files)
    * Docker image with Multi-arch support
    * Single binary
    * Supports x86-64, ARM, and MIPS architectures -> runs fine on Raspberry PI and OpenWrt routers
    * Community supported Helm chart for k8s deployment


## Contribution

Issues, feature suggestions and pull requests are welcome! Blocky lives on :material-github:[GitHub](https://github.com/0xERR0R/blocky).

## Support Blocky :heart:

Blocky is **free, open source, and developed in my spare time** — with no telemetry, no data
collection, and no hidden filtering. **Thousands of people run Blocky; only a handful support it.**
If Blocky is useful to you, please consider chipping in — your support directly funds ongoing
development and helps keep Blocky independent and ad-free, forever. Every contribution, however
small, is hugely appreciated. Thank you! :heart:

[](https://github.com/sponsors/0xERR0R)
[](https://thanks.dev/u/gh/0xERR0R)
[](https://liberapay.com/spx01)
[](https://ko-fi.com/0xerr0r)
[](https://paypal.me/spx01)

---

## File: docs/installation.md

# Installation

You can choose one of the following installation options:

* Run as standalone binary
* Run as docker container

## Prepare your configuration

Blocky supports single or multiple YAML files as configuration. Create new `config.yml` with your configuration
(see [Configuration](configuration.md) for more details and all configuration options).

Simple configuration file, which enables only basic features:

```yaml
upstreams:
  groups:
    default: # (1)!
      - 46.182.19.48
      - 80.241.218.68
      - tcp-tls:fdns1.dismail.de:853
      - https://dns.digitale-gesellschaft.ch/dns-query
blocking:
  denylists:
    ads: # (2)!
      - https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
  clientGroupsBlock:
    default:
      - ads
ports:
  dns: 53 # (3)!
  http: 4000
```

1.  Upstream DNS resolvers blocky forwards allowed queries to. Plain IPs as well as `tcp-tls:` (DoT) and `https://` (DoH) endpoints are supported.
2.  A named denylist group. Each entry is a URL or local file with hosts/domains to block; reference it from `clientGroupsBlock`.
3.  Listening ports — `dns` serves DNS (UDP/TCP), `http` serves the REST API and Prometheus metrics.

## Run Blocky

=== "Standalone binary"

    Download the binary file from [GitHub](https://github.com/0xERR0R/blocky/releases) for your architecture and
    run `./blocky --config config.yml`.

    !!! warning

        To bind a **privileged port (< 1024, e.g. 53 or 853)** as a non-root user on
        Linux, the binary needs the `NET_BIND_SERVICE` capability. Add it with
        `setcap 'cap_net_bind_service=+ep' ./blocky`, run as root (not recommended),
        or configure a port >= 1024.

        If Blocky runs under a **restricted capability bounding set** (for example a
        hardened `systemd` unit, or a container that drops capabilities), use
        `setcap 'cap_net_bind_service=+p' ./blocky` instead. Blocky raises the
        capability to effective itself at startup, which avoids the
        `operation not permitted` exec error that `+ep` triggers when the capability
        is not in the bounding set.

=== "Docker"

    !!! note "Running under a restricted runtime"

        The container image is capability-aware. The binary ships with
        `cap_net_bind_service=+p` (permitted only), so the container starts even under
        a restricted runtime that drops all capabilities (for example the Kubernetes
        *Restricted* Pod Security Standard, `capabilities: drop: [ALL]`).

        - On **ports >= 1024** no capability is required.
        - On **privileged ports (< 1024, e.g. 53/853)** Blocky needs `NET_BIND_SERVICE`.
          With the default runtime capability set it is already present and Blocky
          enables it automatically. Under `drop: [ALL]`, grant it back (Kubernetes
          `securityContext.capabilities.add: ["NET_BIND_SERVICE"]`, or
          `docker run --cap-add NET_BIND_SERVICE`) or configure a port >= 1024.

    !!! note "Writable mounts and file permissions"

        The container runs as the unprivileged user **UID 100**. Any directory Blocky
        writes to - the query log target, or `loading.downloads.cachePath` - must be
        writable by that user, otherwise Blocky logs a `permission denied` error and
        continues without writing.

        The image ships `/app/cache` and `/logs` pre-created and owned by UID 100, so
        mounting a **named volume** over either path works with no host-side setup:

        ```sh
        docker run -v blocky_cache:/app/cache ... spx01/blocky
        ```

        A **bind mount** always keeps the host directory's ownership, so it needs one of:

        - `chown -R 100:100 /path/on/host` before starting the container, or
        - run as the host user that owns the directory:
          `docker run -u "$(id -u):$(id -g)" ...`, or in compose
          `user: "1000:1000"`.

        Read-only mounts such as `config.yml` are unaffected.

    Blocky docker images are deployed to DockerHub (`spx01/blocky`) and GitHub Container Registry (`ghcr.io/0xerr0r/blocky`).

    === "docker run"

        Execute the following command from the command line:

        ```sh
        docker run --name blocky -v /path/to/config.yml:/app/config.yml -p 4000:4000 -p 53:53/udp spx01/blocky
        ```

    === "docker-compose"

        Create the following `docker-compose.yml` file:

        ```yaml
        version: "2.1"
        services:
          blocky:
            image: spx01/blocky
            container_name: blocky
            restart: unless-stopped
            # Optional the instance hostname for logging purpose
            hostname: blocky-hostname
            ports:
              - "53:53/tcp"
              - "53:53/udp"
              - "4000:4000/tcp"
            environment:
              - TZ=Europe/Berlin # Optional to synchronize the log timestamp with host
            volumes:
              # Optional to synchronize the log timestamp with host
              - /etc/localtime:/etc/localtime:ro
              # config file
              - ./config.yml:/app/config.yml:ro
        ```

        Then start the container with:

        ```sh
        docker-compose up -d
        ```

## Container configuration file

You can define the location of the config file in the container with environment variable `BLOCKY_CONFIG_FILE`.
Default value is `/app/config.yml`.

!!! note "Legacy: `CONFIG_FILE`"

    Older Blocky versions used `CONFIG_FILE` instead of
    `BLOCKY_CONFIG_FILE`. The legacy name is still accepted as a
    fallback when `BLOCKY_CONFIG_FILE` is unset. New deployments
    should use `BLOCKY_CONFIG_FILE`.

## Advanced docker-compose setup

Following example shows, how to run blocky in a docker container and store query logs on a SAMBA share. Local black and
allowlists directories are mounted as volume. You can create own black or allowlists in these directories and define the
path like '/app/allowlists/allowlist.txt' in the config file.

!!! example

    ```yaml
    version: "2.1"
    services:
      blocky:
        image: spx01/blocky
        container_name: blocky
        restart: unless-stopped
        ports:
          - "53:53/tcp"
          - "53:53/udp"
          - "4000:4000/tcp" # Prometheus stats (if enabled)
        environment:
          - TZ=Europe/Berlin
        volumes:
          # config file
          - ./config.yml:/app/config.yml:ro
          # write query logs in this volume
          - queryLogs:/logs
          # put your custom allow/denylists in these directories
          - ./denylists:/app/denylists/
          - ./allowlists:/app/allowlists/

    volumes:
      queryLogs:
        driver: local
        driver_opts:
          type: cifs
          # uid=100 is required: the share is mounted root-owned by default and
          # Blocky runs as UID 100, which could then not write the query logs
          o: username=USER,password=PASSWORD,uid=100,gid=100,rw
          device: //NAS_HOSTNAME/blocky
    ```

    !!! note

        The `./denylists` and `./allowlists` bind mounts above are only read by Blocky,
        so they need no ownership changes. See the permission note under
        [Run Blocky](#run-blocky) if you add a writable bind mount.

## Multiple configuration files

For complex setups, splitting the configuration between multiple YAML files might be desired. In this case, folder containing YAML files is passed on startup, Blocky will join all the files.

```sh
./blocky --config ./config/
```

!!! warning

    Blocky simply joins the multiple YAML files. If an option (e.g. `upstream`) is present in multiple files, the configuration will not load and start will fail.

## Other installation types

!!! warning

    These projects are not associated with Blocky devs and are listed here for convenience.

### Arch Linux via AUR

See [https://aur.archlinux.org/packages/blocky/](https://aur.archlinux.org/packages/blocky/)

### Alpine Linux

See [https://pkgs.alpinelinux.org/packages?name=blocky&branch=edge&repo=&arch=](https://pkgs.alpinelinux.org/packages?name=blocky&branch=edge&repo=&arch=)

### CentOS/Debian/Fedora install script

See [https://github.com/m0zgen/blocky-installer](https://github.com/m0zgen/blocky-installer)

### FreeBSD

See [https://www.freebsd.org/cgi/ports.cgi?query=blocky&stype=all](https://www.freebsd.org/cgi/ports.cgi?query=blocky&stype=all)

### Gentoo

See the [Gentoo Wiki](https://wiki.gentoo.org/wiki/Project:GURU/Information_for_End_Users) to enable the GURU repository, then run `emerge net-dns/blocky`.

### NixOS

Add `pkgs.blocky` as a module:

```nix
services.blocky = {
  enable = true;

  settings = {
    # anything from config.yml
  };
};
```

### macOS via Homebrew

See [https://formulae.brew.sh/formula/blocky](https://formulae.brew.sh/formula/blocky)

### TrueNAS SCALE via TrueCharts

See [https://truecharts.org/charts/enterprise/blocky/](https://truecharts.org/charts/enterprise/blocky/)
(TrueCharts is not an official TrueNAS project)

## Companion projects

!!! warning

    These projects are not associated with Blocky devs and are listed here for convenience.

### Lists updater

[Blocky lists updater](https://github.com/shizunge/blocky-lists-updater) updates list related configuration without restarting blocky DNS.

### Web UIs

- [Blocky Frontend](https://github.com/Mozart409/blocky-frontend) provides a Web UI to control blocky.
See linked project for installation instructions.

- [BlockyUI](https://github.com/GabeDuarteM/blocky-ui) provides a fully featured and modern Web UI for managing your Blocky DNS server.

- [Blocky Visor](https://github.com/JCHHeilmann/blocky-visor) is a static SPA dashboard with live metrics and DNS query testing. Comes with an optional Go sidecar that adds historical analytics based on parsing Blocky's log files, log viewing, and config editing.

---

## File: docs/interfaces.md

# Interfaces

## REST API


??? abstract "OpenAPI specification"

    ```yaml
    --8<-- "api/openapi.yaml"
    ```

If http listener is enabled, blocky provides REST API. You can download the [OpenAPI YAML](api/openapi.yaml) interface specification. 

You can also browse the interactive API documentation (RapiDoc) documentation [online](rapidoc.html).

### Common endpoints

| Method | Path                  | Purpose                                              |
| ------ | --------------------- | ---------------------------------------------------- |
| GET    | `/api/blocking/enable`  | Enable blocking globally.                          |
| GET    | `/api/blocking/disable` | Disable blocking globally (optional `duration`, `groups` query params). |
| GET    | `/api/blocking/status`  | Return current blocking status as JSON.            |
| POST   | `/api/lists/refresh`    | Refresh all allow/denylists.                       |
| POST   | `/api/cache/flush`      | Clear the entire DNS response cache.               |
| POST   | `/api/query`            | Run a DNS query through Blocky and return the result as JSON. |
| GET    | `/api/stats`            | In-memory DNS statistics over a rolling 24h window as JSON. Requires [statistics](configuration.md#statistics) to be enabled; returns `503` otherwise. |

!!! example "Flush the DNS cache"

    ```sh
    curl -X POST http://:<http-port>/api/cache/flush
    ```

    Returns HTTP `200` on success. Useful after editing `customDNS`
    or `hostsFile` entries that may already be cached.

!!! note "Statistics semantics"

    For `/api/stats`, the `summary` fields are server-computed categories, so callers never
    interpret a raw response type:

    | Field      | Response types                                        |
    | ---------- | ----------------------------------------------------- |
    | `blocked`  | `BLOCKED` + `REBIND`                                  |
    | `filtered` | `FILTERED` + `NOTFQDN`                                |
    | `forwarded`| `RESOLVED` + `CONDITIONAL`                            |
    | `cached`   | `CACHED`                                              |
    | `local`    | `CUSTOMDNS` + `HOSTSFILE` + `SPECIAL` + `SYNTHESIZED` |
    | `errors`   | `BOGUS`, plus queries a resolver failed outright      |

    `blocked` counts only queries blocked to protect the client: denylist hits and
    [DNS rebinding](configuration.md#dns-rebinding-protection) hits. Two other outcomes are
    deliberately kept out of it, so they cannot inflate `blocked` or the `topBlockedDomains`
    list:

    - Query-type filtering (e.g. `AAAA` via `filtering.queryTypes`) and non-FQDN rejections are
      client-requested, not protective, and are counted as `filtered`.
    - A [DNSSEC](configuration.md#dnssec) validation failure (`BOGUS`) is a SERVFAIL — blocky
      could not obtain a trustworthy answer — so it is a resolution error, not a block, and is
      counted as `errors` together with queries a resolver failed outright.

    The `blocked` / `filtered` split also applies to the `perHour` series, which carries both.
    Together with `dropped` (rate-limited), the categories above partition every query, so they
    always add up to `queries`.

    The `lists` and `cache` objects are point-in-time gauges
    (current values, not affected by the 24h window), while `start`/`end` bound the windowed fields
    only. All timestamps (`start`, `end`, `perHour[].hour`) are always returned in UTC (RFC 3339,
    `Z` suffix), regardless of the server's local time zone. Statistics are independent of Prometheus
    and work with plain JSON.

## CLI

Blocky provides a CLI interface to control. This interface uses internally the REST API.

To run the CLI, please ensure, that blocky DNS server is running, then execute `blocky help` for help or

- `./blocky blocking enable` to enable blocking
- `./blocky blocking disable` to disable blocking
- `./blocky blocking disable --duration [duration]` to disable blocking for a certain amount of time (30s, 5m, 10m30s,
  ...)
- `./blocky blocking disable --groups ads,othergroup` to disable blocking only for special groups
- `./blocky blocking status` to print current status of blocking
- `./blocky query <domain>` execute DNS query (A) (simple replacement for dig, useful for debug purposes)
- `./blocky query <domain> --type <queryType>` execute DNS query with passed query type (A, AAAA, MX, ...)
- `./blocky lists refresh` reloads all allow/denylists
- `./blocky stats` shows DNS statistics (requires `statistics.enable: true`)
- `./blocky validate [--config /path/to/config.yaml]` validates configuration file

!!! tip 

    To run this inside docker run `docker exec blocky ./blocky blocking status`

---

## File: docs/network_configuration.md

# Network configuration

In order, to benefit from all the advantages of blocky like ad-blocking, privacy and speed, it is necessary to use
blocky as DNS server for your devices. You can configure DNS server on each device manually or use DHCP in your network
router and push the right settings to your device. With this approach, you will configure blocky only once in your
router and each device in your network will automatically use blocky as DNS server.

## Transparent configuration with DHCP

Let us assume, blocky is installed on a Raspberry PI with fix IP address `192.168.178.2`. Each device which connects to
the router will obtain an IP address and receive the network configuration. The IP address of the Raspberry PI should be
pushed to the device as DNS server.

```
┌──────────────┐         ┌─────────────────┐
│              │         │ Raspberry PI    │
│  Router      │         │   blocky        │        
│              │         │ 192.168.178.2   │            
└─▲─────┬──────┘         └────▲────────────┘        
  │1    │                     │  3                  
  │     │                     │                         
  │     │                     │ 
  │     │                     │                     
  │     │                     │
  │     │                     │
  │     │                     │
  │     │       ┌─────────────┴──────┐
  │     │   2   │                    │
  │     └───────►  Network device    │
  │             │    Android         │
  └─────────────┤                    │
                └────────────────────┘
```

**1** - Network device asks the DHCP server (on Router) for the network configuration

**2** - Router assigns a free IP address to the device and says "Use 192.168.178.2" as DNS server

**3** - Clients makes DNS queries and is happy to use **blocky** :smile:

!!! warning

    It is necessary to assign the server which runs blocky (e.g. Raspberry PI) a fix IP address.

### Example configuration with FritzBox

To configure the DNS server in the FritzBox, please open in the FritzBox web interface:

* in navigation menu on the left side: Home Network -> Network
* Network Settings tab on the top
* "IPv4 Configuration" Button at the bottom op the page
* Enter the IP address of blocky under "Local DNS server", see screenshot



## Running blocky behind a reverse proxy

When Blocky's HTTP / DoH listener is fronted by a reverse proxy
(nginx, Traefik, Caddy, HAProxy, ...), the TCP peer Blocky sees is
the proxy, not the real client. To preserve per-client behavior
(client groups, query logging, custom DNS rules), Blocky inspects
forwarding headers in this fixed precedence order:

1. **`Forwarded`** (RFC 7239) — the standardized header. The first
   `for=` parameter wins. Both `for=192.0.2.43` and
   `for="[2001:db8::1]:8080"` are accepted.
2. **`X-Forwarded-For`** — the de facto standard. The leftmost IP in
   the comma-separated list wins (per convention, it is the original
   client).
3. **`RemoteAddr`** — direct TCP peer; used when no forwarding header
   is present.

The first header found wins; later headers are ignored. So if your
proxy sets both `Forwarded` and `X-Forwarded-For`, Blocky uses
`Forwarded`.

!!! warning

    Blocky uses `Forwarded` / `X-Forwarded-For` whenever those
    headers are present; this is not a separate feature you can
    enable or disable. If Blocky is reachable directly by clients,
    they can send these headers themselves and spoof their identity.
    Ensure Blocky is only reachable through a trusted reverse proxy,
    or that the proxy strips any incoming `Forwarded` /
    `X-Forwarded-For` headers and overwrites them with trusted values.

!!! example "nginx"

    ```nginx
    location /dns-query {
        proxy_pass http://blocky-backend:4000/dns-query;
        proxy_set_header X-Forwarded-For $remote_addr;
    }
    ```

!!! example "Traefik (dynamic file config)"

    ```yaml
    http:
      services:
        blocky:
          loadBalancer:
            servers:
              - url: "http://blocky-backend:4000"
      middlewares: {}
      # Traefik sets X-Forwarded-For automatically; no extra config needed.
    ```

## Running DoT/DoH behind a TCP proxy

For DNS-over-TLS and HTTPS passthrough, HTTP forwarding headers are not
available before Blocky handles the TLS connection. Enable the HAProxy PROXY
protocol on the Blocky listener and configure the proxy to send it.

!!! warning

    List a listener under `ports.proxyProtocol` only when it is reachable only
    from trusted proxies. When enabled, Blocky requires a PROXY protocol header
    and uses the source address from that header as the client IP.

    The PROXY protocol only covers TCP listeners. HTTP/3 (QUIC/UDP) cannot carry
    a PROXY protocol header, so enabling `https` here automatically disables
    HTTP/3. Plain DNS-over-UDP is likewise unaffected by `dns`.

    The `http` and `https` listeners also serve the Prometheus metrics, the REST
    API and the `/debug` pprof endpoints. Enabling `http`/`https` here makes *all*
    of them require a PROXY protocol header, so anything that connects to that
    port directly (e.g. a Prometheus scrape not routed through the proxy) is
    rejected. Bind those tools behind the same trusted proxy, or expose metrics
    and API on a separate listener without PROXY protocol.

!!! example "Blocky"

    ```yaml
    ports:
      https: 443
      tls: 853
      proxyProtocol:
        - https
        - tls
    ```

!!! example "nginx stream"

    ```nginx
    stream {
        upstream blocky_dot {
            server blocky-backend:853;
        }

        server {
            listen 853;
            proxy_pass blocky_dot;
            proxy_protocol on;
        }

        upstream blocky_doh {
            server blocky-backend:443;
        }

        server {
            listen 443;
            proxy_pass blocky_doh;
            proxy_protocol on;
        }
    }
    ```

---

## File: docs/prometheus_grafana.md

# Integration in Grafana

## Prometheus

### Prometheus export

Blocky can optionally export metrics for [Prometheus](https://prometheus.io/).

Following metrics will be exported:

| name                                             |   Description                                            |
| ------------------------------------------------ | -------------------------------------------------------- |
| blocky_build_info                                | Version number and build info                            |
| blocky_denylist_cache_entries                    | Gauge of entries in the denylist cache, partitioned by group |
| blocky_allowlist_cache_entries                   | Gauge of entries in the allowlist cache, partitioned by group |
| blocky_error_total                               | Counter of total queries that ended in error for any reason |
| blocky_query_total                               | Counter of total queries, partitioned by client and DNS request type (A, AAAA, PTR, etc) |
| blocky_request_duration_seconds                  | Histogram of request duration, partitioned by response type (Blocked, cached, etc)  |
| blocky_response_total                            | Counter of responses, partitioned by response type (Blocked, cached, etc), DNS response code, and reason |
| blocky_blocking_enabled                          | Boolean 1 if blocking is enabled, 0 otherwise |
| blocky_cache_entries                             | Gauge of entries in cache |
| blocky_cache_hits_total                          | Counter of the number of cache hits |
| blocky_cache_misses_total                        | Counter of the number of Cache misses |
| blocky_last_list_group_refresh_timestamp_seconds | Timestamp of last list refresh |
| blocky_prefetches_total                          | Counter of prefetched DNS responses |
| blocky_prefetch_hits_total                       | Counter of requests that hit the prefetch cache |
| blocky_prefetch_domain_name_cache_entries        | Gauge of domain names being prefetched |
| blocky_failed_downloads_total                    | Counter of failed list downloads |
| blocky_dnssec_validation_total                   | Counter of DNSSEC validations, partitioned by result (secure, insecure, bogus, indeterminate) |
| blocky_dnssec_cache_hits_total                   | Counter of DNSSEC validation cache hits |
| blocky_dnssec_validation_duration_seconds        | Histogram of DNSSEC validation duration, partitioned by result |
| blocky_redis_cache_buffer_drops_total            | Counter of cache writes dropped because the Redis write-through buffer is full — non-zero values indicate Redis cannot keep up with cache writes |
| blocky_rate_limit_drops_total                    | Counter of queries dropped by the rate limiter, partitioned by protocol |
| blocky_rate_limit_cap_exhausted_total            | Counter of queries dropped because the rate limiter bucket store was full |
| blocky_rate_limit_active_buckets                 | Gauge of token buckets (≈ distinct clients) currently tracked by the rate limiter |
| blocky_dnstap_frames_dropped_total               | Counter of dnstap frames dropped because the internal buffer is full - non-zero values indicate the collector is slow or unreachable |

!!! note "`reason` label for blocked responses"

    To keep the `reason` label of `blocky_response_total` bounded, blocked responses use the matched
    group names only (e.g. `BLOCKED (ads)`), **not** the matched rule. The full reason including the
    matched rule (e.g. `BLOCKED (ads: *.docler.com)`) is still available in the [query log](configuration.md#query-log).
    This avoids unbounded metric cardinality when large deny lists are used.

### Grafana dashboard

Example [Grafana](https://grafana.com/) dashboard
definition [as JSON](blocky-grafana.json)
or [at grafana.com](https://grafana.com/grafana/dashboards/13768)
.

The dashboard is organized in sections (overview, traffic, latency, blocking & lists, cache & prefetching, DNSSEC,
rate limiting, Go runtime) and uses only Grafana core panels, so no additional plugins are needed. The "Blocking
control" buttons in the overview section enable or temporarily disable blocking via the blocky API.

When importing the dashboard, set the "blocky API URL" input to the address under which your browser can reach the
blocky HTTP API (e.g. `https://blocky.example.com` or `http://192.168.1.2:4000`) — it is used by the blocking
control buttons.

### Requirements

- Grafana 10.2 or newer: the blocking control buttons use canvas button elements with API calls. All other panels
  also work with older Grafana versions.
- blocky newer than v0.31 if Grafana is served from a different origin than the blocky API: older blocky versions
  reject the CORS preflight which Grafana sends for the blocking control buttons. Alternatively, expose the blocky
  API on the same origin as Grafana through your reverse proxy.

### Grafana and Prometheus example project

This [repo](https://github.com/0xERR0R/blocky-grafana-prometheus-example) contains example docker-compose.yml with
blocky, prometheus (with configured scraper for blocky) and grafana with prometheus datasource.

## MySQL / MariaDB

If database query logging is activated (see [Query logging](configuration.md#query-logging)), you can use following
Grafana Dashboard [as JSON](blocky-query-grafana.json)
or [at grafana.com](https://grafana.com/grafana/dashboards/14980)

.

Please define the MySQL source in Grafana, which points to the database with blocky's log entries.

## Postgres

The JSON for a Grafana dashboard equivalent to the MySQL/MariaDB version is located [here](blocky-query-grafana-postgres.json)