Dnsmasq Backend

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 mutations
  • static std::atomic<bool> g_loaded – indicates whether lists have been loaded from disk
  • static std::vector<std::string> g_blacklist – cached blacklist domains
  • static 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 dnsmasq
  • remove_from_blacklist(domain) – removes a domain, persists it, then reloads dnsmasq
  • add_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:

  1. Fast-path check via g_loaded
  2. Mutex lock acquisition (g_mutex)
  3. Second check to prevent race conditions
  4. Blacklist and whitelist file parsing
  5. 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:

  1. Write to <path>.tmp
  2. Close the file to ensure flush
  3. 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 true immediately; detailed failure reporting must be handled through logs or higher-level status reporting.