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 tosystem_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:
SIGINTSIGTERM
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
/devnodes. - 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
/procvisibility (e.g., hidepid), detection may be partial. - Performance: scanning
/procand 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.