milestone 21: abp list exceptions and a regex rule kind
This commit is contained in:
@@ -29,7 +29,7 @@ Directories:
|
||||
| Directory | Role |
|
||||
|---|---|
|
||||
| `src/dns/` | Pure DNS wire format: header, names, questions, records, whole packets, EDNS(0)/ECS (`edns.zig`), enums and limits (`types.zig`). No allocation, no `std.Io` beyond writing to a caller's writer. |
|
||||
| `src/filter/` | Blocklist pipeline: line parsers (hosts, domains, ABP), the compiler that turns a downloaded list into `.list`/`.wild` bodies, `domain_set.zig` (exact-match set, no Bloom filter), `matcher.zig` (the immutable snapshot every query evaluates against), per-group `rules.zig`, `wildcard.zig`, `safesearch.zig`, blocked-response synthesis (`response.zig`). Two I/O edges live here too: `fetcher.zig` (HTTP download) and `manager.zig` (files + DB + snapshot swap). |
|
||||
| `src/filter/` | Blocklist pipeline: line parsers (hosts, domains, ABP), the compiler that turns a downloaded list into `.list`/`.wild`/`.allow` bodies, `domain_set.zig` (exact-match set, no Bloom filter), `matcher.zig` (the immutable snapshot every query evaluates against), per-group `rules.zig`, `wildcard.zig`, `regex.zig` (a Pike VM for the operator's regex rules, linear-time by construction), `safesearch.zig`, blocked-response synthesis (`response.zig`). Two I/O edges live here too: `fetcher.zig` (HTTP download) and `manager.zig` (files + DB + snapshot swap). |
|
||||
| `src/local/` | Local DNS records and conditional forward zones: immutable lookup tables built once from DB rows (`records.zig`, `forward_zones.zig`), plus the plain UDP/TCP client for LAN resolvers (`forward_client.zig`). |
|
||||
| `src/cache/` | `dns_cache.zig`: bounded in-memory TTL cache of whole response messages, keyed by the question. The clock arrives as a parameter. |
|
||||
| `src/upstream/` | Upstream resolution: shared vocabulary and the `Client` interface (`transport.zig`), DoH client (RFC 8484), DoT client (RFC 7858), per-endpoint health and backoff (`health.zig`), and `pool.zig` — priority-ordered failover that is itself a `transport.Client`, so the handler sees one interface. |
|
||||
|
||||
@@ -99,7 +99,7 @@ nxdns import /tmp/nxdns-lab/backup.zon --data-dir /tmp/nxdns-lab/data-restored
|
||||
```
|
||||
|
||||
```
|
||||
info(migrations): config.db migrated from schema version 0 to 2
|
||||
info(migrations): config.db migrated from schema version 0 to 4
|
||||
imported /tmp/nxdns-lab/backup.zon
|
||||
```
|
||||
|
||||
|
||||
@@ -211,7 +211,7 @@ why the container is `docker-nxdns-1`.
|
||||
A healthy first start logs the reconcile, the authority and the bound sockets:
|
||||
|
||||
```
|
||||
info(migrations): config.db migrated from schema version 0 to 2
|
||||
info(migrations): config.db migrated from schema version 0 to 4
|
||||
reconciled '/etc/nxdns/config.zon': upstreams +1 ~0 -0; settings +45 ~0 -0;
|
||||
settings keys changed: dns.bind_ipv4 dns.bind_ipv6 dns.port web.bind web.port …
|
||||
web authentication is now enabled
|
||||
|
||||
@@ -184,8 +184,13 @@ was written before the first start.
|
||||
|
||||
0640 with group `nxdns` rather than 0600: `/etc/nxdns` is a
|
||||
`ConfigurationDirectory`, which systemd leaves owned by root, and the service
|
||||
runs as `nxdns` and has to read this file on the first start. A root-owned 0600
|
||||
file would be unreadable to it.
|
||||
runs as `nxdns`. A root-owned 0600 file would be unreadable to it.
|
||||
|
||||
Keep that group read bit for good, not just for the first boot. Under
|
||||
`run --config` the service reads this file on **every** start, so tightening
|
||||
the mode later breaks the next restart. Under database authority it is
|
||||
`nxdns import` that reads the file, as whoever runs that command, and a bare
|
||||
`nxdns run` never reads it at all.
|
||||
|
||||
Do not expect `nxdns check` to catch a permissive mode here. Its only
|
||||
permission warning is for a TLS private key
|
||||
@@ -220,7 +225,7 @@ nxdns import /etc/nxdns/config.zon
|
||||
```
|
||||
|
||||
```
|
||||
info(migrations): config.db migrated from schema version 0 to 2
|
||||
info(migrations): config.db migrated from schema version 0 to 4
|
||||
imported /etc/nxdns/config.zon
|
||||
```
|
||||
|
||||
|
||||
@@ -41,8 +41,8 @@ zig build bench -Doptimize=ReleaseFast -- filter --domains=100000 --iters=20000
|
||||
nxdns bench suite=filter domains=100000 iters=20000 seed=0x5eed optimize=ReleaseFast
|
||||
|
||||
suite ops p50(us) p95(us) p99(us) max(us)
|
||||
filter 20000 0.14 0.25 0.27 0.51
|
||||
blocked 6670/20000, Snapshot.memoryBytes 3.0 MiB, VmRSS 5.4 MiB
|
||||
filter 20000 2.38 2.76 2.88 20.32
|
||||
blocked 6670/20000, 32 regex rules, Snapshot.memoryBytes 3.0 MiB, VmRSS 5.6 MiB
|
||||
target p95 < 1ms: PASS
|
||||
target VmRSS < 100 MiB: PASS
|
||||
```
|
||||
@@ -60,8 +60,8 @@ zig build bench -Doptimize=ReleaseFast -- cache --iters=20000
|
||||
|
||||
```
|
||||
suite ops p50(us) p95(us) p99(us) max(us)
|
||||
cache 20000 0.12 0.21 0.23 0.54
|
||||
hits 10000/20000, DnsCache.memoryBytes 4.3 MiB, VmRSS 6.2 MiB
|
||||
cache 20000 0.14 0.18 0.20 5.14
|
||||
hits 10000/20000, DnsCache.memoryBytes 4.3 MiB, VmRSS 6.3 MiB
|
||||
target p95 < 5ms: PASS
|
||||
```
|
||||
|
||||
@@ -71,7 +71,7 @@ zig build bench -Doptimize=ReleaseFast -- compile --domains=100000
|
||||
|
||||
```
|
||||
suite ops p50(us) p95(us) p99(us) max(us)
|
||||
compile 100000 wall 11.512ms, 8686215 lines/s, 100000 domains kept (informational)
|
||||
compile 100000 wall 15.623ms, 6400464 lines/s, 100000 domains kept (informational)
|
||||
```
|
||||
|
||||
`--seed=N` changes the generated domains and the query order; the default is
|
||||
@@ -94,6 +94,9 @@ usage: zig build bench -Doptimize=ReleaseFast -- [filter|cache|compile|all] [--d
|
||||
is building the key, getting the entry and stamping the response id.
|
||||
- `blocked N/M` and `hits N/M` are sanity counters. The harness aborts if either
|
||||
is zero — a suite that never hits its own path measures nothing.
|
||||
- `32 regex rules` on the `filter` line is the rule set the suite loads. No
|
||||
generated query matches any of them, so every operation runs all 32 programs
|
||||
to their end, which is the costly case and the one worth measuring.
|
||||
- Two memory figures appear on purpose. `Snapshot.memoryBytes` and
|
||||
`DnsCache.memoryBytes` are the in-repo accounting of those structures; `VmRSS`
|
||||
is what the kernel holds resident for the whole process, allocator slack and
|
||||
@@ -121,8 +124,8 @@ zig build bench -Doptimize=ReleaseFast -- filter --domains=100000 --iters=20000
|
||||
```
|
||||
|
||||
```
|
||||
filter 20000 0.14 0.26 0.27 2.42
|
||||
blocked 6670/20000, Snapshot.memoryBytes 3.0 MiB, VmRSS 5.4 MiB
|
||||
filter 20000 2.34 2.71 2.85 15.06
|
||||
blocked 6670/20000, 32 regex rules, Snapshot.memoryBytes 3.0 MiB, VmRSS 5.6 MiB
|
||||
target p95 < 1ms: PASS
|
||||
target VmRSS < 100 MiB: PASS
|
||||
```
|
||||
|
||||
@@ -401,12 +401,30 @@ snapshot loaded but has no sources in it. The line
|
||||
a source in the admin interface, or a `blocklist_sources` entry to the
|
||||
configuration file with a `group_sources` link naming a group.
|
||||
|
||||
One name resolving while its neighbours are blocked is a third case, and
|
||||
`/api/lookup` answers it directly: it reports which level of the filtering
|
||||
ladder decided, and against what.
|
||||
|
||||
```sh
|
||||
curl -s 'http://127.0.0.1:8080/api/lookup?domain=api.ads.tvb.com'
|
||||
```
|
||||
|
||||
```json
|
||||
{"domain":"api.ads.tvb.com","group_id":1,"local_records":false,"forward_zone":null,"blocked":false,"reason":"blocklist_exception","matched":"api.ads.tvb.com","source_url":"https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt","safe_search_rewrite":null}
|
||||
```
|
||||
|
||||
`blocklist_exception` means a downloaded list lifted that name with an `@@`
|
||||
line, and `source_url` names the list that did it. Nothing is broken, and the
|
||||
list is not overruling you: an exception cancels only what another list blocks.
|
||||
Your own rule wins over it. Adding an exact block rule for the same name and
|
||||
asking again reports `rule_block_exact`, `blocked` true and a null `source_url`.
|
||||
|
||||
## A database stamped by a newer binary
|
||||
|
||||
**Symptom.** After putting an older binary back, it will not start:
|
||||
|
||||
```
|
||||
warning(migrations): config.db is at schema version 99; this nxdns binary supports 2
|
||||
warning(migrations): config.db is at schema version 99; this nxdns binary supports 4
|
||||
nxdns run failed: SchemaTooNew
|
||||
```
|
||||
|
||||
|
||||
@@ -307,7 +307,7 @@ opens the database immutable and never migrates, so on a database still one
|
||||
version behind it reports the mismatch and exits 2 rather than fixing it:
|
||||
|
||||
```
|
||||
FAIL /var/lib/nxdns/config.db: schema version 0, this nxdns expects 2; `nxdns run` migrates it, `check` will not
|
||||
FAIL /var/lib/nxdns/config.db: schema version 0, this nxdns expects 4; `nxdns run` migrates it, `check` will not
|
||||
```
|
||||
|
||||
That line was reproduced here against a database stamped at version 0; the path
|
||||
@@ -317,13 +317,13 @@ A fresh database is created at the current schema version; an older one is
|
||||
stepped up to it. The log line names both versions:
|
||||
|
||||
```
|
||||
info(migrations): config.db migrated from schema version 0 to 2
|
||||
info(migrations): config.db migrated from schema version 0 to 4
|
||||
```
|
||||
|
||||
> Verified on this host: that exact line is what `nxdns import` printed when it
|
||||
> created the scratch database used throughout this page. An empty data
|
||||
> directory is schema version 0, which is why a first run reports a migration
|
||||
> rather than nothing. The step from a populated older schema to 2 was not
|
||||
> rather than nothing. The step from a populated older schema to 4 was not
|
||||
> reproduced here — it needs a database written by an older binary, which this
|
||||
> host does not have.
|
||||
|
||||
@@ -332,14 +332,14 @@ binary refuses to open, so an older binary against an upgraded data directory
|
||||
fails to start:
|
||||
|
||||
```
|
||||
warning(migrations): config.db is at schema version 99; this nxdns binary supports 2
|
||||
warning(migrations): config.db is at schema version 99; this nxdns binary supports 4
|
||||
nxdns run failed: SchemaTooNew
|
||||
```
|
||||
|
||||
> Not reproduced on this host: the same missing ingredient as above, a
|
||||
> database at a schema version this binary does not support. The two lines
|
||||
> are the messages `src/storage/migrations.zig` emits, not a run captured
|
||||
> here.
|
||||
> Reproduced on this host, with one substitution: no binary from the future was
|
||||
> available, so the scratch database's `schema_version` row was set to 99 by
|
||||
> hand and `nxdns run` was pointed at it. The two lines above are that run's
|
||||
> output.
|
||||
|
||||
That run exits 1. Recovering means importing the export you took in step 1 into
|
||||
a fresh data directory with the older binary.
|
||||
|
||||
@@ -277,3 +277,31 @@ included. The password then lives where the rest of the configuration lives: set
|
||||
Request and response schemas for every operation live in the OpenAPI document:
|
||||
`src/web/openapi.yaml` in the repository, or `GET /api/openapi.yaml` from a
|
||||
running server.
|
||||
|
||||
### Block reasons
|
||||
|
||||
Three places carry the same tag: `block_reason` on a `GET /api/queries` row,
|
||||
`block_reason` on a live-stream frame, and `reason` on a `GET /api/lookup`
|
||||
answer. The tag names the level that decided the query, and the levels are
|
||||
listed here in the order they are consulted — the first one that matches wins,
|
||||
so a rule always outranks a list.
|
||||
|
||||
| Tag | Decided by |
|
||||
| --- | --- |
|
||||
| `rule_allow_exact` | An `exact` rule with action `allow` |
|
||||
| `rule_block_exact` | An `exact` rule with action `block` |
|
||||
| `rule_allow_wildcard` | A `wildcard` rule with action `allow` |
|
||||
| `rule_block_wildcard` | A `wildcard` rule with action `block` |
|
||||
| `rule_allow_regex` | A `regex` rule with action `allow` |
|
||||
| `rule_block_regex` | A `regex` rule with action `block` |
|
||||
| `blocklist_exception` | An `@@` exception line in a downloaded list |
|
||||
| `blocklist_domain` | A plain name in a downloaded list |
|
||||
| `blocklist_wildcard` | A domain anchor (`||name^`) in a downloaded list |
|
||||
|
||||
`/api/lookup` also answers `none` when nothing matched. A query row never
|
||||
carries `none`: `block_reason` is null unless the query was blocked.
|
||||
|
||||
A `cname:` prefix means the decision landed on a CNAME target rather than on
|
||||
the name the client asked for, so `cname:blocklist_domain` reads as "the list
|
||||
blocks a name this answer redirects to". Only `/api/queries` and the live
|
||||
stream show the prefix; `/api/lookup` does not follow CNAMEs.
|
||||
|
||||
@@ -62,8 +62,8 @@ reconciled '/etc/nxdns/config.zon': no changes
|
||||
|
||||
Blocklist state is not declarative and survives every reconcile: a source whose
|
||||
URL the file still names keeps its row id, its checksum, its counters and its
|
||||
compiled `<id>.list` and `<id>.wild`, so a restart in file mode downloads
|
||||
nothing. Editing a source's URL is a new identity — a new row, a new id, and a
|
||||
compiled `<id>.list`, `<id>.wild` and `<id>.allow`, so a restart in file mode
|
||||
downloads nothing. Editing a source's URL is a new identity — a new row, a new id, and a
|
||||
fresh download.
|
||||
|
||||
### Failing to start in file mode
|
||||
|
||||
@@ -279,6 +279,14 @@ Consumed by the blocklist manager (`src/filter/manager.zig`): downloaded by the
|
||||
fetcher and compiled into domain sets. A disabled source is neither downloaded
|
||||
nor loaded.
|
||||
|
||||
A list in Adblock Plus syntax may also carry exception lines, `@@||name^` and
|
||||
`@@||name`, either of which may end in `$important`. Those become allow entries
|
||||
that cancel what any attached list blocks, for the name and its subdomains. They
|
||||
cancel nothing an operator decided: every rule of the table above is checked
|
||||
first, so a downloaded list can reopen only a hole another downloaded list dug.
|
||||
Each source reports how many it carried as `exceptions`; there is no way to write
|
||||
one by hand, and no reason to want one — write an allow rule instead.
|
||||
|
||||
### group_sources
|
||||
|
||||
Which groups consult which blocklist sources.
|
||||
@@ -299,15 +307,48 @@ Per-group allow and block overrides, checked before the blocklists.
|
||||
|---|---|---|---|
|
||||
| `group` | string | required | must name a declared group |
|
||||
| `pattern` | string | required | see below |
|
||||
| `kind` | enum `.exact` \| `.wildcard` | required | — |
|
||||
| `kind` | enum `.exact` \| `.wildcard` \| `.regex` | required | — |
|
||||
| `action` | enum `.allow` \| `.block` | required | — |
|
||||
|
||||
Pattern rules: an `.exact` pattern is a plain domain name and may not contain
|
||||
`*`. A `.wildcard` pattern must contain at least one label that is exactly `*`
|
||||
(`*.tracker.example`, or `*` alone), and every other label must be a legal DNS
|
||||
label. `ads*.example` is not a valid wildcard.
|
||||
label. `ads*.example` is not a valid wildcard; a partial label is what the
|
||||
`.regex` kind is for.
|
||||
|
||||
Consumed by the filter engine's rule sets (`src/filter/rules.zig`).
|
||||
A `.regex` pattern is a regular expression matched against the whole normalized
|
||||
lowercase name, unanchored unless you write `^` or `$` — the POSIX-grep
|
||||
convention. It is stored exactly as you typed it, which the other two kinds are
|
||||
not: lowercasing would turn `\D` into `\d`, and trimming a trailing `.` would
|
||||
delete an any-byte atom. The engine (`src/filter/regex.zig`) accepts literal
|
||||
bytes, `.` for any byte, character classes `[a-z0-9]` with a leading `^` for
|
||||
negation, the escapes `\d` and `\w` plus `\` before any other ASCII punctuation
|
||||
to make it a literal, the repetitions `*` `+` `?` `{n}` `{n,m}` `{n,}`,
|
||||
alternation `|`, grouping `(...)`, and the anchors `^` and `$`.
|
||||
|
||||
Everything else is refused at the edge rather than approximated, so a pattern
|
||||
written for another engine fails where you can read the diagnostic instead of
|
||||
silently matching names you did not mean:
|
||||
|
||||
- backreferences, lookaround, captures, named groups, Unicode classes and the
|
||||
`(?…)` prefix they share;
|
||||
- any alphanumeric escape the list above omits — `\s`, `\b`, `\1`, `\D`;
|
||||
- a `]` inside a class, unless written `\]`;
|
||||
- an empty pattern, and an empty branch: `ads|` is refused rather than read as a
|
||||
pattern that matches every name;
|
||||
- a quantifier applied straight to another quantifier: `a+?` is refused rather
|
||||
than read as `(a+)?`, which matches every name. Write `(a+)?` to mean that.
|
||||
|
||||
A pattern is at most 256 bytes and compiles to at most 1024 instructions, each
|
||||
limit with its own diagnostic, and one group holds at most 256 regex rules.
|
||||
Groups do not capture, and the engine simulates every alternative in lockstep,
|
||||
so a pattern costs at most its compiled length times the length of the name —
|
||||
`(a+)+b` is as cheap here as it is expensive in a backtracking engine.
|
||||
|
||||
Consumed by the filter engine's rule sets (`src/filter/rules.zig`), which checks
|
||||
the three kinds in the order they are listed above, allow before block within
|
||||
each. Regex is checked last of the three because it is the only kind that costs
|
||||
more than a hash lookup or a label walk.
|
||||
|
||||
### local_records
|
||||
|
||||
@@ -548,10 +589,12 @@ upstream. Everything else keeps its default.
|
||||
.{ .group = "kids", .source_url = "https://lists.example/ads.txt" },
|
||||
},
|
||||
|
||||
// Overrides beat blocklists. Wildcards need a label that is exactly "*".
|
||||
// Overrides beat blocklists. Wildcards need a label that is exactly "*";
|
||||
// a partial label takes a regex, which is unanchored unless you say "^".
|
||||
.rules = .{
|
||||
.{ .group = "default", .pattern = "allowed.example", .kind = .exact, .action = .allow },
|
||||
.{ .group = "kids", .pattern = "*.tracker.example", .kind = .wildcard, .action = .block },
|
||||
.{ .group = "kids", .pattern = "^ad[0-9]+-", .kind = .regex, .action = .block },
|
||||
},
|
||||
|
||||
// Local names, answered without any upstream.
|
||||
|
||||
@@ -46,19 +46,21 @@ older ones from the main file. That is the "uncheckpointed changes" failure in
|
||||
| `blocklists/` | Compiled blocklist snapshots, one subdirectory of the data directory. | 0700 |
|
||||
| `blocklists/<id>.list` | Exact domains for blocklist source `<id>`, one per line, behind a header. | 0600 |
|
||||
| `blocklists/<id>.wild` | Wildcard entries for the same source. | 0600 |
|
||||
| `blocklists/<id>.raw.tmp`, `<id>.list.tmp`, `<id>.wild.tmp` | Transient refresh state: the downloaded body and the two compile outputs before they are published by rename. | 0600 |
|
||||
| `blocklists/<id>.allow` | Exception entries for the same source: the names its `@@` lines lift. Absent on a source compiled before exceptions were honoured, which reads as empty. | 0600 |
|
||||
| `blocklists/<id>.raw.tmp`, `<id>.list.tmp`, `<id>.wild.tmp`, `<id>.allow.tmp` | Transient refresh state: the downloaded body and the three compile outputs before they are published by rename. | 0600 |
|
||||
|
||||
`<id>` is the `blocklist_sources` row id.
|
||||
|
||||
### The orphan sweep
|
||||
|
||||
The sweep decides by id, not by suffix. It matches all five names above and
|
||||
The sweep decides by id, not by suffix. It matches all seven names above and
|
||||
deletes those whose `<id>` is no longer a `blocklist_sources` row, so the
|
||||
compiled `.list` and `.wild` of a removed source go, and so do a `.raw.tmp`,
|
||||
`.list.tmp` or `.wild.tmp` left behind by a refresh that was killed before it
|
||||
could clean up. Files belonging to a source that still has a row are never
|
||||
touched, whatever state they are in: the sweep holds the same lock every refresh
|
||||
takes, so it never reads the directory while a refresh is part-way through.
|
||||
compiled `.list`, `.wild` and `.allow` of a removed source go, and so do a
|
||||
`.raw.tmp`, `.list.tmp`, `.wild.tmp` or `.allow.tmp` left behind by a refresh
|
||||
that was killed before it could clean up. Files belonging to a source that still
|
||||
has a row are never touched, whatever state they are in: the sweep holds the same
|
||||
lock every refresh takes, so it never reads the directory while a refresh is
|
||||
part-way through.
|
||||
|
||||
It runs at three moments:
|
||||
|
||||
@@ -86,7 +88,7 @@ losing the refresh pass behind them, let alone the server.
|
||||
|
||||
The temporaries of a source that still exists are cleaned by the refresh that
|
||||
owns them rather than by the sweep: each refresh deletes its own `.raw.tmp`,
|
||||
`.list.tmp` and `.wild.tmp` as it finishes, successfully or not.
|
||||
`.list.tmp`, `.wild.tmp` and `.allow.tmp` as it finishes, successfully or not.
|
||||
|
||||
A `querylog.db` is moved aside when it is missing nothing but usability:
|
||||
SQLite reports it corrupt or not a database, `PRAGMA quick_check` does not
|
||||
|
||||
@@ -27,7 +27,7 @@ the asset-free figure is a real measurement rather than an estimate.
|
||||
|
||||
## Measured: x86_64 development host
|
||||
|
||||
Date: 2026-08-02. Hardware and build: Intel Core i7-14700K, Linux 6.18,
|
||||
Date: 2026-08-13. Hardware and build: Intel Core i7-14700K, Linux 6.18,
|
||||
Zig 0.16.0, `-Doptimize=ReleaseFast`, harness defaults (1,000,000 domains,
|
||||
200,000 iterations per suite, seed 0x5eed).
|
||||
|
||||
@@ -37,18 +37,26 @@ baseline for regressions on the machine development happens on.
|
||||
|
||||
```
|
||||
suite ops p50(us) p95(us) p99(us) max(us)
|
||||
filter 200000 0.11 0.18 0.27 16.41
|
||||
blocked 66699/200000, Snapshot.memoryBytes 28.0 MiB, VmRSS 31.8 MiB
|
||||
filter 200000 2.41 2.80 2.97 22.16
|
||||
blocked 66699/200000, 32 regex rules, Snapshot.memoryBytes 28.0 MiB, VmRSS 32.0 MiB
|
||||
target p95 < 1ms: PASS
|
||||
target VmRSS < 100 MiB: PASS
|
||||
cache 200000 0.10 0.14 0.17 3.53
|
||||
hits 100000/200000, DnsCache.memoryBytes 4.3 MiB, VmRSS 7.6 MiB
|
||||
cache 200000 0.11 0.17 0.22 5.40
|
||||
hits 100000/200000, DnsCache.memoryBytes 4.3 MiB, VmRSS 7.7 MiB
|
||||
target p95 < 5ms: PASS
|
||||
compile 1000000 wall 96.025ms, 10413949 lines/s, 1000000 domains kept (informational)
|
||||
compile 1000000 wall 98.597ms, 10142224 lines/s, 1000000 domains kept (informational)
|
||||
```
|
||||
|
||||
Every in-process §18 target passes on this host: the two latency targets by
|
||||
three to four orders of magnitude, the memory target by about 3x.
|
||||
Every in-process §18 target passes on this host: the filter target by about
|
||||
360x, the cache target by about four orders of magnitude, the memory target by
|
||||
about 3x.
|
||||
|
||||
The filter suite loads 32 regex rules that no query in the mix matches, which is
|
||||
the expensive case rather than the cheap one: the regex levels sit below every
|
||||
hash and wildcard level, so a name no pattern matches is the name that runs all
|
||||
32 programs to their end. Every op pays that, which is what moved the filter p95
|
||||
from 0.18 µs before regex rules existed to the 2.80 µs above. The margin against
|
||||
the 1 ms target is what makes paying it on every miss an acceptable price.
|
||||
|
||||
### The two memory figures
|
||||
|
||||
|
||||
+42
-25
@@ -9,13 +9,13 @@ delete the directory.
|
||||
Follow the steps in order. Each one says what it did.
|
||||
|
||||
Every command below was executed on x86_64 Linux with Zig 0.16.0, Node.js
|
||||
24.14.1, dig 9.20.26 and curl 8.21.0. Steps 4 to 11, 13 and 14 were re-run end
|
||||
to end for this revision, and the transcripts are that run's output with the
|
||||
24.14.1, dig 9.20.26 and curl 8.21.0. Steps 2, 4 to 11, 13 and 14 were re-run
|
||||
end to end for this revision, and the transcripts are that run's output with the
|
||||
tutorial directory substituted. Two things were not re-run: the browser page in
|
||||
step 12 — its endpoints were exercised, the page itself was not opened — and the
|
||||
two build commands in steps 1 and 2, which had already produced the binary under
|
||||
test. The ZON block at the end of step 14 was checked with `nxdns check
|
||||
--config` rather than started.
|
||||
`npm` build in step 1, whose `web/dist` was already on disk and is the one the
|
||||
binary under test embeds. The ZON block at the end of step 14 was checked with
|
||||
`nxdns check --config` rather than started.
|
||||
|
||||
## What you need
|
||||
|
||||
@@ -109,7 +109,7 @@ zig-out/bin/nxdns import ~/nxdns-tutorial/config.zon --data-dir ~/nxdns-tutorial
|
||||
```
|
||||
|
||||
```
|
||||
info(migrations): config.db migrated from schema version 0 to 2
|
||||
info(migrations): config.db migrated from schema version 0 to 4
|
||||
imported /home/you/nxdns-tutorial/config.zon
|
||||
```
|
||||
|
||||
@@ -127,7 +127,7 @@ zig-out/bin/nxdns run --data-dir ~/nxdns-tutorial/data
|
||||
|
||||
```
|
||||
info(querylog_schema): created querylog database '/home/you/nxdns-tutorial/data/querylog.db'
|
||||
info(blocklist_manager): blocklist snapshot generation 1: 0 of 0 sources loaded, 199 bytes
|
||||
info(blocklist_manager): blocklist snapshot generation 1: 0 of 0 sources loaded, 231 bytes
|
||||
info(nxdns): authority: database
|
||||
info(nxdns): nxdns <version> serving on udp [::1]:15353 udp 127.0.0.1:15353 tcp [::1]:15353 tcp 127.0.0.1:15353; 1 upstream(s); blocklist generation 1
|
||||
info(web_server): web interface listening on 127.0.0.1:8080
|
||||
@@ -146,8 +146,8 @@ dig @127.0.0.1 -p 15353 example.com A +noall +answer
|
||||
```
|
||||
|
||||
```
|
||||
example.com. 90 IN A 172.66.147.243
|
||||
example.com. 90 IN A 104.20.23.154
|
||||
example.com. 229 IN A 172.66.147.243
|
||||
example.com. 229 IN A 104.20.23.154
|
||||
```
|
||||
|
||||
nxdns had no answer cached, so it forwarded the query to
|
||||
@@ -220,20 +220,27 @@ curl -s -X POST http://127.0.0.1:8080/api/blocklists/update
|
||||
```
|
||||
|
||||
```json
|
||||
{"sources":[{"id":1,"state":"ok","loaded":true,"last_attempt":1786473715,"last_success":1786473715,"url":"https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts","last_error":"","domains":99559,"wildcards":0,"skipped_regex":0}]}
|
||||
{"sources":[{"id":1,"state":"ok","loaded":true,"last_attempt":1786629237,"last_success":1786629238,"url":"https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts","last_error":"","domains":97648,"wildcards":0,"exceptions":0,"skipped_regex":0}]}
|
||||
```
|
||||
|
||||
The three zeros are the parts of this list that a hosts file cannot have.
|
||||
`wildcards` counts entries covering a name and its subdomains, `exceptions`
|
||||
counts the `@@` lines an Adblock Plus list uses to lift a name another list
|
||||
blocks, and `skipped_regex` counts the regex lines nxdns declines to take from a
|
||||
downloaded list. Only the Adblock Plus syntax writes any of them; a hosts file is
|
||||
one name per line.
|
||||
|
||||
The download is about 3 MB and takes a few seconds. Watch the first terminal
|
||||
until this appears:
|
||||
|
||||
```
|
||||
info(blocklist_manager): blocklist snapshot generation 6: 1 of 1 sources loaded, 3096006 bytes
|
||||
info(blocklist_manager): blocklist snapshot generation 6: 1 of 1 sources loaded, 2523973 bytes
|
||||
```
|
||||
|
||||
`1 of 1 sources loaded` is the line to wait for. nxdns builds each blocklist
|
||||
snapshot in full and swaps it in atomically, so queries keep being answered from
|
||||
the previous snapshot the whole time the new one is being built. After this, the
|
||||
domain count in the JSON above — 99559 on the day this was run — is live.
|
||||
domain count in the JSON above — 97648 on the day this was run — is live.
|
||||
|
||||
From here on, the list is on disk under `~/nxdns-tutorial/data/blocklists`.
|
||||
Restarting nxdns does not re-download it.
|
||||
@@ -259,16 +266,18 @@ dig @127.0.0.1 -p 15353 wikipedia.org A +noall +answer
|
||||
```
|
||||
|
||||
```
|
||||
wikipedia.org. 17 IN A 185.15.58.224
|
||||
wikipedia.org. 130 IN A 185.15.58.224
|
||||
```
|
||||
|
||||
One thing to know before you try other names: a blocklist entry blocks exactly
|
||||
the name it names. `doubleclick.net` on the list does not block
|
||||
`ads.doubleclick.net`; that name is blocked because the list happens to contain
|
||||
it too. Blocklist entries do not walk up the parent chain — only rules you write
|
||||
yourself can, with a wildcard pattern such as `*.doubleclick.net`. So when you
|
||||
pick a domain to test, pick one that is literally in the file.
|
||||
`www.google-analytics.com` is another that is.
|
||||
One thing to know before you try other names: an entry in a hosts list blocks
|
||||
exactly the name it names. `doubleclick.net` is on this list, and so are
|
||||
`ad.doubleclick.net` and `www.google-analytics.com`. `ads.doubleclick.net` is
|
||||
not on it, and nxdns does not block it — the entry for the parent says nothing
|
||||
about the child. Two things do walk up the parent chain, and neither is in play
|
||||
here: a rule you write yourself, with a wildcard pattern such as
|
||||
`*.doubleclick.net`, and an Adblock Plus list's `||doubleclick.net^`, which
|
||||
covers the name and everything under it. This list is a hosts file, so when you
|
||||
pick a domain to test against it, pick one that is literally in the file.
|
||||
|
||||
## 12. Open the web interface
|
||||
|
||||
@@ -291,7 +300,7 @@ nxdns catches SIGINT and SIGTERM, stops serving and exits 0.
|
||||
Start it again with the same command as in step 6 and read the first log lines:
|
||||
|
||||
```
|
||||
info(blocklist_manager): blocklist snapshot generation 1: 1 of 1 sources loaded, 3096006 bytes
|
||||
info(blocklist_manager): blocklist snapshot generation 1: 1 of 1 sources loaded, 2523973 bytes
|
||||
info(nxdns): authority: database
|
||||
```
|
||||
|
||||
@@ -314,8 +323,12 @@ zig-out/bin/nxdns run --data-dir ~/nxdns-tutorial/data --config ~/nxdns-tutorial
|
||||
|
||||
```
|
||||
reconciled '/home/you/nxdns-tutorial/config.zon': sources +0 ~0 -1; group_sources +0 ~0 -1;
|
||||
info(blocklist_manager): blocklist snapshot generation 1: 0 of 0 sources loaded, 199 bytes
|
||||
info(blocklist_manager): blocklist snapshot generation 1: 0 of 0 sources loaded, 231 bytes
|
||||
info(nxdns): authority: file (/home/you/nxdns-tutorial/config.zon)
|
||||
info(nxdns): nxdns <version> serving on udp [::1]:15353 udp 127.0.0.1:15353 tcp [::1]:15353 tcp 127.0.0.1:15353; 1 upstream(s); blocklist generation 1
|
||||
info(blocklist_manager): pruned orphaned blocklist file 1.allow
|
||||
info(blocklist_manager): pruned orphaned blocklist file 1.wild
|
||||
info(blocklist_manager): pruned orphaned blocklist file 1.list
|
||||
```
|
||||
|
||||
**Read that first line.** The blocklist source is gone. That is not a bug — it is
|
||||
@@ -324,8 +337,12 @@ source, and in file mode the file is the complete statement of what the
|
||||
configuration is, so anything the database holds that the file does not name is
|
||||
removed at every start. The reconcile said so in one line before doing it.
|
||||
|
||||
The compiled list is still on disk and the query log is untouched; what changed
|
||||
is the configuration, and it now matches the file exactly.
|
||||
The three `pruned` lines are the rest of that removal: with the row gone, the
|
||||
compiled files it owned belong to nobody, so the sweep that runs at every start
|
||||
deletes them. The three names are the three bodies one source compiles into —
|
||||
exact domains, wildcards, and the exceptions an Adblock Plus list can lift. The
|
||||
query log is untouched; what changed is the configuration, and it now matches
|
||||
the file exactly.
|
||||
|
||||
Neither mode is the "advanced" one. Database mode suits a box someone
|
||||
administers through the web interface. File mode suits a file kept in git and
|
||||
@@ -345,7 +362,7 @@ Ctrl-C to stop it.
|
||||
|
||||
## What you have now
|
||||
|
||||
A resolver that answers real queries, a real blocklist of about 99000 domains
|
||||
A resolver that answers real queries, a real blocklist of about 98000 domains
|
||||
attached to the default group, a query log, and a web interface — all inside one
|
||||
directory you can delete:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user