BastionGuard-privacyd

1. Overview

BastionGuard-privacyd is a headless (no-UI) privacy monitoring daemon that detects processes accessing webcam and microphone/audio capture devices. It continuously inspects active file descriptors under /proc to identify processes that hold handles to relevant device nodes and writes event logs to disk.

The daemon is intentionally UI-free: all visualization and user interaction is delegated to the separate GUI layer (PrivacyPage). This separation enables the monitoring function to remain lightweight, always-on, and independent from graphical session availability.

Primary capabilities:

  • Detects processes accessing /dev/video* (webcam) and /dev/snd/pcm* (audio capture/playback nodes)
  • Logs “start access” and “stop access” events
  • Creates daily rotating logs (per date)
  • Automatically deletes logs older than 7 days
  • Gracefully terminates on SIGINT/SIGTERM

2. Logging Subsystem

2.1 Log Location and Naming

Logs are stored under the user profile:

~/.local/share/BastionGuard/logs/privacyd_YYYY-MM-DD.log

Each log entry is appended with a timestamp in HH:MM:SS format:

[HH:MM:SS] <message>

2.2 Asynchronous Write Model

Log writes are executed asynchronously using detached threads:

  • write_log(msg) spawns a short-lived detached thread
  • A global mutex (log_mutex) serializes filesystem access to prevent concurrent write corruption
  • The log directory is created if missing (create_directories)

This approach minimizes latency in the monitoring loop by offloading I/O work, while the mutex guarantees write consistency.


2.3 Automatic Log Retention (7 Days)

After each log write, the daemon performs retention cleanup:

  • Enumerates files under ~/.local/share/BastionGuard/logs
  • Computes file age using std::filesystem::last_write_time() and a clock conversion to system_clock
  • Deletes files older than 7 days (168 hours)

Deletion errors are intentionally ignored to avoid destabilizing the daemon due to transient filesystem conditions.


3. Process and Device Access Detection

3.1 Detection Strategy

The core detection function detect_active_devices() performs a best-effort scan of the process table:

  • Iterates over numeric PID directories in /proc
  • Enumerates file descriptors under /proc/<pid>/fd
  • Reads each FD symbolic link target (read_symlink)
  • Flags the process if any FD targets begin with monitored device prefixes

Monitored device prefixes:

  • /dev/video – video capture devices (webcam)
  • /dev/snd/pcm – ALSA PCM nodes (audio device streams)

When a device access is detected, the daemon reads the process name from:

/proc/<pid>/comm

and reports it as:

<process_name> (PID=<pid>)

3.2 Permission and Robustness Handling

The daemon uses directory_options::skip_permission_denied when enumerating file descriptors and wraps symlink reads with exception handling. This design ensures:

  • Non-readable processes do not crash the daemon
  • Partial visibility (common on hardened systems) results in best-effort detection rather than failure

Detected results are sorted and deduplicated before returning, producing stable output and reducing churn in the main loop.


4. Main Monitoring Loop

4.1 State Tracking

The monitoring loop maintains an in-memory set of currently known active accessors:

std::set<std::string> known;

Each cycle:

  • New entries (present in current scan, not in known) generate “New access” logs
  • Removed entries (present in known, missing from current scan) generate “Access ended” logs

This produces explicit “start” and “stop” events rather than repeatedly logging the same access.


4.2 Sampling Interval

The loop samples every 3 seconds:

std::this_thread::sleep_for(std::chrono::seconds(3));

This interval balances responsiveness and system overhead; it can be tuned depending on deployment requirements.


5. Daemon Lifecycle and Signal Handling

5.1 Graceful Termination

The daemon installs signal handlers for:

  • SIGINT
  • SIGTERM

The handler sets a global atomic:

static std::atomic<bool> running{true};

This causes:

  • The monitoring loop to exit cleanly
  • The main thread to stop its keep-alive loop
  • The monitor thread to be joined before process exit

5.2 Thread Model

Execution model:

  • Main thread: initializes locale/i18n, installs signal handlers, starts monitoring thread, idles until termination
  • Monitor thread: runs monitor_loop() and performs detection every 3 seconds
  • Log threads: short-lived detached threads created on each write_log() call (serialized by mutex)

6. Internationalization (i18n)

The daemon supports localization using gettext:

  • setlocale(LC_ALL, "")
  • bindtextdomain("BastionGuard", LOCALEDIR)
  • bind_textdomain_codeset("BastionGuard", "UTF-8")
  • textdomain("BastionGuard")

All operator-facing log messages use _() so that PrivacyPage can present localized entries consistently.


7. Security and Operational Considerations

  • Threat model: detects only processes that expose device access via file descriptors pointing to relevant /dev nodes.
  • Coverage limitations: access mediated through other frameworks (e.g., portals, PipeWire abstractions) may not always map cleanly to the specific prefixes monitored.
  • Privilege constraints: on systems with restricted /proc visibility (e.g., hidepid), detection may be partial.
  • Performance: scanning /proc and per-process FD sets every 3 seconds can be non-trivial on high-process-count systems; the interval may be tuned.
  • Log integrity: writes are serialized via mutex; cleanup is best-effort and does not affect daemon stability.
  • Separation of concerns: daemon remains headless and stable; UI is delegated to PrivacyPage for presentation and user interaction.

In summary, BastionGuard-privacyd provides an always-on, low-complexity privacy telemetry layer for webcam/microphone access monitoring, with robust logging and retention management suitable for integration into the broader BastionGuard privacy subsystem.