Privacy Webcam / Microphone Monitoring Module (PrivacyPage)

1. Overview

The Privacy module provides runtime monitoring and access-control assistance for webcam and microphone usage. It detects processes accessing device nodes and prompts the user in real time with an explicit decision workflow (Allow / Deny).

The module implements the following behaviors:

  • Automatic startup of the user service BastionGuard-privacyd.service
  • Automatic device unblock at startup (no UI blocking)
  • Real-time notifications when a new process accesses webcam/microphone devices
  • Manual refresh and optional auto-refresh every 5 minutes
  • Asynchronous logging to daily log files under the BastionGuard user data directory
  • No UI blocking: system operations and scans are executed in background threads

2. User Interface Structure

2.1 Header

The page uses a security-themed header consistent with other modules.

  • Container: Gtk::Box with CSS class secure-headerbar
  • Centered title label with CSS class secure-header-title

Displayed title:

🔒 Privacy Webcam / Microphone

2.2 Privacy Switch

A central switch (switch_webcam_) enables or disables the privacy monitoring and device state. The initial switch state is loaded from local configuration using load_privacy_state().

Switch description label:

Enable/Disable Webcam and Microphone

When the switch changes, the handler on_toggle_privacy() is invoked.


2.3 Controls: Manual Refresh and Auto-Refresh

The module provides a control bar containing:

  • Manual refresh button (btn_refresh_)
  • Auto-refresh checkbox (chk_auto_refresh_) with a 5-minute interval

Manual refresh triggers:

on_refresh_clicked()

Auto-refresh toggling triggers:

setup_auto_refresh_toggle()

2.4 Manual Device Unlock Button

A dedicated unlock action (btn_unlock_) is present but hidden by default. It becomes visible only when devices are considered blocked (for example after a “Deny” decision).

  • Label: 🔓 Unlock devices
  • Behavior: runs the helper to unblock devices without blocking the UI

This action triggers:

manual_unlock_devices()

2.5 Process List and Daily Log Viewer

The central content area is a read-only, scrollable text view (txt_processes_ in scroller_). It is used to display:

  • The current set of processes actively using webcam or microphone
  • A separator line
  • The daily log content for the current date

The view is:

  • Read-only
  • Word-wrapped
  • Populated asynchronously

2.6 Status Label

A centered status label (lbl_status_) provides user feedback on initialization, refresh operations, toggle state changes, and device block/unblock results.


3. Initialization Flow

At page construction, the module performs the following initialization sequence:

  1. Loads the last privacy enabled/disabled state from ~/.config/BastionGuard/privacy.conf
  2. Loads the auto-refresh preference (if present) from the same configuration file under the privacy group
  3. Schedules an initial load of the current-day log file into the main viewer
  4. Schedules a background initialization task that:
    • Ensures the privacy daemon is running
    • Automatically unblocks devices locally
    • Updates the process list
    • Starts the monitoring thread

Daemon check/start is performed via systemd user services:

systemctl --user is-active --quiet BastionGuard-privacyd.service
systemctl --user start BastionGuard-privacyd.service

4. Device State Management

4.1 Automatic Unblock at Startup

On startup, the module attempts to unblock webcam and microphone devices locally using the helper:

/usr/bin/bastionguard-privhelper unblock

The result is logged, and the UI remains responsive as the operation is executed outside the GTK main thread.


4.2 Toggle Handling (Block / Unblock)

The privacy switch triggers the asynchronous execution of the helper:

/usr/bin/bastionguard-privhelper block
/usr/bin/bastionguard-privhelper unblock

Execution is performed in a background thread by run_privacy_script_async(unblock). On completion, UI updates are marshaled back to the GTK main loop using Glib::signal_idle().connect_once().

To prevent concurrent toggle execution, the module uses an atomic guard (toggle_busy_) and temporarily disables the switch during execution.


4.3 Manual Unlock

When devices are blocked, the module exposes a visible “Unlock devices” button that triggers manual_unlock_devices(). This executes:

/usr/bin/bastionguard-privhelper unblock

On success, the module hides the unlock button and updates internal state (devices_blocked_ = false).


5. Process Detection (Webcam / Microphone Usage)

Active access to webcam/microphone devices is detected by scanning the /proc filesystem and resolving open file descriptor symlinks.

5.1 Device Node Prefixes

The module considers device usage when a process holds file descriptors pointing to prefixes:

  • /dev/video (video devices / webcam)
  • /dev/snd/pcm (PCM audio devices / microphone capture paths)

5.2 Detection Algorithm

detect_active_devices() performs:

  • Iterates /proc directories, filtering numeric PIDs
  • Checks each process /proc/<pid>/fd directory (skipping permission errors)
  • Reads each FD symlink target and matches against device prefixes
  • Resolves the process name from /proc/<pid>/comm
  • Returns a unique sorted list of entries formatted as:
<process_name> (PID=<pid>)

6. Real-Time Monitoring Thread

The module runs a dedicated monitoring thread (monitor_thread_) controlled by an atomic flag (monitor_running_).

6.1 Polling Interval

The monitoring loop polls every 3 seconds:

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

6.2 New Access Detection

When a new process appears in the active device access list and is not already known, it is:

  • Added to the internal set known_processes_
  • Logged to the daily log
  • Queued for user confirmation via a permission popup

Popup creation is scheduled on the GTK main loop:

Glib::signal_idle().connect_once(...)

6.3 Removal Handling

When processes stop accessing devices, they are removed from known_processes_ to keep the state consistent.


7. Permission Popups (Sequential Decision Queue)

When new device access is detected, the module shows a permission prompt allowing the user to explicitly accept or deny access. Popups are displayed sequentially to prevent multiple simultaneous dialogs.

7.1 Queue Model

The module uses:

  • A global queue alert_queue_ (process names)
  • A single active popup pointer current_alert_

New alerts are enqueued by show_permission_popup(). If no popup is currently displayed, the next alert is shown via show_next_alert().

7.2 Popup Content and Controls

The popup displays:

  • A warning title (“device access detected”)
  • The process name attempting access
  • A pending count if additional processes are queued
  • Two explicit actions: Allow / Deny

7.3 Allow Behavior

Choosing Allow:

  • Logs an “access allowed” entry
  • Closes the popup and continues with the next queued alert
  • Does not apply a device block operation

7.4 Deny Behavior (Block Devices)

Choosing Deny:

  • Logs an “access denied” entry
  • Marks devices as blocked (devices_blocked_ = true)
  • Shows the manual unlock button in the main UI
  • Executes the privileged block operation in a background thread:
/usr/bin/bastionguard-privhelper block

Once completed, the module updates the status label and refreshes the process list.


8. Manual Refresh and Auto-Refresh

8.1 Manual Refresh

Manual refresh is triggered by on_refresh_clicked(), which updates the status label, logs the action, and calls update_process_list().

8.2 Auto-Refresh

The auto-refresh option, when enabled, schedules a recurring timer every 300 seconds (5 minutes). The timer:

  • Spawns a background thread to execute update_process_list()
  • Continues running only while the checkbox remains active

The auto-refresh preference is persisted to:

~/.config/BastionGuard/privacy.conf

9. Process List Rendering and Daily Log Integration

update_process_list() runs asynchronously and builds a combined output containing:

  • Current active device-using processes
  • A visual separator
  • The daily log content for the current date

Daily log files follow the naming convention:

privacyd_YYYY-MM-DD.log

and are stored under:

~/.local/share/BastionGuard/logs

UI updates are applied on the GTK main loop with Glib::signal_idle().connect_once().


10. Asynchronous Logging and Log Retention

Logging is performed via write_log(msg) using a dedicated background thread to avoid blocking the UI.

Each log entry includes a timestamp (HH:MM:SS) and is appended to the current-day log file.

The module also implements automatic retention cleanup: log files older than 7 days are removed from the log directory.


11. Settings Persistence

The module uses a GLib keyfile configuration stored at:

~/.config/BastionGuard/privacy.conf

Stored keys include:

  • [privacy] enabled – current privacy switch state
  • [privacy] auto_refresh – auto-refresh enabled/disabled

State changes are persisted via:

  • save_privacy_state(enabled)
  • setup_auto_refresh_toggle()

12. Threading and UI Safety

  • All blocking operations (system calls, /proc scanning, file I/O, log writing) are executed in background threads.
  • All UI updates are marshaled to the GTK main loop via Glib::signal_idle() or Glib::signal_timeout().
  • The monitoring thread lifecycle is controlled by an atomic flag and is safely joined on destruction.
  • The toggle flow prevents concurrent operations by using an atomic guard (toggle_busy_) and disabling the switch during execution.

13. Security Considerations

  • Least UI privilege: privileged operations are delegated to a dedicated helper (bastionguard-privhelper), keeping the UI process unprivileged.
  • Explicit consent model: new device access attempts require explicit user decision, presented sequentially to avoid dialog flooding.
  • Controlled blocking: the “Deny” decision triggers device block actions and exposes a manual unlock path.
  • Auditability: all important events (startup, access attempts, allow/deny, block/unblock, refresh) are recorded in daily logs.