Metadata-Version: 2.4
Name: foghorn
Version: 0.7.0
Summary: Foghorn is a fast, pluggable DNS server that caches, filters, routes, and bends DNS to your will. Up- and down-stream UDP, TCP, DoT, and DoH. DNSEC. Because DNS should do what you want.
Author-email: Zack Allison <zack@zackallison.com>
License: MIT License
        
        Copyright (c) 2025 Zack Allison
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE.txt
Requires-Dist: cachetools
Requires-Dist: dnslib>=0.9.24
Requires-Dist: jsonschema>=4.17.3
Requires-Dist: pydantic
Requires-Dist: pyyaml>=6.0.1
Requires-Dist: requests>=2.31.0
Requires-Dist: cryptography
Requires-Dist: dnspython>=2.6.1
Requires-Dist: fastapi>=0.111.0
Requires-Dist: psutil
Requires-Dist: python-multipart
Requires-Dist: uvicorn>=0.30.0
Requires-Dist: paramiko
Requires-Dist: docker>=7.0.0
Requires-Dist: publicsuffix2
Requires-Dist: watchdog
Requires-Dist: zeroconf
Requires-Dist: mariadb
Requires-Dist: mysql-connector-python
Requires-Dist: paho-mqtt
Requires-Dist: psycopg2
Requires-Dist: pymemcache
Requires-Dist: pymongo
Requires-Dist: redis
Provides-Extra: example-plugins
Requires-Dist: whois; extra == "example-plugins"
Provides-Extra: dev
Requires-Dist: black; extra == "dev"
Requires-Dist: build; extra == "dev"
Requires-Dist: coverage; extra == "dev"
Requires-Dist: httpx; extra == "dev"
Requires-Dist: isort; extra == "dev"
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Requires-Dist: twine; extra == "dev"
Dynamic: license-file

# Foghorn Configuration & Operations

Foghorn is a versatile DNS server designed for flexibility and performance. Built on a robust caching forwarder or recursive resolver foundation, it seamlessly integrates with YAML-based configuration and modular plugins to transform into a powerful ad-blocker, local hosts service, kid-safe filter. Fine-tune its capabilities with a Redis backend, InfluxDB logging, and customizable function-cache sizes tailored to your specific needs.

With built-in admin and API server support, Foghorn empowers you to monitor and manage its operations efficiently. Plugins extend its functionality by providing their own status pages, seamlessly integrated into the admin dashboard. Newer releases add DNSSEC signing helpers, zone transfers (AXFR/IXFR), RFC 8914 Extended DNS Errors (EDE), and SSH host key utilities so you can treat DNS as a first-class security and operations tool.

[![Python Tests](https://github.com/zallison/foghorn/actions/workflows/pytest.yml/badge.svg)](https://github.com/zallison/foghorn/actions/workflows/pytest.yml) ![Test Coverage](https://img.shields.io/badge/test_coverage-89%25-blue) [![Docker Pulls](https://img.shields.io/docker/pulls/zallison/foghorn)](https://hub.docker.com/r/zallison/foghorn/)  [![PyPI Downloads](https://static.pepy.tech/personalized-badge/foghorn?period=total&units=INTERNATIONAL_SYSTEM&left_color=GRAY&right_color=BLUE&left_text=downloads)](https://pepy.tech/projects/foghorn)  [![BuyMeACoffee](https://raw.githubusercontent.com/pachadotdev/buymeacoffee-badges/main/bmc-blue.svg)](https://www.buymeacoffee.com/foghorndns)

<img src="https://raw.githubusercontent.com/zallison/foghorn/refs/heads/main/assets/screenshot-1.png" height="300" /> <img src="https://raw.githubusercontent.com/zallison/foghorn/refs/heads/main/assets/screenshot-2.png" height="300" /> <img src="https://raw.githubusercontent.com/zallison/foghorn/refs/heads/main/assets/screenshot-3.png" height="300" /> <img src="https://raw.githubusercontent.com/zallison/foghorn/refs/heads/main/assets/screenshot-4.png" height="300" />

## Foghorn DNS Server

### Overview
- **Foghorn** is a DNS server that lets you do almost anything.
- Nearly *everything* is configurable, tunable, and observable via the admin Web UI, even down to Python function cache settings.
- Plugins and the resolver do most of the heavy lifting.
- Out of the box, with no plugins enabled, it behaves like a standard DNS server with:
  - UDP / TCP
  - DoT (DNS over TLS)
  - DoH (DNS over HTTPS)
  - DNSSEC support for secure DNS
  - DNS
- Enabling TLS is straightforward. The `Makefile` includes targets to generate a CA and sign keys.

---

### Query Pipeline
- Queries flow through a pipeline of plugins. A plugin can appear multiple times with different configurations.
- Plugins execute in priority order. The first plugin that produces a final answer immediately short-circuits the pipeline.
- If no pre-resolve plugin responds, the configured resolver, either forwarder or recursive, runs.
- The result then flows through a post-resolve pipeline, if configured, sent to the client, then added to the logging queue.

---

### Plugins
Key plugins include:

- **[ACL](docs/plugins/resolve/access_control.md)**: Access control
- **[EtcHosts](docs/plugins/resolve/etc_hosts.md)**: Serve a hosts-file-style set of A/AAAA records via DNS. Ideal for small, simple setups.
- **[FileDownloader](docs/plugins/resolve/file_downloader.md) + [Filter](docs/plugins/resolve/filter.md)**: Download blocklists and filter queries, similar to Pi-hole. Can return:
  - An IP
  - `REFUSED`
  - `SERVFAIL`
  - Or silently drop the connection
- **[ZoneRecords](docs/plugins/resolve/zone_records.md)**: Load BIND9 zone files and/or define arbitrary records without creating a full zone. Supports combining multiple files and enabling DNSSEC.
- **[UpstreamRouter](docs/plugins/resolve/upstream_router.md)**: Route queries to different upstreams based on name, for example forwarding `.corp` to a VPN resolver.
- Additional plugins for:
  - [Rate limiting](docs/plugins/resolve/rate_limit.md) - Static or Dynamic
  - [Docker host discovery](docs/plugins/resolve/docker_hosts.md) - Add containers to DNS
  - [Zeroconf / mDNS / Bonjour](docs/plugins/resolve/mdns_bridge.md) - Forward mDNS over DNS, also the admin UI offers observability.
  - [Simulating unreliable upstreams](docs/plugins/resolve/flaky_server.md) for development, including on-the-wire fuzzing. Seedable for testing purposes.

Some plugins ship with configuration *profiles* (preset bundles) stored as YAML in
`src/foghorn/plugins/resolve/*_profiles.yaml` (for example `rate_limit_profiles.yaml`).

"Targets" let you limit the scope of the plugin to given client IPs, domain names, query types, listener type, and more.

Creating new plugins is simple. You can implement custom DNS logic without writing an entire DNS server. For example, the “finger-over-dns” example plugin can be built in under an hour.

---

### Highly Customizable
- Fine-grained controls let you tune behavior precisely.
- Resize and configure the Python function cache to match your workload.
- Variables allow multiple servers to share the same configuration with small differences, such as listen address.
- Environment variables make it easy to adjust behavior in CI/CD environments.

---

### Fits Into Your Infrastructure
- Flexible caching backends:
  - In-memory
  - SQL databases
  - Valkey or Redis
  - MongoDB
- Logging integrates with your existing systems, can log to multiple targets.:
  - File-based logging
  - SQL databases
  - InfluxDB
  - MQTT

---

# Full Documentation

## Table of Contents
- [0. Thanks](#0-thanks)
- [1. Quick Start](#1-quick-start)
  - [1.1 Installation](#11-install)
  - [1.2 Quick Config](#12-quick-config)
- [2. Configuration layout overview](#2-configuration-layout-overview)
  - [2.1 Top-level keys](#21-top-level-keys)
  - [2.2 Server block](#22-server-block)
  - [2.3 Upstreams block](#23-upstreams-block)
  - [2.4 Logging](#24-logging)
  - [2.5 Stats and query log](#25-stats-and-query-log)
  - [2.6 Plugins](#26-plugins)
- [3. Listeners and upstreams by example](#3-listeners-and-upstreams-by-example)
  - [3.1 UDP/TCP listener](#31-udptcp-listener)
  - [3.2 DNS-over-TLS (DoT) upstream](#32-dns-over-tls-dot-upstream)
  - [3.3 DNS-over-HTTPS (DoH) listener with TLS](#33-dns-over-https-doh-listener-with-tls)
  - [3.4 DoH listener behind an HTTP reverse proxy](#34-doh-listener-behind-an-http-reverse-proxy)
- [4. Plugin cookbook](#4-plugin-cookbook)
  - [4.1 Access control (acl)](#41-access-control-acl)
  - [4.2 Docker containers (docker)](#42-docker-containers-docker)
  - [4.3 Hosts files (hosts)](#43-hosts-files-hosts)
  - [4.4 List downloader (lists)](#44-list-downloader-lists)
  - [4.5 Domain filter / adblock (filter)](#45-domain-filter--adblock-filter)
  - [4.6 Flaky upstream simulator (flaky)](#46-flaky-upstream-simulator-flaky)
  - [4.7 mDNS / Bonjour bridge (mdns)](#47-mdns--bonjour-bridge-mdns)
  - [4.8 Rate limiting (rate)](#48-rate-limiting-rate)
  - [4.9 Per-domain upstream routing (router)](#49-per-domain-upstream-routing-router)
  - [4.10 Inline and file-based records (zone)](#410-inline-and-file-based-records-zone)
- [5. Example Plugins](#5-example-plugins)
  - [5.1 DNS prefetch (prefetch)](#51-dns-prefetch-prefetch)
  - [5.2 Example rewrites (examples)](#52-example-rewrites-examples)
  - [5.3 Greylist new names (greylist_example)](#53-greylist-new-names-greylist_example)
  - [5.4 New-domain WHOIS filter (new_domain)](#54-new-domain-whois-filter-new_domain)
  - [5.5 File over DNS (file_over_dns)](#55-file-over-dns-file_over_dns)
  - [5.6 Finger over DNS (finger)](#56-finger-over-dns-finger)
- [6. Variables](#6-variables)
- [7. Sample configurations](#7-sample-configurations)
  - [7.1 `local`: workstation config](#71-local-workstation-config)
  - [7.2 `lan`: home LAN with adblock and kid filter](#72-lan-home-lan-with-adblock-and-kid-filter)
  - [7.3 `smb`: small business](#73-smb-small-business)
  - [7.4 `enterprise`: layered caches and rich stats](#74-enterprise-layered-caches-and-rich-stats)

## Additonal Documentation

- [Developer notes and contribution guide](docs/README-DEV.md)
- [Makefile targets and build helpers](docs/MAKEFILE.md)
- [Pi-hole replacement example configuration](docs/PiholeConfig.md)
- [Query-log hardening and sampling](docs/query-log-hardening.md)
- [OpenSSL make targets (certificate helpers)](docs/open-ssl-make-easy.md)
- [DNS RFC compliance, EDNS/EDE, and AXFR/IXFR notes](docs/RFCs.md)
- [SSH host keys, SSHFP records, and DNSSEC integration](docs/openssh-key-records.md)


---

## 0. Thanks

With special thanks to **Fiona** Weatherwax for their contributions and inspiration, to the **dnslib** team for the low level / on wire primitives, and to **dnspython** for the DNSSEC implementation. Additional shout outs to the whole **python community**, and the teams of **fastapi, pydantic, black, ruff, pytest,** and every other giant on whose shoulders I stand.

Also thanks to my junior developers, AI from both local and remote models, some via warp.dev, who keeps my docstrings and unit tests up to date, creates good commit messages, and other janitorial tasks. Also ~~a lot of help with the~~ all the HTML/JS. Because I'm just not good at it.

## 1. Quick Start

## 1.1 Install

Foghorn can be installed a few different ways, depending on how you prefer to run services:

•  From PyPI (recommended for most users)
```bash
  Install the latest released version into your Python environment:
  pip install foghorn
  This gives you the foghorn CLI and library directly on your host system.
```

### Minimal / headless installs (optional dependencies)

Foghorn can run as a small UDP/TCP DNS server with lightweight plugins (for example
`hosts` / `EtcHosts`) without enabling DNSSEC validation, DoH, or the admin HTTP UI.

However, some features depend on optional third-party packages. If you remove those
packages from your image/environment:

- **DNSSEC local validation** (`server.dnssec.mode: validate` with
  `server.dnssec.validation: local|local_extended`) requires **dnspython** and
  **cryptography**. If enabled but missing, Foghorn will exit with an error.
- **Admin HTTP UI** (`server.http`) and **DoH listener** (`server.listen.doh.enabled: true`)
  require **fastapi** (and the DoH path uses **uvicorn**). If enabled but missing,
  Foghorn will exit with an error.
- **Plugins** may require extra dependencies. During startup, plugins that fail to
  import due to missing dependencies are **skipped by default**.
  To make a plugin import failure fatal, set `abort_on_failure: true` inside that
  plugin's `config` block.

Example (require `ssh_keys` plugin deps at startup):
```yaml
plugins:
  - type: ssh_keys
	config:
	  abort_on_failure: true
```

Developer strict mode: to make *plugin discovery* itself strict (raise on ImportError
while scanning plugin modules), set `FOGHORN_STRICT_PLUGIN_DISCOVERY=1`.

•  From source (GitHub)
  If you want to track development, hack on plugins, or run a specific commit/branch, clone the repository and install it in editable mode:
  ```bash
  git clone https://github.com/zallison/foghorn.git
  cd foghorn
  pip install -e .
  ```
  This keeps your local checkout and installed code in sync as you make changes.

•  Prebuilt Docker images (amd64 and armhf)
  If you prefer to run Foghorn in a container, prebuilt images for both amd64 and armhf are available on Docker Hub at https://hub.docker.com/r/zallison/foghorn.
  Pull the image for your architecture and run it with your configuration mounted as /foghorn/config.yaml, along with any port mappings you need for DNS, DoT/DoH, and the admin web UI.

   ```bash
	docker run --name foghorn -v ./config/:/foghorn/config/ -p 53:53/udp -p 53:53/tcp -p 5380:5380 --privileged zallison/foghorn:latest
   ```

## 1.2 Quick config

This example listens on all interfaces for UDP/TCP DNS and forwards to a public DoT resolver. It also enables a simple in-memory cache.

```yaml
# config/config.yaml

vars:
  ENV: prod

server:
  listen:
	dns:
	  udp:
		enabled: true
		host: 0.0.0.0
		port: 53
	  tcp:
		enabled: true
		host: 0.0.0.0
		port: 53
  cache:
	module: memory  # memory | sqlite | redis | memcached | mysql | mariadb | postgres | mongodb | none

upstreams:
  strategy: failover        # failover | round_robin | random
  max_concurrent: 1         # 1 | 2 | 4 ...
  endpoints:
	- host: 1.1.1.1
	  port: 853
	  transport: dot        # udp | tcp | dot
	  tls:
		server_name: cloudflare-dns.com

plugins: []
```

You can start Foghorn with:

```bash
foghorn --config config/config.yaml
```

From here you layer in plugins to get adblocking, hosts files, per-user allowlists, and more.

### Makefile helpers

For local development there is a `Makefile` with a few convenience targets:

- `make run` – create a venv if needed and start Foghorn with `config/config.yaml`.
- `make env` / `make env-dev` – create the virtualenv in `./venv` and install dependencies (with dev extras for `env-dev`).
- `make build` – prepare the development environment (keeps the JSON schema up to date).
- `make schema` – regenerate `assets/config-schema.json` from the Python code.
- `make ui-bundle` – build a single JavaScript admin UI bundle with embedded HTML/CSS/JS at `dist/foghorn-admin-ui.cdn.js`.
- `make ui-bundle-runtime` – build a runtime-only JavaScript admin UI bundle at `dist/foghorn-admin-ui.cdn.js`.
- `make test` – run the test suite with coverage.
- `make dnssec-sign-zone` – sign a BIND-style zone file with DNSSEC using the bundled helper script, writing a signed zone that can be served by the ZoneRecords plugin.
- `make clean` – remove the venv, build artefacts, and temporary files.
- `make docker`, `make docker-build`, `make docker-run`, `make docker-logs`, `make docker-clean`, `make docker-ship` – build and run Docker images/containers.
- `make package-build` / `make package-publish` / `make package-publish-dev` – build and (optionally) publish Python packages.
- `make ssl-cert` – generate a self-signed TLS key and certificate under `./var` using `openssl req -x509`.

### Rendering config diagrams (Graphviz dot)

If you have a `diagram.dot` and want to render it to `diagram.png`:

```bash
dot -Tpng diagram.dot -o diagram.png
```

---

## Additional Documentation

- [OpenSSL make targets (made easy)](docs/open-ssl-make-easy.md)
- [DNS RFC compliance and protocol notes (including EDE and AXFR)](docs/RFCs.md)
- [SSH host keys, SSHFP records, and DNSSEC integration](docs/openssh-key-records.md)

---

## 2. Configuration layout overview

### 2.1 Top-level keys

At the top level the schema defines these keys:

- `vars`: key/value variables for interpolation inside the rest of the file.
- `server`: listener, DNSSEC, resolver, cache, and admin HTTP settings.
- `upstreams`: how outbound DNS queries are sent.
- `logging`: global logging level and outputs.
- `stats`: runtime statistics and query-log persistence.
- `plugins`: the ordered list of plugins that wrap each query.

Conceptually, a request flows like this:

```text
client ---> UDP/TCP/DoH listener
		---> DNS cache (memory, redis, etc) (optional)
		---> plugins (pre_resolve chain)
		---> [maybe upstream DNS calls or recursive resolving]
		---> plugins (post_resolve chain)
		---> response or deny
```

**Note:** when a pre_resolve plugin returns an `override` decision the generated
response is sent immediately and the post_resolve chain is skipped entirely; a
post_resolve `override` short-circuits any later post_resolve plugins for that
query.

### 2.2 Server block

Key parts of `server`:

- `server.listen`
  - `dns.udp` / `dns.tcp`: classic DNS listeners.
  - `dns.dot`: DNS-over-TLS listener.
  - `doh`: DNS-over-HTTPS listener.
- `server.cache`
  - `module`: which cache plugin to use.
  - `config`: plugin-specific cache settings.
	- For the in-memory DNS cache (`module: memory`), the underlying
	  `FoghornTTLCache` supports optional capacity and eviction controls when
	  instantiated from Python code: `maxsize` (positive integer, unbounded when
	  `None` or non-positive) and `eviction_policy` (one of `none`, `lru`,
	  `lfu`, `fifo`, `random`, or `almost_expired`).
  - `modify` / `decorated_overrides` / `func_caches`: optional overrides for
	internal helper caches (functions decorated with `registered_cached`,
	`registered_lru_cache`, `registered_foghorn_ttl`, or
	`registered_sqlite_ttl`). These let you tune TTL and maxsize for specific
	helpers without code changes.
	- Valid `backend` values for `func_caches` entries are:
	  - `ttlcache` (cachetools.TTLCache)
	  - `lfu_cache` (cachetools.LFUCache)
	  - `rr_cache` (cachetools.RRCache)
	  - `fifo_cache` (cachetools.FIFOCache)
	  - `lru_cache` (cachetools.LRUCache)
	  - `foghorn_ttl` (FoghornTTLCache-based helpers via registered_foghorn_ttl)
	  - `sqlite_ttl` (SQLite3TTLCache-based helpers via registered_sqlite_ttl)

Example: increase the TTL and maxsize for a DNSSEC helper cached via
`cachetools.TTLCache`:

```yaml
server:
  cache:
	module: memory
	config:
	  min_cache_ttl: 60

	func_caches:
	  - module: foghorn.dnssec.dnssec_validate
		name: _find_zone_apex_cached
		backend: ttlcache            # ttlcache | lru_cache | foghorn_ttl | sqlite_ttl | lfu_cache | rr_cache
		ttl: 300                     # seconds; applies to TTL-style backends
		maxsize: 2048                # logical max size for this helper cache
		reset_on_ttl_change: true    # clear TTLCache once when ttl changes
```

_Note:_ the `backend` field here selects the **desired cache backend type** for
that helper (matching the "Backend" column in the admin "Decorated caches"
table). For cachetools-backed helpers wrapped with `registered_cached`, Foghorn
rebuilds the function's cache at startup so you can switch between
`ttlcache`, `lfu_cache`, `rr_cache`, `fifo_cache`, and `lru_cache` purely from
configuration.
- `server.dnssec`
  - Mode and DNSSEC validation knobs (e.g., UDP payload size).

#### glibc, `trust-ad`, and the DNSSEC AD bit

On Linux systems that use glibc, applications (including `ssh` when using SSHFP records) only see the DNSSEC AD bit when the resolver is explicitly configured to trust it. If you run Foghorn (or another validating resolver that honors the upstream AD bit) and want glibc clients to accept that AD as trustworthy, point `/etc/resolv.conf` at Foghorn and add `trust-ad` to the options line:

```text
nameserver 127.0.0.1
options edns0 trust-ad
```

Without `trust-ad`, glibc clears the AD flag before handing answers to applications, so tools like OpenSSH will ignore SSHFP records even when Foghorn has validated them.

- `server.enable_ede`
  - Optional toggle for RFC 8914 Extended DNS Errors; when true and the client advertises EDNS(0), Foghorn can attach EDE options to certain policy or upstream-failure responses and surface per-code stats in the admin UI.
- `server.resolver`
  - Timeouts, recursion depth, and resolver mode:
	- `forward` (default): forward to configured `upstreams`.
	- `recursive`: walk from root servers.
	- `master` / `none`: authoritative-only (no forwarding; cache miss -> REFUSED).
- `server.http`
  - Admin web UI listener configuration.

### 2.3 Upstreams block

`upstreams` describes how Foghorn talks to other DNS servers:

- `strategy`: `failover` (try in order), `round_robin`, or `random`.
- `max_concurrent`: maximum simultaneous outstanding upstream queries.
- `endpoints`: list of `upstream_host` definitions.

Notes:
- If an upstream returns `SERVFAIL`, malformed DNS bytes, or a **mismatched response** (TXID/question), Foghorn treats that upstream as failed for that query and continues failover.
- Skip/failover events are logged at **DEBUG**, but de-duplicated *per upstream* until that upstream succeeds again (to avoid log spam).
- To surface these events, set `logging.python.level: debug`.

An `upstream_host` entry:

```yaml
upstreams:
  endpoints:
	- host: 9.9.9.9
	  port: 53        # 853 for DoT
	  transport: udp  # udp | tcp | dot
	  tls:
		server_name: dns.quad9.net
		verify: true  # true | false
```

An `upstream_host` entry: using `DNS-over-HTTP(s)`

```yaml
upstreams:
  endpoints:
	- transport: doh
	  url: https://dns.example.com/dns-query
	  method: POST    # POST | GET
	  tls:
		verify: true  # true | false
		ca_file: /etc/ssl/certs/ca-certificates.crt
```

### 2.4 Logging

`logging` controls both the process-wide Python logger **and** the
statistics/query-log backends.

Python logging (global defaults):

```yaml
logging:
  python:
	level: info       # debug | info | warn | error | critical
	stderr: true      # true | false
	file: ./var/foghorn.log
	syslog: false     # false | true | {address, facility, tag}
```

Plugins can also override logging per-instance via their own `logging` block on
the plugin entry, using the same shape as `logging.python`.

`logging.backends` describes where persistent stats/query-log data is written.
Each entry maps to a statistics backend such as SQLite or MQTT logging:

```yaml
logging:
  async: true          # default async behaviour for stats backends
  query_log_only: false  # false | true
  backends:
	- id: local-log
	  backend: sqlite
	  config:
		db_path: ./config/var/stats.db
		batch_writes: true
	- id: backup-mqtt
	  backend: mqtt_logging
	  config:
		host: mqtt.internal
		port: 1883
		topic: foghorn/query_log
```

### 2.5 Stats and query log

The `stats` section controls runtime statistics behaviour and selects which
logging backend to read from. When `logging.async` is true, writes to
stats/query-log backends are performed by a background worker so request
handling stays fast; setting it to `false` forces synchronous writes.

Important fields include:

- `enabled`: master on/off switch for statistics.
- `source_backend`: which `logging.backends[*].id` (or backend alias) to treat
  as the primary read backend.

Example:

```yaml
logging:
  async: true
  query_log_only: false     # false | true
  backends:
	- id: local-log
	  backend: sqlite
	  config:
		db_path: ./config/var/stats.db
		batch_writes: true

stats:
  enabled: true
  source_backend: local-log
  interval_seconds: 300
  ignore:
	include_in_stats: true # Just don't display them
	ignore_single_host: false
	top_domains_mode: suffix   # exact | suffix
	top_domains:
	  - example.com
```

#### Query-log flood hardening (no code changes)

If query logging is enabled on an exposed resolver, set explicit limits so
flood traffic cannot grow persistence usage without bounds.

Recommended controls:

- `logging.query_log_retention` global defaults:
  - `max_records`: cap row count.
  - `days`: cap age.
  - `max_bytes`: cap estimated backend storage size.
  - `prune_interval_seconds`: avoid pruning on every insert.
  - `prune_every_n_inserts`: run prune on an insert cadence.
- `logging.max_logging_queue`: keep async queue bounded (default is bounded; do
  not set `<= 0` unless you explicitly want unbounded memory growth).
- `logging.query_log_only`: when true, skip mirroring aggregate counters to the
  persistence backend and keep only raw query-log rows.
- `logging.query_log_sampling.enabled`: when false, suppress persistent
  query-log writes from the stats collector.
- `logging.query_log_sampling.sample_rate`: keep only a fraction of query-log
  rows (for example `0.1` for ~10%).
- `logging.query_log_sampling.rate`: compatibility alias for `sample_rate`.
- `logging.query_log_sample_rate`: legacy compatibility alias.
- `logging.query_log_dedupe.window_seconds`: suppress repeated identical
  query-log rows inside a short window.
- Full hardening profile: `example_configs/logging/query_log_hardening.yaml`.
- Detailed guide: `docs/query-log-hardening.md`.

Rate-limit integration:

- `rate` plugin `deny_log_first_n` logs only the first N denies for each active
  blocked episode.
- Remaining deny events set `PluginDecision.suppress_query_log` and are skipped
  in persistent query logs until the episode cools down.

Example hardening profile:

```yaml
logging:
  async: true
  max_logging_queue: 4096
  query_log_only: false
  query_log_retention:
	max_records: 500000
	days: 7
	max_bytes: 2147483648
	prune_interval_seconds: 30
	prune_every_n_inserts: 200
  query_log_sampling:
	sample_rate: 0.25
  query_log_dedupe:
	window_seconds: 2
	max_entries: 50000
  backends:
	- id: local-log
	  backend: sqlite
	  config:
		db_path: ./config/var/stats.db
		# Per-backend overrides (optional):
		# retention_max_records: 250000
		# retention_days: 3
		# retention_max_bytes: 1073741824
		# retention_prune_interval_seconds: 15
		# retention_prune_every_n_inserts: 100
```

Backend-specific optional maintenance controls:

- SQLite: `retention_vacuum_on_prune`, `retention_vacuum_interval_seconds`,
  `sqlite_auto_vacuum` (`none`, `full`, `incremental`).
- MongoDB: `retention_native_ttl` (enables a TTL index when `retention_days`
  is set).
- MySQL/MariaDB: `retention_optimize_on_prune`,
  `retention_optimize_interval_seconds`.
- PostgreSQL: `retention_vacuum_on_prune`, `retention_vacuum_interval_seconds`.

### 2.6 Plugins

In the `plugins` list each entry is a `PluginInstance`:

```yaml
plugins:
  - type: filter
	id: main-filter
	enabled: true
	setup:
	  abort_on_failure: true
	hooks:
	  pre_resolve:
		enabled: true
		priority: 50
	logging:
	  level: info
	config:
	  # plugin-specific config here
```

You normally care about:

- `type`: short alias for the plugin.
- `id`: optional stable identifier for this plugin instance (surfaced in stats, logs, and admin UI).
- `enabled`: whether it runs.
- `hooks`: per-hook enable/priority overrides (optional).
- `setup`: one-time setup behaviour; `abort_on_failure` controls whether a failing setup() aborts startup.
- `logging`: per-plugin logging overrides (level, file, stderr, syslog) using the same shape as the global `logging` block.
- `config`: the actual configuration for that plugin.

Common plugin‑wide config options:

Targeting (preferred shape uses a nested `targets` block):

- `config.targets` (object or legacy list/string)
  - **Preferred object keys**:
	- `ips`: list/string of CIDR/IPs to target.
	- `ignore_ips`: list/string of CIDR/IPs to exclude.
	- `listeners`: list/string of listeners (`udp`, `tcp`, `dot`, `doh`) or aliases:
	  - `secure` → `dot` + `doh`
	  - `unsecure`/`insecure` → `udp` + `tcp`
	  - `any`/`*`/`null` → no restriction
	- `domains`: list/string of domain names to match.
	- `domains_mode`: `exact` or `suffix` (defaults to `suffix` when `domains` is set).
	- `qtypes`: list/string of qtype names or `'*'` for all types (e.g. `['A', 'AAAA']`).
	- `opcodes`: list/string of DNS opcodes (e.g. `['QUERY']`).
	- `rcodes`: list/string of response codes for post‑resolve plugins (e.g. `['NOERROR', 'NXDOMAIN']`).
  - **Legacy form**: if `targets` is a list/string, it is treated as `targets.ips`.

Logging

- **Per-plugin logging**:
  - The `logging` stanza on the plugin instance lets you bump log level or direct
	output differently from the global logger.

Example with all common knobs:

```yaml
plugins:
  - type: some-plugin
	id: example
	enabled: true
	logging:
	  level: debug
	config:
	  targets:
		ips:
		  - 192.168.0.0/16
		  - 10.10.10.0/24
		ignore_ips: [ "192.168.0.10" ]
		listeners: [ dot, doh ]
		domains: [ corp.example ]
		domains_mode: suffix  # exact | suffix
		qtypes: [ 'A', 'AAAA' ]
		opcodes: [ 'QUERY' ]
		rcodes: [ 'NOERROR', 'NXDOMAIN' ]
```

---

## 3. Listeners and upstreams by example

### 3.1 UDP/TCP listener

A typical `server.listen.dns` configuration:

```yaml
server:
  listen:
	dns:
	  udp:
		enabled: true
		host: 0.0.0.0
		port: 53
	  tcp:
		enabled: true
		host: 0.0.0.0
		port: 53
```

Turn TCP off if you never want to accept TCP DNS:

```yaml
	  tcp:
		enabled: false  # true | false
```

### 3.2 DNS-over-TLS (DoT) upstream

To talk to an upstream DoT resolver:

```yaml
upstreams:
  strategy: round_robin      # failover | round_robin | random
  endpoints:
	- host: 1.1.1.1
	  port: 853
	  transport: dot
	  tls:
		server_name: cloudflare-dns.com
		verify: true
```

You can mix DoT and plain UDP endpoints in the same list; the `strategy` decides how they are chosen.

### 3.3 DNS-over-HTTPS (DoH) listener with TLS

To expose a DoH listener directly from Foghorn (for example on port 8053 with TLS termination):

```yaml
server:
  listen:
	# ... dns.udp / dns.tcp here ...
	doh:
	  enabled: true
	  host: 0.0.0.0
	  port: 8053
	  cert_file: /etc/foghorn/tls/server.crt
	  key_file: /etc/foghorn/tls/server.key
```

### 3.4 DoH listener behind an HTTP reverse proxy

When Foghorn itself is running behind an HTTP reverse proxy (for example, nginx or Envoy), you typically terminate TLS at the proxy and run the DoH listener as plain HTTP on localhost. The proxy handles `https://` and forwards `/dns-query` to Foghorn:

```yaml
server:
  listen:
	# ... dns.udp / dns.tcp here ...
	doh:
	  enabled: true
	  host: 127.0.0.1
	  port: 8443
	  # No cert_file/key_file here; TLS is terminated at the reverse proxy.
```

Your reverse proxy is then configured to listen on `443` with TLS and proxy requests such as `https://dns.example.com/dns-query` to `http://127.0.0.1:8443/dns-query`.

### 3.5 Helper Make targets for TLS keys and certificates

For quick local testing, the `Makefile` includes convenience targets that generate a small CA and server key material under `./keys`:

- `make ssl-ca` – create `foghorn_ca.key` and a self-signed `foghorn_ca.crt` with CA key usage.
- `make ssl-ca-pem` – export the CA certificate as `foghorn_ca.pem` for use as a trust anchor (e.g. `upstreams.*.tls.ca_file`).
- `make ssl-cert CNAME=myserver` – create a server key and certificate signed by the local CA, named `foghorn_${CNAME}.key` / `.crt`.
- `make ssl-server-pem CNAME=myserver` – build a combined `foghorn_${CNAME}.pem` containing the server certificate and key.

Use `ca.pem` when Foghorn is a TLS **client** and needs to *trust* an internal CA (for example for DoT/DoH upstreams via `tls.ca_file`). Use `server.pem` when Foghorn is acting as a TLS **server** and you need a single file containing both cert and key for a listener.

These are intended for development and lab environments only; for production, use your normal PKI or certificate management.

---

## 4. Plugin cookbook

Below are the built‑in plugins, with short descriptions and minimal configs. All examples assume they live in the shared `plugins:` list.

### 4.1 Access control (`acl`)

IP-based allow/deny control at the edge.

```yaml
plugins:
  - type: acl
	config:
	  default: allow       # allow | deny
	  allow:
		- 192.168.1.1 # Overrides the deny below.
		- 10.0.0.0/8
	  deny:
		- 192.168.0.0/16
		- 172.16.0.0/12
```

### 4.2 Docker containers (`docker`)

Expose Docker container names as DNS answers.

```yaml
plugins:
  - type: docker
	config:
	  endpoints:
		- url: unix:///var/run/docker.sock
		- url: tcp://my.server.lan:2375
	  ttl: 60
	  health: ['healthy', 'running']
	  discovery: true   # false | true
```

### 4.3 Hosts files (`hosts`)

Serve additional records from one or more hosts-style files.

```yaml
plugins:
  - type: hosts
	config:
	  file_paths:
		- /etc/hosts
		- ./config/hosts.
	  ttl: 300
	  watchdog_enabled: true   # null | true | false
```

### 4.4 List downloader (`lists`)

Fetches remote blocklists/allowlists on a schedule and stores them as files for other plugins.

```yaml
plugins:
  - type: lists
	hooks:
	  setup: { priority: 10 } # Setup early so other plugins have their files available.
	config:
	  download_path: ./config/var/lists
	  interval_days: 1
	  hash_filenames: true # false | true - Multiple "hosts.txt" files easily handled by hashing the URL
	  urls:
		- https://example.com/ads.txt
		- https://example.com/hosts.txt
		- https://serverA/hosts.txt
		- https://serverB/hosts.txt
```

### 4.5 Domain filter / adblock (`filter`)

Flexible domain/IP/pattern filter used to build adblockers and kid-safe DNS.

```yaml
plugins:
  - type: filter
	hooks:
	  priority: 25 # Applies to pre_resolve + post_resolve + setup unless overridden.
	config:
	  default: allow  # deny | allow
	  targets:
		- 10.0.1.0/24 # Kids subnet
	  ttl: 300
	  # When a pre_resolve deny happens, synthesize an IP response pointing at a sinkhole address
	  deny_response: ip  # nxdomain | refused | servfail | ip | noerror_empty
	  deny_response_ip4: 0.0.0.0

	  # Post-resolve IP filtering rules (answer inspection)
	  blocked_ips:
		- ip: 203.0.113.10        # Replace a specific IP with a safer landing page
		  action: replace
		  replace_with: 0.0.0.0
		- ip: 203.0.113.0/24      # Strip an entire subnet from answers
		  action: remove
		- ip: 198.51.100.42       # Block a single IP entirely (maps to deny)
		  action: deny

	  blocked_domains_files:
		- ./config/var/lists/*.txt
	  allowed_domains:
		- homework.example.org
	  blocked_domains:
		- how-to-cheat.org
		- current-game-obession.io

```

### 4.6 Flaky upstream simulator (`flaky`)

Injects DNS errors and timeouts for testing client behaviour.

```yaml
plugins:
  - type: flaky
	id: dev-servfail-5-A-AAAA
	config:
	  servfail_percent: 5
	  nxdomain_percent: 0
	  timeout_percent: 0
	  truncate_percent: 0
	  noerror_empty_percent: 0
	  apply_to_qtypes: ['A', 'AAAA']
```

### 4.7 mDNS / Bonjour bridge (`mdns`)

Expose mDNS / DNS-SD services as normal DNS records.

***Note***
For mDNS / DNS-SD / Bonjour / Zeroconf / Avahi you *must* be on the same L2 network in order to receive (e.g. in Docker you might need to run with --net=host or --net=macvlan)

```yaml
plugins:
  - type: mdns
	config:
	  domain: '.'
	  ttl: 120
	  include_ipv4: true   # true | false
	  include_ipv6: true   # true | false
	  network_enabled: true
```

### Security hardening and DNS amplification protection

Foghorn includes several built-in security protections to mitigate DoS/DDoS attacks and DNS amplification risks:

- **DoH parameter size validation**: Oversized base64-encoded DNS parameters are rejected (HTTP 413) before decoding, preventing processing of megabyte-scale payloads.
- **Recursive resolver depth limits**: Default `max_depth` is 12 (configurable via `server.resolver.max_depth`) to limit recursion depth and prevent abuse through deep delegation chains.
- **Upstream health cleanup**: The `DNSUDPHandler._cleanup_upstream_health()` method periodically removes stale healthy entries from the `upstream_health` tracking dictionary to prevent unbounded memory growth.
- **Rate limiting and concurrency controls**: The `rate` plugin provides per-client or per-(client,domain) rate limiting (see below). Combined with listener connection limits (`max_connections`, `max_connections_per_ip`) and per-connection query caps (`max_queries_per_connection`), this provides defense at multiple layers.
- **DNS response size limits**: UDP responses are capped at 1232 bytes to minimize amplification potential. DoH response sizes are also limited to large payloads.

When deploying Foghorn as an authoritative or recursive resolver on exposed interfaces, consider enabling these protections and monitoring the metrics exposed via the admin UI for query patterns and error rates.

### 4.8 Rate limiting (`rate`)

Adaptive rate limiting per client or per (client,domain).
Note: listener/transport protections (for example UDP inflight shedding and
TCP/DoT connection limits) can trigger before plugin hooks run, so under heavy
load some traffic may be dropped/refused before the `rate` plugin evaluates it.

```yaml
plugins:
  - type: rate
	config:
	  mode: per_client  # per_client | per_client_domain | per_domain
	  window_seconds: 10
	  warmup_windows: 6
	  burst_factor: 3.0
	  burst_windows: 6
	  stats_log_interval_seconds: 900
	  min_enforce_rps: 50.0
	  deny_response: nxdomain  # nxdomain | refused | servfail | noerror_empty | ip
	  deny_response_ip4: 0.0.0.0
	  db_path: ./config/var/dbs/rate_limit.db
```

### 4.9 Per-domain upstream routing (`router`)

Send different domains to different upstreams.

```yaml
upstreams:
  strategy: failover
  endpoints:
	- host: 9.9.9.9
	  port: 53
	  transport: udp

plugins:
  - type: router
	config:
	  routes:
		- domain: internal.example
		  upstreams:
			- host: 10.0.0.53
			  port: 53
		- suffix: corp.
		  upstreams:
			- host: 192.168.1.1
			  port: 53
```

### 4.10 Inline and file-based records (`zone`)

The ZoneRecords ("zonerecords") plugin (`type: zone`) is for custom DNS answers. If you only need a handful of local overrides, you **do not** need to create and maintain an entire RFC-1035 zonefile — just use inline `records` or a simple `file_paths` records file.

Define custom records either:

- Inline using the `records` list and the pipe-delimited format
  `<domain>|<qtype>|<ttl>|<value>`.
- From one or more custom records files using `file_paths` (same
  pipe-delimited format as above).
- From one or more RFC‑1035 style BIND zonefiles using `bind_paths`
  (parsed via dnslib; supports `$ORIGIN`, `$TTL`, and normal RR syntax). Each
  bind_paths entry can be either a string path, or an object with `path`, plus
  optional `origin`/`ttl` overrides.
- Optional `path_allowlist` can restrict `file_paths` and `bind_paths` to a set
  of allowed directory prefixes.
  - Paths containing explicit `..` segments are rejected.

All sources are merged into a single internal view per (name, qtype):

- The TTL for each (name, qtype) pair comes from the first occurrence of
  that pair across all sources in configuration order.
- Values are kept in first-seen order with duplicates dropped.
- An SOA record at a name marks the apex of an authoritative zone; inside
  such a zone the plugin behaves like an authoritative server, including
  correct NXDOMAIN/NODATA and ANY semantics with SOA in the authority
  section.
- For zones with an SOA apex and a TCP/DoT listener enabled, Foghorn also
  answers AXFR/IXFR for that zone using the same ZoneRecords data.
- IXFR server side currently implemented as a full AXFR-style transfer.
- IXFR client side is not yet supported.

Wildcard notes:
- ZoneRecords treats a leading `*` label as matching **one or more** labels,
  which differs from RFC 4592. For example, `*.example.org` matches both
  `a.example.org` and `a.b.example.org`.

Operational guidance:
- Keep authoritative zone apex counts to less than **1,000 zones per instance**
  unless you have benchmarked higher counts.

Optional merge controls:

- `load_mode`:
  - `merge` (default) preserves any existing in-memory records and overlays new data.
  - `replace` rebuilds the mapping on each load/reload.
  - `first` uses the first configured source group in this order:
	file_paths → bind_paths → axfr_zones → records (inline), and ignores the others.
- `merge_policy`: `add` (default) appends distinct values into an RRset;
  `overwrite` replaces an RRset when a later source defines the same
  `(domain, qtype)`.
- `nxdomain_zones`: optional list of zone suffixes where, if a name does not
  exist in ZoneRecords, the plugin returns NXDOMAIN/NODATA instead of falling
  through to upstream resolution.
- `max_file_size_bytes`: max allowed bytes per source file in `file_paths` and
  `bind_paths`.
- `max_records`: max accepted record values per load cycle.
- `max_record_value_length`: max per-record value length in characters.
- `auto_ptr_enabled` / `max_auto_ptr_records`: enable and bound auto-generated
  PTR values.
- `soa_synthesis_enabled`: controls inferred SOA fallback when no SOA exists.

```yaml
plugins:
  - type: zone
	config:
	  # Optional: pipe-delimited records files
	  file_paths:
		- ./config/zones.d/internal.records
	  # Optional: native BIND zonefiles
	  bind_paths:
		- ./config/zones.d/internal.zone
	  # Optional: inline records
	  records:
		- 'printer.lan|A|300|192.168.1.50'
		- 'files.lan|AAAA|300|2001:db8::50'
	  # Optional: load/reload behaviour
	  load_mode: merge     # replace | merge | first
	  merge_policy: add    # add | overwrite
	  ttl: 300
```

---

## 5. Example Plugins

Some resolver filters are shipped as **examples only** and now live under
`foghorn.plugins.resolve.examples` (for example the DNS prefetch, example
rewrites, greylist, new-domain WHOIS filter, file-over-DNS helper, and
finger-style user lookup over DNS).

These plugins are functional, but they are intended as reference implementations
for advanced users. They are not wired into a running server by default; a
config that references these example types will not work unless you first move
or copy the corresponding module into `foghorn.plugins.resolve` (or build your
own plugin based on them).

Treat them as starting points: read the code end‑to‑end, decide whether the
behaviour and trade‑offs match your environment, and in most cases prefer
creating your own plugin that uses these as an example rather than dropping
them directly into production.

### 5.1 DNS prefetch (`prefetch`)

Prefetches the most popular names from statistics to keep the cache warm. Not very efficient yet, but it shows the concept.

```yaml
plugins:
  - type: prefetch
	config:
	  interval_seconds: 60
	  prefetch_top_n: 100
	  max_consecutive_misses: 5
	  qtypes: ['A', 'AAAA']
```

### 5.2 Example rewrites (`examples`)

A playground plugin that can rewrite responses or demonstrate behaviours.

```yaml
plugins:
  - type: examples
	config:
	  base_labels: 2
	  max_subdomains: 5
	  apply_to_qtypes: ['A']
```

### 5.3 Greylist new names (`greylist_example`)

Introduces a delay window before new names are allowed. The origin of the project! Started as a greylist for security researchers working on phishing protection.

```yaml
plugins:
  - type: greylist_example
	config:
	  db_path: ./config/var/greylist.db
	  cache_ttl_seconds: 300
	  duration_hours: 24
```

### 5.4 New-domain WHOIS filter (`new_domain`)

Blocks domains that appear too new according to WHOIS data.

```yaml
plugins:
  - type: new_domain
	config:
	  threshold_days: 7
	  whois_db_path: ./config/var/whois_cache.db
	  whois_cache_ttl_seconds: 3600
```

### 5.5 File over DNS (`file_over_dns`)

Serves byte ranges from configured files over TXT DNS queries using a pattern
like `<name>.<start>.<end>.<rest-of-domain>`.

```yaml
plugins:
  - type: file_over_dns
	config:
	  files:
		- name: readme
		  file_path: ./README.md
	  ttl: 300
	  max_chunk_bytes: 1024
	  format: base64  # base64 | raw
```

### 5.6 Finger over DNS (`finger`)

Exposes a finger-style view of user information over TXT records for
`<user>.<domain>` when enabled, reading from per-user files such as
`$HOME/.finger` or a configured path template.

Exposes a finger-style view of user information over TXT records for
`<user>.<domain>` when enabled, reading from per-user files such as
`$HOME/.finger` or a configured path template.

---

## 6. Variables

Foghorn supports a simple variable system so you can avoid repetition, keep
secrets out of committed configs, and adapt the same config to multiple
environments (local dev, CI/CD, containers, different sites, and so on).

Variables are resolved in this order (highest priority last):

1. Command-line `--var` / `--vars` flags.
2. Environment variables.
3. The `vars:` section at the top of the config file.

Later sources override earlier ones for the same variable name.

### 6.1 Basic usage

At the top of your config:

```yaml
vars:
  PROFILE: lan
  MAIN_SUBNET: 192.168.0.0/16
  TEST_SUBNET: 192.168.50.0/24
```

You can then reference these variables elsewhere using `${NAME}` syntax:

```yaml
server:
  listen:
	dns:
	  udp: { enabled: true, host: 0.0.0.0, port: 53 }

plugins:
  - type: filter
	id: main-filter
	config:
	  targets: ${MAIN_SUBNET}
  - type: filter
	id: test-filter
	config:
	  targets: ${TEST_SUBNET}
```

This keeps common values (like subnets) in one place so they are easy to update
and reuse.

### 6.2 Named subnets example (kids vs adults)

```yaml
vars:
  LAN: 192.168.0.0/16
  ADULTS: 192.168.1.0/24
  KIDS: 192.168.2.0/24

server:
  listen:
	dns:
	  udp: { enabled: true, host: 0.0.0.0, port: 53 }

plugins:
  - type: filter
	id: adblock
	config:
	  targets: ${LAN}
	  default: allow
	  # ... normal adblock rules ...

  - type: filter
	id: kids-filter
	config:
	  targets: ${KIDS}
	  default: deny
	  # ... strict allowlist rules for kids ...
```

### 6.3 Why use variables?

- **Less repetition**: Define shared values (subnets, hostnames, DNS names) once
  and reuse them throughout the config.
- **Safer secrets**: Keep API tokens, passwords, and other sensitive data out of
  the main config by passing them via environment variables or command-line
  overrides.
- **Environment-specific overrides**: In CI/CD or container setups you can keep
  a single base config file and override only the variables per environment.
  - In local dev, you might rely on the `vars:` section.
  - In staging/prod, environment variables or `--var` flags can redefine a few
	keys (for example, stats database DSN, upstreams, or listen addresses).
- **Caveat for IDEs**: Some editors/IDEs run JSON/YAML schema validation on the
  raw file and do not interpolate `${VAR}` placeholders first. In that case the
  config may show spurious validation errors even though Foghorn will load it
  correctly at runtime.

---

## 7. Sample configurations

These sketches show how all the pieces fit together. Adjust paths and IPs to match your environment.

### 7.1 `local`: workstation config

Goals:

- Cache locally for speed.
- Forward to a public DoT resolver.
- No plugins yet, no logging, no

```yaml
vars:
  PROFILE: local

server:
  listen:
	dns:
	  udp: {enabled: "true", host: "127.0.0.1", port: 5353}
  cache:
	module: memory # Default

upstreams:
  strategy: failover
  endpoints:
	- host: 1.1.1.1
	  port: 853
	  transport: dot
	  tls: {server_name: "cloudflare-dns.com", verify: true}

logging:
  python:
	level: info

plugins: []
```

### 7.2 `lan`: home LAN with adblock and kid filter

Goals:

- Listen on all interfaces.
- Use filter+lists to block ads.
- Use a second filter instance as a stricter allowlist for kids.
- Route internal corp domains to a separate upstream.

```yaml
vars:
  PROFILE: lan
  LAN: 192.168.0.0/16
  KIDS: 192.168.2.0/24

server:
  listen:
	dns:
	  udp: {enabled: true, host: 0.0.0.0, port: 53}
  cache:
	module: sqlite
	config:
	  db_path: ./config/var/dns_cache.db

upstreams:
  strategy: round_robin
  endpoints:
	- host: 9.9.9.9
	  port: 53
	  transport: udp
	- host: 1.1.1.1
	  port: 53
	  transport: udp

logging:
  python:
	level: info
  async: true
  query_log_only: false
  backends:
	- id: lan-log
	  backend: sqlite
	  config:
		db_path: ./config/var/stats_lan.db

stats:
  enabled: true
  source_backend: lan-log

plugins:
  - type: lists
	id: blocklists
	config:
	  pre_priority: 20
	  download_path: ./config/var/lists
	  interval_days: 1
	  urls:
		- https://example.com/ads.txt

  - type: filter
	id: adblock  # Ad block for everyone
	config:
	  pre_priority: 40
	  default: allow
	  blocked_domains_files:
		- ./config/var/lists/ads.txt

  - type: filter
	id: kids  # Filter just for the kids.
	config:
	  pre_priority: 50
	  targets: ${KIDS}
	  default: deny
	  allowed_domains:
		- homework.example.org
		- library.example.org
	  deny_response: ip
	  deny_response_ip4: 0.0.0.0

  - type: router
	id: corp-router
	config:
	  pre_priority: 80
	  routes:
		- suffix: corp
		  upstreams:
			- host: 192.168.100.53
			  port: 53
```

### 7.3 `smb`: small business

Goals:

- Persistent DNS cache.
- Local LAN overrides via hosts, mDNS bridge, and zone records.
- Simple access control and rate limiting.
- Query-log persistence in MariaDB/MySQL (configured via logging.backends).

```yaml
vars:
  PROFILE: smb
  LISTEN: "0.0.0.0"
  LAN: 192.168.0.0/16
  FLOOR1: 192.168.10.50
  FLOOR2: 192.168.20.50
  FLOOR1_NET: 192.168.10.0/24
  FLOOR2_NET: 192.168.20.0/24

server:
  listen:
	dns:
	  udp: {enabled: true, host: ${LISTEN}, port: 53}
	  tcp: {enabled: true, host: ${LISTEN}, port: 53}
  cache:
	module: sqlite
	config:
	  db_path: ./config/var/dns_cache.db

upstreams:
  strategy: failover
  endpoints:
	- host: dot1.myisp.example
	  port: 853
	  transport: dot
	  tls: {server_name: dot1.myisp.example, verify: true}
	- host: dot2.myisp.example
	  port: 853
	  transport: dot
	  tls: {server_name: dot2.myisp.example, verify: true}

logging:
  python:
	level: info
  async: true
  query_log_only: false
  backends:
	- id: mariadb
	  backend: mariadb
	  config:
		host: db.internal
		port: 3306
		user: foghorn
		database: foghorn_stats
		# Optional: control which Python DB driver is used.
		driver: auto                 # auto | mariadb | mysql-connector-python | mysql
		driver_fallback: auto        # auto | none | <driver> | [<driver>, ...]

stats:
  enabled: true
  source_backend: mariadb

plugins:
  - type: acl
	id: lan-only
	config:
	  pre_priority: 10
	  default: deny
	  allow:
		- ${LAN}

  - type: hosts
	id: office-hosts
	config:
	  pre_priority: 20
	  file_paths:
		- /etc/hosts
		- ./config/hosts.office

  - type: mdns
	id: office-mdns
	config:
	  pre_priority: 30
	  domain: 'devices.mycorp'
	  ttl: 120

  - type: zone
	id: printers-floor1
	config:
	  pre_priority: 40
	  targets: ${FLOOR1_NET}
	  records:
		- 'printer.corp|A|300|${FLOOR1}'

  - type: zone
	id: printers-floor2
	config:
	  pre_priority: 41
	  targets: ${FLOOR2_NET}
	  records:
		- 'printer.corp|A|300|${FLOOR2}'

  - type: docker
	id: lan-docker
	config:
	  pre_priority: 50
	  targets: ${LAN}
	  endpoints:
		- url: unix:///var/run/docker.sock
	  ttl: 60

  - type: rate
	id: smb-rate
	config:
	  pre_priority: 5 # Run first thing
	  mode: per_client
	  window_seconds: 10
	  min_enforce_rps: 20.0
	  deny_response: refused
```

### 7.4 `enterprise`: layered caches and rich stats

Goals:

- Redis or Memcached cache for large edge deployments.
- Multiple PostgreSQL statistics backends plus Influx logging.
- Heavy use of plugins (router, filter, docker, mdns, zone).
- Fine-grained client targeting and per-plugin priorities.

```yaml
vars:
  PROFILE: enterprise
  LISTEN: 0.0.0.0
  LAN: 10.0.0.0/16
  OFFICE: 10.10.0.0/16
  OFFICE_REMOTE: 10.20.0.0/16

server:
  listen:
	dns:
	  udp: {enabled: true, host: ${LISTEN}, port: 53}
	  tcp: {enabled: true, host: ${LISTEN}, port: 53}
	  dot:
		enabled: true
		host: ${LISTEN}
		port: 853
		cert_file: /etc/foghorn/tls/server.crt
		key_file: /etc/foghorn/tls/server.key
  cache:
	module: redis # redis | memcached | sqlite | memory | mongodb | none
	config:
	  url: redis://redis-cache.internal:6379/0
	  namespace: foghorn:dns_cache:

upstreams:
  strategy: round_robin
  max_concurrent: 2
  endpoints:
	- host: 10.0.0.53
	  port: 53
	  transport: udp
	  pool:
		max_connections: 64
		idle_timeout_ms: 30000
	- host: 10.0.1.53
	  port: 53
	  transport: udp
	  pool:
		max_connections: 64
	- host: 10.0.2.53
	  port: 853
	  transport: dot
	  tls: {server_name: dns.corp.example, verify: true}
	  pool:
		max_connections: 32

logging:
  python:
	level: info
  async: true
  backends:
	- id: pg_primary
	  backend: postgr
	- id: pg_reporting
	  backend: postgres
	  config:
		  host: pg-reporting.internal
		  port: 5432
		  user: foghorn_ro
		  database: foghorn_stats
	- id: influx-logging
	  backend: influx
	  config:
		  write_url: http://metrics.internal:8086/api/v2/write
		  bucket: dns
		  org: infra

stats:
  enabled: true
  source_backend: pg_primary

plugins:
  - type: acl
	id: lan-only
	hooks:
	  pre_resolve: 10
	config:
	  default: deny
	  allow:
		- ${LAN}

  - type: docker
	id: lan-docker
	hooks:
	  pre_resolve: 20
	config:
	  targets: ${LAN}
	  endpoints:
		- url: unix:///var/run/docker.sock
	  ttl: 60

  - type: mdns
	id: enterprise-mdns
	hooks:
	  pre_resolve: 30
	  domain: 'devices.lan'
	  ttl: 120

  - type: zone
	id: zone-1-office
	hooks:
	  pre_resolve: 40
	config:
	  pre_priority: 40
	  targets: ${OFFICE}
	  file_paths:
		- ./config/zones.d/zone-1.zone

  - type: zone
	id: zone-2-remote
	hooks:
	  pre_resolve:
		priority: 41
	config:
	  targets: ${OFFICE_REMOTE}
	  file_paths:
		- ./config/zones.d/zone-2.zone

  - type: router
	id: corp-router
	hooks:
	  pre_resolve: 60
	config:
	  routes:
		- suffix: corp.example
		  upstreams:
			- host: 10.1.0.53
			  port: 53

  - type: filter
	id: global-filter
	hooks:
	  pre_resolve: 80
	config:
	  default: allow
	  blocked_domains_files:
		- ./config/var/lists/global_block.txt
```

From here you can mix and match plugins, caches, and stats backends to shape Foghorn into exactly the DNS service you need.

[![Python Tests](https://github.com/zallison/foghorn/actions/workflows/pytest.yml/badge.svg)](https://github.com/zallison/foghorn/actions/workflows/pytest.yml) ![Test Coverage](https://img.shields.io/badge/test_coverage-89%25-blue) [![Docker Pulls](https://img.shields.io/docker/pulls/zallison/foghorn)](https://hub.docker.com/r/zallison/foghorn/)  [![PyPI Downloads](https://static.pepy.tech/personalized-badge/foghorn?period=total&units=INTERNATIONAL_SYSTEM&left_color=GRAY&right_color=BLUE&left_text=downloads)](https://pepy.tech/projects/foghorn)  [![BuyMeACoffee](https://raw.githubusercontent.com/pachadotdev/buymeacoffee-badges/main/bmc-blue.svg)](https://www.buymeacoffee.com/foghorndns)

## Stargazers over time
[![Stargazers over time](https://starchart.cc/zallison/foghorn.svg?variant=adaptive)](https://starchart.cc/zallison/foghorn)
