1. Overview
The DnsmasqBackend module provides a lightweight, asynchronous backend for domain-based blocking using dnsmasq. It maintains a blacklist (translated into dnsmasq address=/domain/127.0.0.1 rules) and a whitelist (stored as plain domain lines). The implementation is optimized for UI responsiveness through:
- Lazy loading (lists are loaded only when first needed)
- Threaded operations (non-blocking add/remove functions)
- Atomic writes (temporary file + rename strategy)
- Systemd integration (dnsmasq reload/restart via
systemctl)
2. Responsibilities and Scope
This backend is responsible for:
- Loading blacklist/whitelist content from configured file paths
- Maintaining in-memory cached lists to avoid repeated disk reads
- Adding/removing domains from the cached lists
- Persisting updated lists to disk using atomic write semantics
- Triggering dnsmasq reload/restart to apply updated blacklist rules
The constructor is intentionally minimal and performs no I/O, ensuring instant initialization and deferring all heavy work to the first operation.
3. Responsibilities in the BastionGuard Stack
The backend is typically used by higher-level modules (e.g., Anti-Phishing, DNS filtering, or UI management panels) that need a simple domain enforcement mechanism. It bridges:
- UI-level actions (user adds/removes a domain)
- Persistent storage (blacklist/whitelist files)
- Runtime enforcement (dnsmasq service reload/restart)
4. Internal State and Concurrency Model
4.1 Global Cache and Synchronization
The module uses a shared in-memory cache and synchronization primitives to provide safe concurrent access:
static std::mutex g_mutex– protects shared list access and mutationsstatic std::atomic<bool> g_loaded– indicates whether lists have been loaded from diskstatic std::vector<std::string> g_blacklist– cached blacklist domainsstatic std::vector<std::string> g_whitelist– cached whitelist domains
All mutations (add/remove) are protected by g_mutex. The lazy-load routine uses a double-check strategy (atomic fast path + mutex-protected initialization) to ensure the lists are loaded only once.
4.2 Asynchronous Public API
All public mutation operations execute in detached background threads:
add_to_blacklist(domain)– adds a domain, persists it, then reloads dnsmasqremove_from_blacklist(domain)– removes a domain, persists it, then reloads dnsmasqadd_to_whitelist(domain)– adds a domain and persists it (no dnsmasq reload)remove_from_whitelist(domain)– removes a domain and persists it (no dnsmasq reload)
Each function returns immediately (always true), while the actual work (load, update, write, reload) runs asynchronously. This prevents blocking the UI thread in GTK/QT front-ends.
5. Lazy Load Pipeline
5.1 Lazy Initialization Guard
List loading is deferred until the first operation calls ensure_loaded(). The routine performs:
- Fast-path check via
g_loaded - Mutex lock acquisition (
g_mutex) - Second check to prevent race conditions
- Blacklist and whitelist file parsing
- Cache activation by setting
g_loaded = true
5.2 Blacklist Parsing Rules
Blacklist files are assumed to contain dnsmasq rules. Parsing extracts the domain from lines matching the pattern:
address=/example.com/127.0.0.1
The parser searches for the substring address=/, then extracts the domain token up to the next /.
5.3 Whitelist Parsing Rules
Whitelist files are treated as plain text. Each non-empty line is stored as a raw domain entry without dnsmasq syntax.
6. Persistence and Atomic Writes
6.1 Atomic Write Strategy
Persistence is implemented through atomic_write(path, content) using a temp-file + rename approach:
- Write to
<path>.tmp - Close the file to ensure flush
- Replace the target with
std::filesystem::rename()
This minimizes corruption risk if the system crashes mid-write. The operation returns false on I/O or filesystem failures.
6.2 Blacklist Serialization (dnsmasq rules)
Blacklist content is generated by make_dnsmasq_entries(domains) as:
address=/domain/127.0.0.1
One entry per line is written to the configured blacklist path.
6.3 Whitelist Serialization (plain list)
Whitelist persistence writes one domain per line to the configured whitelist path, with no additional formatting.
7. Service Application (dnsmasq Reload)
7.1 Reload/Restart Execution
After any blacklist change, the backend calls reload_dnsmasq(), implemented as:
systemctl --quiet try-reload-or-restart dnsmasq.service
The backend evaluates success using WIFEXITED() and WEXITSTATUS() == 0. This allows seamless application of changes when dnsmasq supports reload, and falls back to restart when required.
8. Runtime and Security Considerations
- Non-blocking UI behavior: all operations run in detached threads and return immediately.
- Data integrity: atomic write semantics reduce the risk of partial file corruption.
- Thread safety: a shared mutex protects both blacklist and whitelist caches.
- Idempotency: add operations skip insertion if the domain already exists.
- Operational coupling: dnsmasq reload is tied to blacklist changes to ensure enforcement consistency.
- Error visibility: public methods currently return
trueimmediately; detailed failure reporting must be handled through logs or higher-level status reporting.