A local, undetectable ad sinkhole for macOS — think Pi-hole, but running entirely on your own MacBook and protecting every app (Safari, Chrome, Firefox, Electron apps, native app telemetry — the whole OS), not just one browser.
Ads and trackers are blocked at the DNS layer, so the ad host never even resolves to an IP. Nothing is injected into web pages, so there's no extension fingerprint for anti-adblock scripts to detect — blocked hosts simply look like ordinary network failures.
macOS system DNS ──► 127.0.0.1 (set via networksetup)
│
▼
┌─────────────────────────────────────────────┐
│ BottomlessHole daemon (Go, runs via launchd)│
│ • DNS server on 127.0.0.1 │
│ • blocked domain → NXDOMAIN / 0.0.0.0 │
│ • allowed domain → forwarded upstream (DoH) │
│ • SQLite query log + metrics │
│ • local HTTP API on 127.0.0.1:8353 │
│ • blocklist auto-updater (timer) │
└─────────────────────────┬─────────────────────┘
▼
┌─────────────────────────────────────────────┐
│ BottomlessHole.app (Tauri desktop UI) │
│ dashboard · metrics · whitelist · pause │
└─────────────────────────────────────────────┘
- Blocklists are pulled from live community repos (StevenBlack/hosts, OISD, Hagezi,
AdGuard, EasyList) and refreshed on a timer. Both
hostsand Adblock Plus filter syntaxes are parsed. - Whitelisting / blacklisting with exact, wildcard, and regex rules that survive blocklist updates.
- Encrypted upstream — clean queries are forwarded over DNS-over-HTTPS, so your browsing lookups aren't visible to the local network / ISP.
- Metrics — total queries, % blocked, top blocked domains, top clients, and a live query log, all in labeled tables.
- Pause / kill switch from the desktop app.
- Native macOS dashboard — a standard menu bar with keyboard shortcuts, right-click actions on any domain (whitelist / blacklist in one click), a Light/Dark theme that follows the system appearance, and window size/position that persists across launches.
| Path | What |
|---|---|
core/ |
Go daemon — DNS sinkhole, blocklist engine, metrics, HTTP API |
app/ |
Tauri desktop app (Rust shell + React/TS frontend) |
scripts/ |
Helper scripts to point macOS DNS at the daemon and back |
deploy/ |
launchd plist to run the daemon at boot |
ARCHITECTURE.md |
Design rationale and component detail |
# 1. Build & run the core daemon
cd core
go build -o ../bin/bottomlesshole . # build only the main package
sudo ../bin/bottomlesshole --config config.example.yaml # binds DNS on :53
# 2. Point macOS at it (run once; revert with scripts/unset-dns.sh)
sudo ../scripts/set-dns.sh
# 3. Run the dashboard app (connects to the daemon's API on 127.0.0.1:8353)
cd ../app
npm install
npm run tauri devThe daemon and the dashboard are independent processes: the daemon serves DNS and a
local HTTP API, and the app is only a dashboard that reads that API. tauri dev
does not start the daemon for you — run it yourself (step 1) or install it as a
background service (below).
Run the daemon as a root launchd service so it starts at boot and can bind port 53,
then point macOS at it:
cd core && go build -o ../bin/bottomlesshole . && cd ..
sudo ./scripts/install-daemon.sh # installs + loads the LaunchDaemon
sudo ./scripts/set-dns.sh # route macOS DNS through itBuild the distributable dashboard app with:
cd app && npm install && npm run tauri build # outputs BottomlessHole.app + a .dmgNote on "undetectable": DNS sinkholing is far stealthier than a browser extension, but nothing is 100%. A site can still infer blocking from a failed request and show an anti-adblock nag. Defeating those specifically needs a cosmetic-filtering browser extension layer, which is intentionally out of scope for the core daemon (see
ARCHITECTURE.md→ "Cosmetic filtering").
The dashboard shows which country each top blocked domain's server sits in. Blocked
domains are sinkholed, so their real IP is resolved on demand and looked up in a local
country database — no external geo API calls. Fetch/refresh the database with
scripts/fetch-geoip.sh (it's optional; geolocation is silently disabled without it).
IP-to-country data is © DB-IP, licensed under CC BY 4.0.
Beyond blocking, BottomlessHole attributes domains to the companies behind them (via an embedded DuckDuckGo Tracker Radar dataset, Apache-2.0). The dashboard shows:
- a "Who's Watching You" panel — the companies profiling this machine, ranked;
- company/category badges on the top-blocked and query-log lists;
- a transparent privacy score (0–100) with an explainable breakdown — it penalizes known trackers still reaching the machine via allowed queries.
Attribution is read-time only; it never touches DNS resolution. Unknown/long-tail domains simply show no badge.
Refresh the dataset: ./scripts/build-tracker-db.sh (re-compiles
core/internal/tracker/tracker_db.json), then rebuild the daemon binary.
Early scaffold. The Go core compiles and runs a working DNS sinkhole with metrics and an
HTTP API; the Tauri app provides the dashboard shell. See ARCHITECTURE.md for the
roadmap and the TODOs throughout the code.