USBD – USB Monitoring Daemon (libudev + systemd sd-bus)

1. Overview

BastionGuard-usbd is a reliable USB monitoring daemon that detects removable USB storage devices and notifies other BastionGuard components through systemd D-Bus (sd-bus). The daemon is built on top of libudev for device enumeration and uses a periodic polling loop to ensure stable behavior across distributions and desktop environments.

Main capabilities:

  • Detect USB removable disks (block devices) using libudev and strict filtering
  • Emit D-Bus signals on device add/remove events over the system bus
  • Write daily logs with automatic rotation (older than 7 days)
  • Localization-ready startup by loading BastionGuard language configuration before gettext initialization

2. Global Runtime Model

2.1 Lifecycle Flag and Signal Handling

The daemon lifecycle is controlled by:

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

Signal handlers terminate the daemon cleanly:

  • SIGINT and SIGTERMhandle_signal() sets running = false

The main thread waits until running becomes false, then joins the monitor thread and releases D-Bus resources.


2.2 D-Bus Connection

The daemon connects to the system bus using sd-bus:

  • sd_bus_open_system(&bus)
  • sd_bus_request_name(bus, "org.BastionGuard.USBD", 0)

Signals are emitted under:

  • Object path: /org/BastionGuard/USBD
  • Interface: org.BastionGuard.USBD

3. Logging and Log Rotation

3.1 Log Location Selection

write_log() writes to different base directories depending on privilege level:

  • If running as root: /var/log/BastionGuard
  • Otherwise: ~/.local/share/BastionGuard/logs

This supports both system service deployments and user-context execution.


3.2 Daily Log File Naming

Logs are written to a daily file named:

usbd_YYYY-MM-DD.log

Each entry uses a time prefix:

[HH:MM:SS] message

Logging is protected by log_mutex to prevent interleaving when multiple threads write concurrently.


3.3 Retention Policy (7 Days)

On each log write, the daemon enforces retention by scanning the log directory and deleting files older than 7 days based on file write time. This provides a simple built-in rotation mechanism without external dependencies.


4. USB Device Model

Detected devices are represented by:

  • name – a human-friendly name assembled from manufacturer/product or fallback to devnode
  • devnode – the device node path (e.g., /dev/sdb)
  • serial – device serial (if provided by udev sysattrs)

5. Device Enumeration (Reliable USB Detection)

5.1 Enumeration Strategy

detect_usb_devices() uses libudev enumeration to scan the block subsystem:

  • udev_enumerate_add_match_subsystem(enumerate, "block")
  • udev_enumerate_scan_devices(enumerate)

Each enumerated sysfs entry is converted into a udev_device instance and filtered to identify USB removable disks.


5.2 Filtering Criteria

A device is considered a valid USB removable disk only if it meets all of the following:

  • Block device type is “disk” (not a partition):
    • udev_device_get_devtype(dev) == "disk"
  • Has a USB parent device (strict association):
    • udev_device_get_parent_with_subsystem_devtype(dev, "usb", "usb_device")
  • SIZE > 0 to avoid empty readers until media is inserted:
    • sysattr size must exist and be non-zero
  • Device is physically removable:
    • sysattr removable must be "1"
  • Has a valid devnode:
    • udev_device_get_devnode(dev) must not be null

When a device passes filters, the daemon extracts:

  • manufacturer, product, and serial from the USB parent sysattrs

If manufacturer/product are missing, the name defaults to the devnode.


6. D-Bus Signaling

6.1 Device Added Signal

When a new device is detected, the daemon emits:

  • Signal name: DeviceAdded
  • Signature: sss (three strings)
  • Payload:
    • Device name
    • Device devnode
    • Device serial

The signal is sent from:

/org/BastionGuard/USBD
org.BastionGuard.USBD.DeviceAdded

6.2 Device Removed Signal

When a previously known device disappears, the daemon emits:

  • Signal name: DeviceRemoved
  • Signature: s (one string)
  • Payload: devnode

7. Monitor Loop and Event Semantics

7.1 Known Set Tracking

The monitor loop maintains a set of known devices by devnode:

  • std::set<std::string> known

This provides stable add/remove semantics even across repeated enumeration runs.


7.2 Polling Interval

The loop executes every 2 seconds:

  • Enumerate devices
  • Detect additions (present in current list but not in known)
  • Detect removals (present in known but not in current list)
  • Sleep for 2 seconds

This approach is robust across distros and avoids complex event-driven edge cases, while maintaining acceptable responsiveness for user interactions.


8. Localization Bootstrapping

8.1 Loading Language Configuration Early

Before initializing gettext and locale, the daemon loads environment overrides from:

~/.config/BastionGuard/lang.conf

Each non-empty, non-comment line is parsed as:

KEY=VALUE

Valid entries are applied via setenv(). This ensures that subsequent calls to:

  • setlocale(LC_ALL, "")
  • gettext initialization (bindtextdomain, textdomain)

operate under the correct user-selected locale without requiring a restart of the daemon.


9. Main Execution Flow

  1. Load lang.conf (if present) and apply env
  2. Call setlocale(LC_ALL, "")
  3. Initialize gettext domain (BastionGuard) and UTF-8 codeset
  4. Register signal handlers
  5. Open system D-Bus and request name org.BastionGuard.USBD
  6. Write startup log entry
  7. Start monitor_loop thread
  8. Wait until running == false
  9. Join thread, unref D-Bus, write shutdown log, exit

10. Runtime and Security Considerations

  • Strict USB filtering: requiring a USB parent, removable flag show “1”, and non-zero size prevents false detections (e.g., internal disks or empty readers).
  • System bus usage: signals are emitted on the system bus; consumers should enforce authorization policies as needed.
  • Polling vs event-driven: polling every 2 seconds is predictable and stable, but can be replaced by udev monitor events if lower latency is required.
  • Logging privacy: logs include device names and devnodes; retention is limited to 7 days.
  • Thread safety: logging is mutex-protected and D-Bus calls are made only when the bus is available.
  • Localization correctness: applying language settings before gettext initialization ensures consistent translated messages in logs and stderr.