Skip to content
hollow

Core mechanics

Blocking

Three published list formats, read by the same parser, held in two maps. The StevenBlack list — the one nearly everybody loads — is 79,746 entries and 5.5 MB of heap, and a lookup is one map access plus, on a miss, a walk of at most a handful of parent names.

Formats

No configuration picks between them. Each line is classified as it is read, and a line in no recognised format is counted and skipped rather than being fatal — public lists routinely carry a few lines in a syntax their own tooling ignores.

Hosts
0.0.0.0 ads.doubleclick.net
127.0.0.1 tracker.example.com

Field one is parsed as an address rather than matched against 0.0.0.0, because the last line of a real preamble is an IPv6 literal. What the address is does not matter: a null route and a loopback both say the name should not resolve.

Domain per line
telemetry.analytics.io

One name, no address. Matches that name exactly and nothing under it.

Adblock
||malicious-tracking.com^

The name and everything under it. The caret is what ends the domain in adblock syntax, and a line missing it is skipped rather than guessed at.

The preamble trap

A real hosts file opens with localhost, localhost.localdomain, local and broadcasthost, all sitting in field two. A parser that takes field two ingests every one of them, and a resolver that blocks localhost breaks the machine it is running on. Those names, and the ip6- ones that Debian and Ubuntu put in /etc/hosts, are skipped by name.

How a blocked name is answered

Each mode is internally consistent, which is the requirement that shapes the whole thing. Returning 0.0.0.0 for A and NXDOMAIN for AAAA is the obvious implementation of “null” and it is wrong: the A answer asserts the name exists and the AAAA answer asserts it does not, so a dual-stack client that asks both — as every browser does — gets two contradictory statements about one name.

  • nxdomaindefault

    The name does not exist. The only answer that is true in the same way for every query type, and the one a client stops asking about.

  • null

    A and AAAA get an unroutable address; every other type gets NODATA. Slower to fail, since the client usually tries to connect and waits out the timeout.

  • nodata

    The name exists and has no records of the type asked for. NOERROR with an empty answer section.

Both negative shapes carry a synthetic SOA in the authority section. Without it a resolver downstream has no TTL to cache the denial against and will ask again for every single query.

Allowlists

An allowlist entry wins outright over every kind of block rule. It is an operator saying they know this name is on a list and they want it anyway, which is a statement about intent that no amount of matching on the block side should be able to overrule.

$hollow serve --block hosts.txt --allow keep.txt

What is honoured, and what is skipped

Adblock $ options are honoured without their conditions. ||ads.example^$third-party asks for a domain to be blocked only in third-party context, which a resolver cannot observe — so the block is applied everywhere, and is broader than the rule asked for. Dropping such rules instead would quietly unblock most of a real filter list.

Element hiding, @@ exception rules and regular-expression rules mean things a DNS resolver cannot express at all. Those are counted and skipped, and the count is printed at startup, because a half-honoured filter rule is worse than one that was visibly ignored.