Skip to content
ZoeHao2026Public

About

High-performance cross-platform ad blocker in Rust: domain, TLS SNI and DNS filtering over a pure, lock-free rule engine

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

NullAD

English · 简体中文

NullAD is a Rust ad and tracker blocker with a desktop interface and a headless CLI. It filters plaintext HTTP requests, TLS connections by hostname, and DNS queries. Its matching engine contains no network, filesystem, or platform integration code.

Bundled rules, local detection and proxy coexistence

NullAD ships a 44,442-domain advertising and tracking snapshot derived from official EasyList/EasyPrivacy sources at commit 129e63db3096f78e6dc94ac7ca6a15e27b5d1b79 (2026-10-05). The separate rule data is CC BY-SA 3.0; application code remains MIT. No runtime subscription download is needed. The native defaults load 44,607 rules: 165 base rules plus this domain snapshot. See sources, extraction and licensing.

Desktop/CLI enable the bundled lists and Balanced local detection by default. They can run independently or together:

Configuration Desktop CLI check / serve
Rules only Keep lists enabled; save heuristic mode Off --heuristic off
Local detection only Disable every list; keep a detection mode enabled --no-lists --heuristic balanced
Both Keep lists and a detection mode enabled Defaults, or --heuristic balanced

The independent Chrome/Edge extension 0.2.0 uses the same domain snapshot and reversible page cleanup. Both layer checkboxes are initially selected, while activation starts Off. Extract nullad-browser-extension.zip, load it unpacked, select the layers, and click a page/all-sites enable button to apply them and grant HTTP/HTTPS resource access. Changing all-sites layers preserves site allowances; explicitly enabling this page resumes protection for its host. Conservative/Balanced changes the heuristic layer, not the domain snapshot. The snapshot becomes 87 browser DNR groups; the extension downloads no lists and changes no proxy, DNS, certificates or request headers.

The native listener can explicitly chain to an unauthenticated HTTP/SOCKS5 endpoint with --upstream-proxy; upstream failure never falls back to direct routing. Existing system proxy takeover needs an explicit upstream; the independent extension preserves the existing PAC route.

The current Edge target-site sequence was Off 16/132 → heuristics only 30 → rules only 21 → both 33 → Off 16. The combined run recorded 15 client-blocked requests and passed the two script/two cosmetic checks. Totals also include independent network failures. New native checks confirm packaged 44,607-rule loading, disabled-list re-enabling, list-free SDK blocking and normal/rule-blocked traffic. Native rules + extension + an existing local HTTP upstream produced 56/132 twice; this setup has no matched old-232-rule control. This round's native normal-close acceptance is Unknown because visible QA was stopped. Earlier 232-rule / 60/132 and zero-list 16 → 30 → 16 runs are historical results from a different setup, not evidence of a measured before/after gain. No universal blocking or proxy-version acceptance is claimed. See usage and limits, enhanced acceptance, earlier acceptance and measured cost.

node --test extension/tests/*.test.cjs
.\target\release\nullad-cli.exe check http://ads.vendor.example/ad-loader.js --no-lists --heuristic balanced --type script
.\target\release\nullad-cli.exe serve --no-lists --upstream-proxy http://127.0.0.1:7890

The endpoint is an example; use your proxy software's actual port. Local detection now recognizes specific third-party ad SDK loaders and advertising delivery paths; the browser also reads short multilingual text/ARIA labels. These signals and curated domain data have different coverage and can still produce false positives.

Historical Windows acceptance (before local detection)

This table records runtime source 5ac216b and documentation head 422eb53. New checks/packages are recorded separately in the acceptance linked above.

Area Result and evidence
Windows engineering checks Pass: formatting, strict Clippy and 229 workspace tests (3 explicitly ignored); release build recorded in validation
Network and lifecycle Pass: 22 local HTTP/DNS checks; ports rebound after 20 cycles and 40 concurrent start/stop operations
Settings, rules and recovery Pass: 30 core and 32 host regressions, including failed persistence, obsolete reloads and retained failed restores
Windows system proxy Pass: explicit apply/revert and CLI Ctrl+Break restore all four raw registry values; DNS unchanged, pending=0
Native Windows desktop Pass: four pages, IPC, Chinese/English, dark theme, real traffic/logs, explicit save, restart marker and close cleanup without tray
Native minimum window Pass: final portable GUI checked on all four pages at 840x560, including the corrected log timestamp column
Privileged Windows DNS / IPv6 UDP Unknown: no privileged DNS writes; no passing native IPv6 UDP loopback observation
Tray menu / actual 125%/150% OS scaling Unknown: browser equivalents pass but do not establish native system scaling or tray menu acceptance
Delivery packages / remote CI Pass: NSIS/ZIP builds, clean-directory portable startup and Windows CI; installation/uninstallation remain Unknown. See Windows validation
macOS and Linux Deferred: known restoration gaps and no native acceptance

No executed final ordinary check failed; unverified work remains Unknown/Deferred.

A passing build or unit test does not establish successful installation, visible desktop operation, or restoration on another operating system. See Windows validation for the acceptance procedure and remaining gaps.

Build on Windows

Use Rust 1.96 or later, the MSVC toolchain, Visual Studio C++ build tools with a Windows SDK, and WebView2 Runtime for the desktop application. Node.js is needed for the UI test runner and the Tauri packaging command; Python is needed for the local traffic acceptance suite.

Run from the repository root:

cargo build --release --workspace --locked
cargo test --workspace --all-features --locked
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
node --test ui/tests/ui.test.cjs
python tests/e2e.py

When the MSVC environment needs initialization, use:

scripts\build.bat release
scripts\build.bat test

Cargo uses its normal cache and can fetch dependencies. Offline builds are an explicit option after the necessary dependencies are cached:

cargo build --release --workspace --locked --offline
scripts\build.bat release --offline

Keep Cargo.lock committed. The repository does not require a private or workspace-local Cargo registry. The UI is HTML, CSS and JavaScript; it has no separate frontend compilation step.

Windows delivery formats

The desktop installer is built with NSIS:

Push-Location crates/nullad-desktop
npx --yes @tauri-apps/cli@2.12.1 build --bundles nsis -- --locked
Pop-Location

The installer output is under target/release/bundle/nsis/. It includes the desktop resources and filter lists. The desktop loads those lists from Tauri's resource directory, so startup does not depend on the launch directory.

A portable CLI directory contains:

NullAD-cli/
  nullad-cli.exe
  lists/
    nullad-base.txt
    nullad-hosts.txt
    THIRD_PARTY_NOTICES.md

The CLI finds default lists in ./lists, then beside the executable. Use --list <PATH> to select a specific list; the option is repeatable. A portable CLI archive and an NSIS installer are separate artifacts and require separate startup checks. scripts/package-windows.ps1 produces desktop/CLI portable ZIPs, the independent browser extension ZIP, NSIS installer and SHA256SUMS.txt after both release builds complete. Extension loading is a separate browser operation.

Use

# Inspect the bundled lists and rules.
.\target\release\nullad-cli.exe load

# Evaluate a URL.
.\target\release\nullad-cli.exe check "https://ads.example.com/banner.gif"

# Include the initiating page and resource type.
.\target\release\nullad-cli.exe check "https://googletagmanager.com/gtm.js" --page "https://example.com/" --type script

# Run local HTTP and DNS listeners. Configure clients to use these endpoints.
.\target\release\nullad-cli.exe serve --port 8080 --dns-port 5353

# Opt in to changing the system proxy while serving.
.\target\release\nullad-cli.exe serve --port 8080 --system-proxy

The desktop interface defaults to Simplified Chinese, with English available in settings. Listener settings changed during protection take effect after a restart; status reports the actual bound ports and whether a restart is needed. DNS is disabled by default. Starting a DNS listener does not itself configure the operating system resolver.

Mechanism Filters Limit
HTTP proxy Plaintext HTTP URLs and paths Does not inspect encrypted HTTPS paths or response content
TLS SNI TLS connections by hostname Encrypted Client Hello can hide that hostname
DNS sinkhole Domain queries routed through its listener Cannot identify a URL path or control applications using another resolver

NullAD does not install a root certificate or decrypt HTTPS. Manual client configuration and --system-proxy are distinct: system proxy integration only covers applications that honor that platform's proxy settings. System DNS changes need administrative privileges. Low-port binding rules vary by OS; an unavailable port or insufficient privilege is reported by the listener.

CLI serve changes the system proxy only with --system-proxy, after every requested listener binds. It journals the previous settings and, on Ctrl+C (or Ctrl+Break on Windows), stops its listeners and connections before restoring and verifying those settings. A restore failure exits with an error and retains the recovery snapshot for the desktop application. An existing pending snapshot must be restored before a new CLI session can take over the system proxy.

Isolated settings and QA

By default, settings, data and logs use the operating system's per-user directories. Set NULLAD_HOME to a non-empty absolute path to isolate them:

$env:NULLAD_HOME = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'NullAD-QA'
.\target\release\nullad-desktop.exe

The isolated directory contains config/, data/ and logs/. It is an environment option, not a serialized setting. NULLAD_WEBVIEW_DATA_DIR can also isolate WebView2's user-data directory on Windows. These variables isolate application files; opting into system proxy changes still changes the real OS configuration.

Rules and loading

nullad-hosts.txt contains the 44,442-domain snapshot; the existing base list remains separate. EasyList covers advertising, while EasyPrivacy also covers analytics, tracking and telemetry. Telemetry is not synonymous with advertising. The snapshot imports only unconditional domain anchors, keeps conditional rules out, and excludes domains affected by literal-host exceptions. It is a domain subset, not execution of every EasyList/EasyPrivacy rule. See the reproducible data process.

Supported rule forms include domain anchors (||domain^), start/end anchors, separator markers, wildcards, exceptions (@@), regex literals and hosts-file entries. Resource-type, third-party, domain, match-case and important options are handled by the engine. Exceptions normally override blocking rules; an important blocking rule overrides a non-important exception.

The native engine recognizes/counts cosmetic list rules but does not apply them. The independent extension hides elements using local semantic detection; it does not import ABP cosmetic subscriptions. The $csp, $redirect and $removeparam payloads are retained by the parser but are not applied by the interceptors.

Malformed rules are quarantined. The desktop loader parses accepted content once with its configured list ID and reuses one HTTP client. Failed reads, zero-rule updates, HTML responses and excessive remote-list shrinkage do not replace the last valid rules for the same list ID and source. Changing a source does not reuse a different source's cached rules. Disabled lists remain in the desktop catalog so they can be enabled again; displayed cached counts refer only to the same ID and source. Disabling every list explicitly installs an empty rule set, while enabled local detection continues. A reload started against obsolete settings cannot publish over a newer configuration.

Recovery and platform boundaries

System changes are journaled before application. A partially failed apply attempts rollback and retains its original snapshot. Restoration checks the result before clearing that kind of entry; failed restoration remains visible and can be retried. Journal updates are serialized and written atomically. Corrupt recovery files are preserved and surfaced as an error.

Windows proxy changes use the current user's Internet Settings and broadcast the update to applications. New snapshots preserve existence, registry type and raw bytes of the four touched values; restoration sets or deletes exactly from that snapshot. Legacy proxy snapshots without that metadata remain pending with an error. Windows DNS snapshots retain an adapter GUID and the IPv4 DHCP/static mode; current interface indices are resolved again when restoring. Old DNS snapshots without a mode are retained with an error instead of guessing whether they used DHCP. IPv6 system resolver configuration is not modified by this adapter.

macOS and Linux are not accepted release targets for automatic system configuration at present:

  • macOS: service parsing and network-service identity need native validation; the generic proxy snapshot does not preserve separate HTTP/HTTPS settings or the complete PAC state. DHCP DNS currently cannot be captured by that backend.
  • Linux: proxy integration targets GNOME gsettings, rather than every desktop. Auto mode, independent HTTPS state and bypass-list restoration have known gaps. Rewriting a regular resolv.conf does not preserve all original directives; managed symlinks are refused. NetworkManager/systemd-resolved integration is not implemented.

Mobile binaries and native HTTPS decryption are outside the current scope. Browser resource blocking and page cleanup are provided by the independent extension within the documented limits.

Tests and historical performance

New default local-detection cost is separately measured in performance.md. The baseline comparison below predates those additional features.

Normal tests use mock system adapters, isolated files and local traffic fixtures. Tests that write live proxy settings are ignored unless explicitly selected. See Windows validation before running a live system test.

Five warmed runs per configuration compared the unchanged baseline and optimized release builds on the same machine. With 100,000 synthetic rules, mixed-load engine throughput was 1.030x, while actual decide with its 500-entry log was 11.485x. Allocation in that decision path fell from about 480 KB to 116 B per request; the reused engine path was essentially unchanged. See performance validation for latency, smaller workloads and all raw data. A quick CLI measurement is:

.\target\release\nullad-cli.exe bench --iterations 400000 --synthetic 300000 --json

The full comparison is recorded in docs/performance-comparison.json. Synthetic matching results describe that workload; they do not establish real-network throughput, complete Adblock Plus conformance, or another machine's performance.

Architecture

Component Responsibility
nullad-engine Parsing, indexes, matching and atomic rule-set replacement
nullad-api Shared status types and interfaces
nullad-intercept HTTP proxy, TLS hostname inspection, DNS UDP/TCP
nullad-host OS proxy/DNS adapters, filesystem paths and recovery journal
nullad-core Settings, runtime status, list loading and restoration reports
nullad-cli check, load, bench and serve
nullad-desktop + ui/ Tauri shell, IPC commands, tray and interface
extension/ Independent browser domain DNR, request heuristics and reversible DOM cleanup

The engine is shared across the CLI and desktop. The desktop owns asynchronous listener lifecycle and task cancellation; it delegates system changes and matching to the appropriate layers.

License

Application code: MIT. The derived domain snapshot is independently CC BY-SA 3.0, with attribution to the EasyList authors. Retain the data's license and change notices when redistributing it; see rule-data notices and official upstream licensing.

About

High-performance cross-platform ad blocker in Rust: domain, TLS SNI and DNS filtering over a pure, lock-free rule engine

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages