> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dnzwithazee.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Live capture

> Watch the queries holding a few keywords (typed, or saved keyword lists) as the server answers them, grouped by domain or one by one, and block a domain straight from the results.

**Live capture** shows the queries the server answers as they arrive, filtered by keywords, so you
can see what a device or a whole network is reaching right now and block what you don't want. Type
`tiktok, facebook`, start the capture, and every query whose name holds one of the keywords
(`api.tiktok.com`, `v16.tiktokcdn.com`, `graph.facebook.com`…) shows up within a second.

The results come two ways:

* **By domain**: queries grouped by registered domain (`api.tiktok.com` and `www.tiktok.com` under
  `tiktok.com`, `p16.tiktokcdn-us.com` under `tiktokcdn-us.com`; the public suffix list knows that
  `example.co.uk` is one domain). Each domain shows how many queries, names and clients it has, how
  many were already answered as blocked, and when it was last seen. Open a domain to see the names
  inside it.
* **Live**: every query, newest first (the last 500), with the type, response code, where the answer
  came from and the client.

## Start a capture

> [tab] Dashboard
>
> **1. Open Live Capture**
>
> Open **DNS → Live Capture**, or click **Live capture** on **DNS → Query Logs**, which carries
> the log's search over.
>
> **2. Pick a keyword list or enter keywords**
>
> Click one or more [keyword lists](#keyword-lists) under the keyword field, or type keywords,
> for example `tiktok, facebook` (or both). Optionally give a **client** address to watch one
> device, and, if you can see all queries, pick the scope.
>
> **3. Start**
>
> Click **Start capture**. Matching queries show up within a second. **Stop** ends it.
>
> `?q=`, `?lists=` (keyword list ids) and `?client=` in the page's address fill the form, so a
> capture can be shared as a link.

> [tab] CLI
>
> ```bash theme={null}
> dnz capture tiktok facebook                 # every matching query, until Ctrl-C
> dnz capture tiktok --group                  # each domain once, when it first shows up
> dnz capture --client 192.168.1.20 --for 2m  # everything one device asks, for two minutes
> dnz capture ads --division it --json        # one JSON object per query
> dnz capture --list "Social media" -l Games  # the keywords of saved keyword lists
> ```
>
> `--division` captures that division's queries; superadmins capture the whole server by default.
> When the capture ends (Ctrl-C or `--for`), the CLI lists the domains it saw, busiest first, with
> query, name, client and blocked counts. Block one with `dnz filter rule block <domain>`.

> [tab] AI assistant
>
> Over [MCP](https://docs.dnzwithazee.com/integrations/ai-assistants.md), the `capture_queries` tool watches for 1 to 60 seconds
> (15 by default) and returns the matching domains, grouped as above. Ask, for example:
>
> > What is 192.168.1.20 talking to right now? Suggest what to block.
>
> Its `lists` argument takes keyword list names; `capture_keyword_lists` lists them.

## Keywords

* Separate keywords with commas or spaces; a name holding **any** of them matches. `tiktok` matches
  `tiktok.com`, `tiktokcdn.com` and `tiktokv.com`.
* Matching is on the name as asked, in lowercase. A leading `*.` or a trailing dot is ignored.
* No keywords captures every query. Use it with a client address to see everything one device asks.
* Up to 50 keywords per capture, typed and from keyword lists together.

## Keyword lists

Keywords you watch for often can be saved once under a name and then picked instead of typed: a
**Social media** list holding `tiktok facebook instagram snapchat`, a **Games** list holding
`steam epicgames roblox`. On **DNS → Live Capture** the lists show as buttons under the keyword
field. Click one or more (and type extra keywords if you like), then start the capture: it watches
for every keyword of the picked lists plus the typed ones.

> [tab] Dashboard
>
> * **New list** saves a list; with keywords typed, the button reads **Save keywords as list** and
>   starts from them.
> * The pencil button next to it edits or deletes a list.
> * A list holds up to 50 keywords (one per line, or separated by commas or spaces) and a name of
>   up to 64 characters, unique within its scope.

> [tab] CLI
>
> ```bash theme={null}
> dnz capture-list list [--division it]                          # server-wide lists, and the division's
> dnz capture-list add "Social media" tiktok facebook instagram  # server-wide for superadmins
> dnz capture-list add Games steam roblox --division it          # the division's
> dnz capture-list edit "Social media" --keywords tiktok facebook instagram snapchat
> dnz capture-list rm Games --division it
> ```

| List | Who can pick it | Who can change it |
| - | - | - |
| A division's | Captures of that division (**View query logs** in it) | **Manage blocklists** in the division |
| Server-wide (globe icon, *Every division*) | Every capture | **Manage server-wide filtering** |

Changes are in the [audit log](https://docs.dnzwithazee.com/admin/audit-log.md) as `capture_list.create`, `capture_list.update` and
`capture_list.delete`; a capture's `capture.start` entry names the lists it used. On a DNZ secondary
the lists are the primary's, copied with its other settings, and read-only there.

## Block from the results

Each domain has a **Block** button, and **Block N selected** blocks every ticked domain at once.

Blocking adds a rule to the **block list** (**DNS → Filtering → Blocking → Your rules**, see
[Filtering](https://docs.dnzwithazee.com/dns/filtering.md)). A rule covers the name and everything under it, so blocking
`tiktok.com` blocks `api.tiktok.com` too. To block one name only, open its domain and click
**Block only this**. Domains a block rule already covers say **In block list**.

To keep what you block together, pick a list in **Block into** next to the results' filter: **Just a
rule** (the default), one of your own lists (the ones you made under **Blocklists**), or **New list…**
with a name, which makes the list with the domains you block and keeps it picked for the next ones. A
domain in a list is blocked with everything under it, like a rule.

The rule's comment records the keywords it came from, and it is in the
[audit log](https://docs.dnzwithazee.com/admin/audit-log.md) as `filter_rule.create`, like any other rule. Rules take effect on the
next query: cached answers are not used for blocked names.

Which block list the rule lands in follows the capture's scope:

| Capture | The rule goes to | Needs |
| - | - | - |
| **All queries** | The server-wide block list | **Manage server-wide filtering** |
| A division | That division's block list | **Manage blocklists** (`filtering.manage`) in it |

Without that permission, the results show no **Block** buttons.

## Another server

Queries are answered where clients send them: often at a secondary, while you are on the primary. On a primary,
**DNS → Live Capture** has a **Server** choice: this server and each DNZ secondary. Pick one or more; the queries of the
ones picked show here, with a **Server** column in the Live view when more than one is shown.

* **You need nothing on the secondary**: no account, and no way to reach its API. Permissions are asked and checked on the
  primary (**View query logs** in a division, **View all query logs** for all of a server's queries), and the secondary
  connects out to the primary, as it does for everything else. A firewall that keeps the primary from its API changes nothing.
* **It is connected only during a capture.** Nothing stays open between captures. *Start* notes the capture on the primary,
  in memory. The secondary hears of it in its check-in's answer and only then opens two connections to the primary: a
  server-sent event stream for the filter, and a streaming upload of its hits. Both close when the capture ends.
* **Usually at once, within a minute at worst.** At *Start* the primary sends a DNS NOTIFY for the reserved name
  `capture.dnz.invalid` to the secondary's NOTIFY addresses, which makes it check in now. If a firewall keeps it out, the
  capture starts at the next check-in, and the page says *Waiting for ns2…* until then. A NOTIFY says nothing by itself: only
  the check-in's answer decides, so a forged one costs one check-in, at most one every 5 seconds. It never counts in the
  NOTIFY status, so a lost one doesn't mark the secondary *Behind*.
* **Bounded on both sides.** A capture stops when the viewer leaves, the stream breaks or its time is up (its time counts
  from when the secondary connects). A primary that restarts knows no capture, so nothing stays running on the secondary.
  At most 16 captures run on a server, and at most 16 per secondary are asked for. Hits past a slow link are dropped and counted
  on the page (*N skipped*), as a slow viewer's are.
* **A division's capture** goes to the secondary as zone names and client networks, since zone ids differ between servers.
* **Which servers.** Only DNZ secondaries are listed; BIND, Knot and the like aren't. One running an older DNZ shows disabled
  with *update it*, and one that hasn't checked in for 3 minutes with *not connected*. A secondary shows only its own queries: it
  has no server choice and can't watch its primary.
* **Nothing more per query.** The secondary runs the same capture code as ever: while nobody captures, a batch of queries
  costs one atomic load.

Starting one is in the primary's audit log as `capture.start`, with `on` naming the server. Blocking from the results adds the
rule on the primary, which the secondaries copy.

> [!NOTE]
>
> A reverse proxy in front of the primary must not buffer request bodies (nginx: `proxy_request_buffering off` for
> `/api/replication/capture/`), or hits wait until the capture ends. In a cluster of primaries, the secondary's check-in must
> reach the server you are connected to: a capture starts there and nowhere else. On a secondary that is a cluster itself,
> the leader answers it.

## Scope and permissions

| Capture | Permission | Sees |
| - | - | - |
| **All queries** | **View all query logs** (`logs.server`) | Every query this server answers |
| A division | **View query logs** (`logs.read`) in it | Names in its zones, and what clients in its [client networks](https://docs.dnzwithazee.com/dns/filtering.md#scopes) ask |

A **client** address narrows either one to a single device. API tokens with `logs.read` can capture
their division. See [Access](https://docs.dnzwithazee.com/admin/access.md) for roles and permissions.

## How it works

* The query log writer feeds the captures: it takes each batch of answered queries off the hot path
  every second (or every 500 queries). While nobody captures, a batch costs one atomic load. DNS
  answers never wait on a capture.
* **Nothing is stored.** A capture lives as long as its connection and ends when the page is closed,
  **Stop** is pressed, or its time runs out. Starting one is in the audit log as `capture.start`.
* At most **16 captures** run at once on a server. A viewer that falls behind gets a count of the
  queries it skipped instead of slowing anything down.
* Live capture works on every server, a DNZ secondary included, and a primary can show a secondary's
  queries ([Another server](#another-server)).
* In a [cluster](https://docs.dnzwithazee.com/deploy/cluster.md), a capture shows the queries answered by the server you are
  connected to, like the query log. Queries a client sends to another server show up in a capture
  there.
* `dnz_live_captures` on [`/metrics`](https://docs.dnzwithazee.com/dashboard/prometheus) is the number of captures running.

## API

`POST /api/capture` answers with server-sent events. It needs `logs.read` in the division given by
`division_id`, or `logs.server` for the whole server.

```bash theme={null}
curl -N -X POST https://dns.example.com/api/capture \
  -H "Authorization: Bearer $DNZ_TOKEN" -H "Content-Type: application/json" \
  -d '{"keywords": "tiktok, facebook", "lists": [3], "division_id": null, "client": "", "seconds": 60}'
```

| Field | Meaning |
| - | - |
| `keywords` | Keywords, separated by commas or spaces; empty for every query |
| `lists` | Keyword list ids whose keywords are added |
| `division_id` | A division, or `null` for the whole server |
| `client` | One client address, or empty for all |
| `seconds` | How long to run; empty runs until the client disconnects (at most 24 hours) |
| `on` | A DNZ secondary's name or id, to watch [that server](#another-server) from its primary; empty is this server |

Events:

| Event | Data |
| - | - |
| `start` | `{"keywords": ["tiktok", "facebook"], "lists": ["Social media"], "division_id": null, "client": null, "seconds": 60, "node": "…"}` |
| `hits` | `{"at": "2026-10-27T08:55:03.120Z", "hits": [{"name": "api.tiktok.com", "domain": "tiktok.com", "qtype": "A", "client": "192.168.1.20", "rcode": "NOERROR", "source": "upstream", "duration_ms": 12}], "dropped": 0}` |
| `end` | `{"reason": "time"}` when `seconds` ran out |

With `on`, `start` also has `"server": "ns2", "waiting": true`; `connected` (`{"server": "ns2"}`) says the secondary's stream is
up; each `hits` has `server`; and `end` can also be `{"reason": "ended", "message": "…"}` (it stopped or broke) or
`{"reason": "not_connected", "message": "…"}` (it never connected, after 150 seconds). `on` is refused on a secondary (`400`) and
for a server that is older or not connected (`409`).

`GET /api/capture/servers?division_id=2` lists what can be picked (`logs.read` there; without `division_id`, `logs.server`):
`{"this": "ns1", "follows_primary": null, "servers": [{"id": 4, "name": "ns2", "available": true, "why": null, "message": null, "last_seen": "…"}]}`.
`why` is `older` or `not_connected`; `follows_primary` names the primary on a secondary, which lists nothing.

A comment line (`: ping`) every 15 seconds keeps idle streams open through proxies. When too many
captures are running, the answer is `429`.

Keyword lists:

| Method | Path | Body or query |
| - | - | - |
| `GET` | `/api/capture/lists?division_id=2` | Server-wide lists and division 2's (`logs.read` there); without `division_id`, the server-wide ones (`logs.server`) |
| `POST` | `/api/capture/lists` | `{"name": "Social media", "keywords": "tiktok facebook", "division_id": 2}`; `division_id` `null` is server-wide |
| `PATCH` | `/api/capture/lists/{id}` | `{"name": "…", "keywords": "…"}`, either one |
| `DELETE` | `/api/capture/lists/{id}` | |

A list comes back as `{"id": 3, "division_id": null, "name": "Social media", "keywords": ["tiktok", "facebook"], "created_at": "…"}`.
API tokens with `logs.read` can read and use their division's lists.

## Next steps

> [card] [Query logs](https://docs.dnzwithazee.com/dns/query-logs.md)
>
> Search what clients asked in the past.

> [card] [Filtering](https://docs.dnzwithazee.com/dns/filtering.md)
>
> Blocklists, rules, client groups and schedules.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.