diff --git a/bin/input-monitor.c b/bin/input-monitor.c
index 0276aa0dd2687c38236c97ec85145d5f1b1a51d7..00089a6cf035db59cb46e6d3594337f1c6f3a701 100644
--- a/bin/input-monitor.c
+++ b/bin/input-monitor.c
@@ -159,6 +159,7 @@ interrupt_cb (void *user_data)
   return G_SOURCE_CONTINUE;
 }
 
+static SrtInputDeviceMonitorFlags opt_mode = SRT_INPUT_DEVICE_MONITOR_FLAGS_NONE;
 static SrtInputDeviceMonitorFlags opt_flags = SRT_INPUT_DEVICE_MONITOR_FLAGS_NONE;
 static gboolean opt_evdev = FALSE;
 static gboolean opt_hidraw = FALSE;
@@ -174,8 +175,30 @@ opt_once_cb (const gchar *option_name,
   return TRUE;
 }
 
+static gboolean
+opt_direct_cb (const gchar *option_name,
+               const gchar *value,
+               gpointer data,
+               GError **error)
+{
+  opt_mode = SRT_INPUT_DEVICE_MONITOR_FLAGS_DIRECT;
+  return TRUE;
+}
+
+static gboolean
+opt_udev_cb (const gchar *option_name,
+             const gchar *value,
+             gpointer data,
+             GError **error)
+{
+  opt_mode = SRT_INPUT_DEVICE_MONITOR_FLAGS_UDEV;
+  return TRUE;
+}
+
 static const GOptionEntry option_entries[] =
 {
+  { "direct", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_direct_cb,
+    "Find devices using /dev and /sys", NULL },
   { "evdev", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_evdev,
     "List evdev event devices", NULL },
   { "hidraw", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_hidraw,
@@ -188,6 +211,8 @@ static const GOptionEntry option_entries[] =
   { "seq", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_seq,
     "Output application/json-seq [default: pretty-print as concatenated JSON]",
     NULL },
+  { "udev", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_udev_cb,
+    "Find devices using udev", NULL },
   { "version", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_print_version,
     "Print version number and exit", NULL },
   { NULL }
@@ -230,7 +255,7 @@ run (int argc,
     return FALSE;
 
   int_handler = g_unix_signal_add (SIGINT, interrupt_cb, &monitor);
-  monitor = srt_input_device_monitor_new (opt_flags);
+  monitor = srt_input_device_monitor_new (opt_mode | opt_flags);
 
   if (opt_evdev)
     srt_input_device_monitor_request_evdev (monitor);
diff --git a/bin/input-monitor.md b/bin/input-monitor.md
index a7ddca2bb95f25363adbab201fa9f558e1e92282..5066fd1c077a5561b19677486308f348a6aa5f8f 100644
--- a/bin/input-monitor.md
+++ b/bin/input-monitor.md
@@ -17,6 +17,11 @@ steam-runtime-input-monitor - list input devices
 
 # OPTIONS
 
+**--direct**
+:   List input devices by reading `/dev` and `/sys`.
+    This currently only lists input devices for which at least read access
+    to the underlying device node is available.
+
 **--evdev**
 :   List all input event devices.
 
@@ -37,6 +42,10 @@ steam-runtime-input-monitor - list input devices
     each event is printed as U+001E RECORD SEPARATOR, followed by
     a JSON object, followed by U+000A LINE FEED.
 
+**--udev**
+:   List input devices by contacting udevd. This is usually not
+    expected to work inside a container.
+
 **--version**
 :   Print the version number as YAML and exit.
 
@@ -56,6 +65,10 @@ Options to limit devices more selectively (for example, only evdev
 devices that have the **ID_INPUT_JOYSTICK** udev property) are likely
 to be added in future versions.
 
+By default, an implementation is chosen automatically. The **--direct**
+and **--udev** options override this, with the last one used
+taking precedence.
+
 Each event is a JSON object with a single key. Consumers should ignore
 unknown keys to allow for future expansion.
 
diff --git a/debian/control b/debian/control
index 4afc66f5889f02f79db1001cb0461b1e72cbd66c..9cda84983dceea1081e115b12bca603cb5178e84 100644
--- a/debian/control
+++ b/debian/control
@@ -14,6 +14,7 @@ Build-Depends:
  libglib2.0-dev,
  libjson-glib-dev (>= 1.0),
  libtheora0 <!nocheck>,
+ libudev1 <!nocheck> | libudev0 <!nocheck>,
  libva-dev,
  libvdpau-dev,
  libvulkan-dev,
diff --git a/steam-runtime-tools/direct-input-device-internal.h b/steam-runtime-tools/direct-input-device-internal.h
index 7f596f3ca27469662256bf1ae81de336d5adb9c5..310b6b9805ca62e90f3beb6ca94a1cfcfd06e5aa 100644
--- a/steam-runtime-tools/direct-input-device-internal.h
+++ b/steam-runtime-tools/direct-input-device-internal.h
@@ -57,3 +57,11 @@ GType _srt_direct_input_device_monitor_get_type (void);
 
 G_DEFINE_AUTOPTR_CLEANUP_FUNC (SrtDirectInputDevice, g_object_unref)
 G_DEFINE_AUTOPTR_CLEANUP_FUNC (SrtDirectInputDeviceMonitor, g_object_unref)
+
+static inline SrtInputDeviceMonitor *
+srt_direct_input_device_monitor_new (SrtInputDeviceMonitorFlags flags)
+{
+  return g_object_new (SRT_TYPE_DIRECT_INPUT_DEVICE_MONITOR,
+                       "flags", flags,
+                       NULL);
+}
diff --git a/steam-runtime-tools/input-device.c b/steam-runtime-tools/input-device.c
index 9ffffc49830af9fb1916b51041078ae0b0703551..525265f4f9737fe669b1f720dded6d37060147d5 100644
--- a/steam-runtime-tools/input-device.c
+++ b/steam-runtime-tools/input-device.c
@@ -31,6 +31,7 @@
 #include "steam-runtime-tools/enums.h"
 
 #include "steam-runtime-tools/direct-input-device-internal.h"
+#include "steam-runtime-tools/udev-input-device-internal.h"
 
 /**
  * SrtInputDevice:
@@ -174,6 +175,33 @@ static guint monitor_signal_added = 0;
 static guint monitor_signal_removed = 0;
 static guint monitor_signal_all_for_now = 0;
 
+static const SrtInputDeviceMonitorFlags mode_flags = (SRT_INPUT_DEVICE_MONITOR_FLAGS_UDEV
+                                                      | SRT_INPUT_DEVICE_MONITOR_FLAGS_DIRECT);
+
+/*
+ * @monitor_out: (out) (not optional):
+ * @error: (inout) (not optional):
+ */
+static gboolean
+try_udev (SrtInputDeviceMonitorFlags flags,
+          SrtInputDeviceMonitor **monitor_out,
+          GError **error)
+{
+  /* Don't try it again if we already failed, to avoid another warning */
+  if (*error != NULL)
+    return FALSE;
+
+  *monitor_out = srt_udev_input_device_monitor_new (flags, error);
+
+  if (*monitor_out != NULL)
+    return TRUE;
+
+  /* We usually expect this to succeed, so log a warning if it fails */
+  g_warning ("Unable to initialize udev input device monitor: %s",
+             (*error)->message);
+  return FALSE;
+}
+
 /**
  * srt_input_device_monitor_new:
  * @flags: Flags affecting the behaviour of the input device monitor.
@@ -189,10 +217,35 @@ static guint monitor_signal_all_for_now = 0;
 SrtInputDeviceMonitor *
 srt_input_device_monitor_new (SrtInputDeviceMonitorFlags flags)
 {
-  /* For now we only have one implementation. */
-  return g_object_new (SRT_TYPE_DIRECT_INPUT_DEVICE_MONITOR,
-                       "flags", flags,
-                       NULL);
+  SrtInputDeviceMonitor *udev = NULL;
+  g_autoptr(GError) udev_error = NULL;
+
+  if (__builtin_popcount (flags & mode_flags) > 1)
+    g_warning ("Requesting more than one of UDEV and DIRECT "
+               "monitoring has undefined results: 0x%x", flags);
+
+  /* First see whether the caller expressed a preference */
+
+  if (flags & SRT_INPUT_DEVICE_MONITOR_FLAGS_UDEV)
+    {
+      if (try_udev (flags, &udev, &udev_error))
+        return udev;
+    }
+
+  if (flags & SRT_INPUT_DEVICE_MONITOR_FLAGS_DIRECT)
+    return srt_direct_input_device_monitor_new (flags);
+
+  /* Prefer a direct monitor if we're in a container */
+  if (g_file_test ("/.flatpak-info", G_FILE_TEST_EXISTS)
+      || g_file_test ("/run/pressure-vessel", G_FILE_TEST_EXISTS)
+      || g_file_test ("/run/host", G_FILE_TEST_EXISTS))
+    return srt_direct_input_device_monitor_new (flags);
+
+  if (try_udev (flags, &udev, &udev_error))
+    return udev;
+
+  /* Fall back to direct monitoring if we don't have libudev */
+  return srt_direct_input_device_monitor_new (flags);
 }
 
 static void
diff --git a/steam-runtime-tools/input-device.h b/steam-runtime-tools/input-device.h
index 3b8df8a91619ba47ecd29d1a381b6bc546db7d39..504522e6d2419164a135f6cefe21b5916c03a90b 100644
--- a/steam-runtime-tools/input-device.h
+++ b/steam-runtime-tools/input-device.h
@@ -55,6 +55,9 @@ const char *srt_input_device_get_subsystem (SrtInputDevice *device);
  * SrtInputDeviceMonitorFlags:
  * @SRT_INPUT_DEVICE_MONITOR_FLAGS_ONCE: Enumerate the devices that were
  *  available when monitoring starts, and then stop monitoring.
+ * @SRT_INPUT_DEVICE_MONITOR_FLAGS_UDEV: Prefer to get devices from udev.
+ * @SRT_INPUT_DEVICE_MONITOR_FLAGS_DIRECT: Prefer to get devices by
+ *  monitoring /dev, /sys directly.
  * @SRT_INPUT_DEVICE_MONITOR_FLAGS_NONE: No special behaviour.
  *
  * Flags affecting the behaviour of the input device monitor.
@@ -62,6 +65,8 @@ const char *srt_input_device_get_subsystem (SrtInputDevice *device);
 typedef enum
 {
   SRT_INPUT_DEVICE_MONITOR_FLAGS_ONCE = (1 << 0),
+  SRT_INPUT_DEVICE_MONITOR_FLAGS_UDEV = (1 << 1),
+  SRT_INPUT_DEVICE_MONITOR_FLAGS_DIRECT = (1 << 2),
   SRT_INPUT_DEVICE_MONITOR_FLAGS_NONE = 0
 } SrtInputDeviceMonitorFlags;
 
diff --git a/steam-runtime-tools/meson.build b/steam-runtime-tools/meson.build
index bf878fb09c5861637aae9004a41339a662d48772..c9c3edd5d4b424b82c226263c095ba5986b19026 100644
--- a/steam-runtime-tools/meson.build
+++ b/steam-runtime-tools/meson.build
@@ -53,6 +53,8 @@ libsteamrt_sources = [
     'steam-internal.h',
     'steam.c',
     'system-info.c',
+    'udev-input-device-internal.h',
+    'udev-input-device.c',
     'utils-internal.h',
     'utils.c',
     'xdg-portal.c',
diff --git a/steam-runtime-tools/udev-input-device-internal.h b/steam-runtime-tools/udev-input-device-internal.h
new file mode 100644
index 0000000000000000000000000000000000000000..9ee0d36c83568935f1c0a62620eaa30eafb3a186
--- /dev/null
+++ b/steam-runtime-tools/udev-input-device-internal.h
@@ -0,0 +1,68 @@
+/*
+ * libudev-based input device monitor, loosely based on SDL code.
+ *
+ * Copyright © 1997-2020 Sam Lantinga <slouken@libsdl.org>
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: Zlib
+ *
+ * This software is provided 'as-is', without any express or implied
+ * warranty.  In no event will the authors be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, subject to the following restrictions:
+ *
+ * 1. The origin of this software must not be misrepresented; you must not
+ *    claim that you wrote the original software. If you use this software
+ *    in a product, an acknowledgment in the product documentation would be
+ *    appreciated but is not required.
+ * 2. Altered source versions must be plainly marked as such, and must not be
+ *    misrepresented as being the original software.
+ * 3. This notice may not be removed or altered from any source distribution.
+ */
+
+#pragma once
+
+#include "steam-runtime-tools/glib-backports-internal.h"
+#include "steam-runtime-tools/input-device.h"
+
+typedef struct _SrtUdevInputDevice SrtUdevInputDevice;
+typedef struct _SrtUdevInputDeviceClass SrtUdevInputDeviceClass;
+
+#define SRT_TYPE_UDEV_INPUT_DEVICE (_srt_udev_input_device_get_type ())
+#define SRT_UDEV_INPUT_DEVICE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SRT_TYPE_UDEV_INPUT_DEVICE, SrtUdevInputDevice))
+#define SRT_IS_UDEV_INPUT_DEVICE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SRT_TYPE_UDEV_INPUT_DEVICE))
+#define SRT_UDEV_INPUT_DEVICE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), SRT_TYPE_UDEV_INPUT_DEVICE, SrtUdevInputDeviceClass))
+#define SRT_UDEV_INPUT_DEVICE_CLASS(c) (G_TYPE_CHECK_CLASS_CAST ((c), SRT_TYPE_UDEV_INPUT_DEVICE, SrtUdevInputDeviceClass))
+#define SRT_IS_UDEV_INPUT_DEVICE_CLASS(c) (G_TYPE_CHECK_CLASS_TYPE ((c), SRT_TYPE_UDEV_INPUT_DEVICE))
+
+G_GNUC_INTERNAL
+GType _srt_udev_input_device_get_type (void);
+
+#define SRT_TYPE_UDEV_INPUT_DEVICE_MONITOR (_srt_udev_input_device_monitor_get_type ())
+#define SRT_UDEV_INPUT_DEVICE_MONITOR(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SRT_TYPE_UDEV_INPUT_DEVICE_MONITOR, SrtUdevInputDeviceMonitor))
+#define SRT_IS_UDEV_INPUT_DEVICE_MONITOR(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SRT_TYPE_UDEV_INPUT_DEVICE_MONITOR))
+#define SRT_UDEV_INPUT_DEVICE_MONITOR_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), SRT_TYPE_UDEV_INPUT_DEVICE_MONITOR, SrtUdevInputDeviceMonitorClass))
+#define SRT_UDEV_INPUT_DEVICE_MONITOR_CLASS(c) (G_TYPE_CHECK_CLASS_CAST ((c), SRT_TYPE_UDEV_INPUT_DEVICE_MONITOR, SrtUdevInputDeviceMonitorClass))
+#define SRT_IS_UDEV_INPUT_DEVICE_MONITOR_CLASS(c) (G_TYPE_CHECK_CLASS_TYPE ((c), SRT_TYPE_UDEV_INPUT_DEVICE_MONITOR))
+
+typedef struct _SrtUdevInputDeviceMonitor SrtUdevInputDeviceMonitor;
+typedef struct _SrtUdevInputDeviceMonitorClass SrtUdevInputDeviceMonitorClass;
+
+G_GNUC_INTERNAL
+GType _srt_udev_input_device_monitor_get_type (void);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (SrtUdevInputDevice, g_object_unref)
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (SrtUdevInputDeviceMonitor, g_object_unref)
+
+static inline SrtInputDeviceMonitor *
+srt_udev_input_device_monitor_new (SrtInputDeviceMonitorFlags flags,
+                                   GError **error)
+{
+  return g_initable_new (SRT_TYPE_UDEV_INPUT_DEVICE_MONITOR,
+                         NULL /* cancellable */, error,
+                         "flags", flags,
+                         NULL);
+}
diff --git a/steam-runtime-tools/udev-input-device.c b/steam-runtime-tools/udev-input-device.c
new file mode 100644
index 0000000000000000000000000000000000000000..60701c92bd6607ee822dfbb0e161adbcec8c45a9
--- /dev/null
+++ b/steam-runtime-tools/udev-input-device.c
@@ -0,0 +1,671 @@
+/*
+ * libudev-based input device monitor, loosely based on SDL code.
+ *
+ * Copyright © 1997-2020 Sam Lantinga <slouken@libsdl.org>
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: Zlib
+ *
+ * This software is provided 'as-is', without any express or implied
+ * warranty.  In no event will the authors be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, subject to the following restrictions:
+ *
+ * 1. The origin of this software must not be misrepresented; you must not
+ *    claim that you wrote the original software. If you use this software
+ *    in a product, an acknowledgment in the product documentation would be
+ *    appreciated but is not required.
+ * 2. Altered source versions must be plainly marked as such, and must not be
+ *    misrepresented as being the original software.
+ * 3. This notice may not be removed or altered from any source distribution.
+ */
+
+#include "steam-runtime-tools/udev-input-device-internal.h"
+
+#include <dlfcn.h>
+#include <linux/input.h>
+
+#include <glib-unix.h>
+#include <libglnx.h>
+
+#include "steam-runtime-tools/glib-backports-internal.h"
+#include "steam-runtime-tools/input-device.h"
+#include "steam-runtime-tools/input-device-internal.h"
+#include "steam-runtime-tools/utils-internal.h"
+
+static void srt_udev_input_device_iface_init (SrtInputDeviceInterface *iface);
+static void srt_udev_input_device_monitor_iface_init (SrtInputDeviceMonitorInterface *iface);
+static void srt_udev_input_device_monitor_initable_iface_init (GInitableIface *iface);
+
+struct udev;
+struct udev_list_entry;
+struct udev_device;
+struct udev_monitor;
+struct udev_enumerate;
+
+static struct
+{
+  void *handle;
+
+  struct udev *(*udev_new) (void);
+  struct udev *(*udev_unref) (struct udev *);
+
+  const char *(*udev_list_entry_get_name) (struct udev_list_entry *);
+  struct udev_list_entry *(*udev_list_entry_get_next) (struct udev_list_entry *);
+
+  struct udev_device *(*udev_device_new_from_syspath) (struct udev *,
+                                                       const char *);
+  const char *(*udev_device_get_action) (struct udev_device *);
+  const char *(*udev_device_get_devnode) (struct udev_device *);
+  const char *(*udev_device_get_subsystem) (struct udev_device *);
+  const char *(*udev_device_get_syspath) (struct udev_device *);
+  struct udev_device *(*udev_device_ref) (struct udev_device *);
+  struct udev_device *(*udev_device_unref) (struct udev_device *);
+
+  struct udev_monitor *(*udev_monitor_new_from_netlink) (struct udev *,
+                                                         const char *);
+  int (*udev_monitor_filter_add_match_subsystem_devtype) (struct udev_monitor *,
+                                                          const char *,
+                                                          const char *);
+  int (*udev_monitor_enable_receiving) (struct udev_monitor *);
+  struct udev_device *(*udev_monitor_receive_device) (struct udev_monitor *);
+  int (*udev_monitor_get_fd) (struct udev_monitor *);
+  struct udev_monitor *(*udev_monitor_unref) (struct udev_monitor *);
+
+  struct udev_enumerate *(*udev_enumerate_new) (struct udev *);
+  int (*udev_enumerate_add_match_subsystem) (struct udev_enumerate *,
+                                             const char *);
+  int (*udev_enumerate_scan_devices) (struct udev_enumerate *);
+  struct udev_list_entry *(*udev_enumerate_get_list_entry) (struct udev_enumerate *);
+  struct udev_enumerate *(*udev_enumerate_unref) (struct udev_enumerate *);
+} symbols;
+
+struct _SrtUdevInputDevice
+{
+  GObject parent;
+
+  struct udev_device *dev;
+};
+
+struct _SrtUdevInputDeviceClass
+{
+  GObjectClass parent;
+};
+
+struct _SrtUdevInputDeviceMonitor
+{
+  GObject parent;
+
+  struct udev *context;
+  struct udev_monitor *monitor;
+  GHashTable *devices;
+  GMainContext *monitor_context;
+  GSource *monitor_source;
+
+  gboolean want_evdev;
+  gboolean want_hidraw;
+  SrtInputDeviceMonitorFlags flags;
+  enum
+  {
+    NOT_STARTED = 0,
+    STARTED,
+    STOPPED
+  } state;
+};
+
+struct _SrtUdevInputDeviceMonitorClass
+{
+  GObjectClass parent;
+};
+
+G_DEFINE_TYPE_WITH_CODE (SrtUdevInputDevice,
+                         _srt_udev_input_device,
+                         G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (SRT_TYPE_INPUT_DEVICE,
+                                                srt_udev_input_device_iface_init))
+
+G_DEFINE_TYPE_WITH_CODE (SrtUdevInputDeviceMonitor,
+                         _srt_udev_input_device_monitor,
+                         G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE,
+                                                srt_udev_input_device_monitor_initable_iface_init)
+                         G_IMPLEMENT_INTERFACE (SRT_TYPE_INPUT_DEVICE_MONITOR,
+                                                srt_udev_input_device_monitor_iface_init))
+
+static void
+_srt_udev_input_device_init (SrtUdevInputDevice *self)
+{
+}
+
+static void
+srt_udev_input_device_finalize (GObject *object)
+{
+  SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (object);
+
+  g_clear_pointer (&self->dev, symbols.udev_device_unref);
+
+  G_OBJECT_CLASS (_srt_udev_input_device_parent_class)->finalize (object);
+}
+
+static void
+_srt_udev_input_device_class_init (SrtUdevInputDeviceClass *cls)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (cls);
+
+  object_class->finalize = srt_udev_input_device_finalize;
+}
+
+static const char *
+srt_udev_input_device_get_dev_node (SrtInputDevice *device)
+{
+  SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device);
+
+  return symbols.udev_device_get_devnode (self->dev);
+}
+
+static const char *
+srt_udev_input_device_get_subsystem (SrtInputDevice *device)
+{
+  SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device);
+
+  return symbols.udev_device_get_subsystem (self->dev);
+}
+
+static const char *
+srt_udev_input_device_get_sys_path (SrtInputDevice *device)
+{
+  SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device);
+
+  return symbols.udev_device_get_syspath (self->dev);
+}
+
+static void
+srt_udev_input_device_iface_init (SrtInputDeviceInterface *iface)
+{
+#define IMPLEMENT(x) iface->x = srt_udev_input_device_ ## x
+
+  IMPLEMENT (get_dev_node);
+  IMPLEMENT (get_sys_path);
+  IMPLEMENT (get_subsystem);
+
+#undef IMPLEMENT
+}
+
+typedef enum
+{
+  PROP_0,
+  PROP_FLAGS,
+  PROP_IS_ACTIVE,
+  N_PROPERTIES
+} Property;
+
+static void
+_srt_udev_input_device_monitor_init (SrtUdevInputDeviceMonitor *self)
+{
+  self->monitor_context = g_main_context_ref_thread_default ();
+  self->state = NOT_STARTED;
+  self->devices = g_hash_table_new_full (g_str_hash,
+                                         g_str_equal,
+                                         NULL,
+                                         g_object_unref);
+}
+
+static void
+srt_udev_input_device_monitor_get_property (GObject *object,
+                                            guint prop_id,
+                                            GValue *value,
+                                            GParamSpec *pspec)
+{
+  SrtUdevInputDeviceMonitor *self = SRT_UDEV_INPUT_DEVICE_MONITOR (object);
+
+  switch ((Property) prop_id)
+    {
+      case PROP_FLAGS:
+        g_value_set_flags (value, self->flags);
+        break;
+
+      case PROP_IS_ACTIVE:
+        g_value_set_boolean (value, (self->state == STARTED));
+        break;
+
+      case PROP_0:
+      case N_PROPERTIES:
+      default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+srt_udev_input_device_monitor_set_property (GObject *object,
+                                            guint prop_id,
+                                            const GValue *value,
+                                            GParamSpec *pspec)
+{
+  SrtUdevInputDeviceMonitor *self = SRT_UDEV_INPUT_DEVICE_MONITOR (object);
+
+  switch ((Property) prop_id)
+    {
+      case PROP_FLAGS:
+        /* Construct-only */
+        g_return_if_fail (self->flags == 0);
+        self->flags = g_value_get_flags (value);
+        break;
+
+      case PROP_IS_ACTIVE:
+      case PROP_0:
+      case N_PROPERTIES:
+      default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+srt_udev_input_device_monitor_dispose (GObject *object)
+{
+  srt_input_device_monitor_stop (SRT_INPUT_DEVICE_MONITOR (object));
+  G_OBJECT_CLASS (_srt_udev_input_device_monitor_parent_class)->dispose (object);
+}
+
+static void
+srt_udev_input_device_monitor_finalize (GObject *object)
+{
+  srt_input_device_monitor_stop (SRT_INPUT_DEVICE_MONITOR (object));
+  G_OBJECT_CLASS (_srt_udev_input_device_monitor_parent_class)->finalize (object);
+}
+
+static void
+_srt_udev_input_device_monitor_class_init (SrtUdevInputDeviceMonitorClass *cls)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (cls);
+
+  object_class->get_property = srt_udev_input_device_monitor_get_property;
+  object_class->set_property = srt_udev_input_device_monitor_set_property;
+  object_class->dispose = srt_udev_input_device_monitor_dispose;
+  object_class->finalize = srt_udev_input_device_monitor_finalize;
+
+  g_object_class_override_property (object_class, PROP_FLAGS, "flags");
+  g_object_class_override_property (object_class, PROP_IS_ACTIVE, "is-active");
+}
+
+static gboolean
+srt_udev_input_device_monitor_initable_init (GInitable *initable,
+                                             GCancellable *cancellable G_GNUC_UNUSED,
+                                             GError **error)
+{
+  static const char * const libs[] = { "libudev.so.1", "libudev.so.0" };
+  void *handle = NULL;
+  const char *lib = NULL;
+  const char *which = NULL;
+  gsize i;
+
+  /* Only initialize once per process */
+  if (symbols.handle != NULL)
+    return TRUE;
+
+  for (i = 0; i < G_N_ELEMENTS (libs); i++)
+    {
+      lib = libs[i];
+      handle = dlopen (lib, RTLD_NOW|RTLD_LOCAL|RTLD_NODELETE);
+
+      if (handle != NULL)
+        break;
+    }
+
+  if (handle == NULL)
+    {
+      /* Try the first library again, for the side-effect of getting the
+       * error message */
+      lib = libs[0];
+      handle = dlopen (lib, RTLD_NOW|RTLD_LOCAL|RTLD_NODELETE);
+    }
+
+  if (handle == NULL)
+    return glnx_throw (error, "Unable to load %s: %s", lib, dlerror ());
+
+#define SYMBOL(name) \
+  do \
+    { \
+      void *symbol; \
+      \
+      which = #name; \
+      symbol = dlsym (handle, which); \
+      \
+      if (symbol != NULL) \
+        symbols.name = (__typeof__ (symbols.name)) G_CALLBACK (symbol); \
+      else \
+        goto fail; \
+    } \
+  while (0)
+
+  SYMBOL (udev_new);
+  SYMBOL (udev_unref);
+
+  SYMBOL (udev_list_entry_get_name);
+  SYMBOL (udev_list_entry_get_next);
+
+  SYMBOL (udev_device_new_from_syspath);
+  SYMBOL (udev_device_get_action);
+  SYMBOL (udev_device_get_devnode);
+  SYMBOL (udev_device_get_subsystem);
+  SYMBOL (udev_device_get_syspath);
+  SYMBOL (udev_device_ref);
+  SYMBOL (udev_device_unref);
+
+  SYMBOL (udev_monitor_new_from_netlink);
+  SYMBOL (udev_monitor_filter_add_match_subsystem_devtype);
+  SYMBOL (udev_monitor_enable_receiving);
+  SYMBOL (udev_monitor_receive_device);
+  SYMBOL (udev_monitor_get_fd);
+  SYMBOL (udev_monitor_unref);
+
+  SYMBOL (udev_enumerate_new);
+  SYMBOL (udev_enumerate_add_match_subsystem);
+  SYMBOL (udev_enumerate_scan_devices);
+  SYMBOL (udev_enumerate_get_list_entry);
+  SYMBOL (udev_enumerate_unref);
+
+#undef SYMBOL
+
+  symbols.handle = handle;
+  return TRUE;
+
+fail:
+  return glnx_throw (error, "Unable to find symbol %s in %s: %s",
+                     which, lib, dlerror ());
+}
+
+static void
+add_device (SrtUdevInputDeviceMonitor *self,
+            struct udev_device *dev)
+{
+  SrtUdevInputDevice *device = NULL;
+  const char *syspath;
+  const char *devnode;
+  gboolean want = FALSE;
+  gboolean is_evdev = FALSE;
+  gboolean is_hidraw = FALSE;
+  const char *slash;
+
+  syspath = symbols.udev_device_get_syspath (dev);
+  devnode = symbols.udev_device_get_devnode (dev);
+
+  if (G_UNLIKELY (syspath == NULL))
+    {
+      g_warning ("not adding udev_device with NULL syspath");
+      return;
+    }
+
+  /* If we added a device at this /sys path already, don't add it again.
+   * (This is for the "change" action, and the rarer "bind" action.) */
+  if (g_hash_table_lookup_extended (self->devices, syspath, NULL, NULL))
+    {
+      g_debug ("ignoring device \"%s\" which we already have", syspath);
+      return;
+    }
+
+  if (devnode == NULL)
+    {
+      /* We only care about the devices that could, in principle, be
+       * opened */
+      g_debug ("ignoring device \"%s\" with NULL device node", syspath);
+      return;
+    }
+
+  slash = strrchr (syspath, '/');
+
+  if (slash != NULL)
+    {
+      if (self->want_hidraw &&
+          g_str_has_prefix (slash + 1, "hidraw") &&
+          _srt_str_is_integer (slash + 1 + strlen ("hidraw")))
+        want = is_hidraw = TRUE;
+
+      if (self->want_evdev &&
+          g_str_has_prefix (slash + 1, "event") &&
+          _srt_str_is_integer (slash + 1 + strlen ("event")))
+        want = is_evdev = TRUE;
+    }
+
+  if (!want)
+    {
+      g_debug ("ignoring uninteresting udev_device \"%s\"", syspath);
+      return;
+    }
+
+  g_debug ("Adding device %s", syspath);
+
+  device = g_object_new (SRT_TYPE_UDEV_INPUT_DEVICE, NULL);
+  device->dev = symbols.udev_device_ref (dev);
+
+  g_hash_table_replace (self->devices, (char *) syspath, device);
+  _srt_input_device_monitor_emit_added (SRT_INPUT_DEVICE_MONITOR (self),
+                                        SRT_INPUT_DEVICE (device));
+}
+
+static void
+remove_device (SrtUdevInputDeviceMonitor *self,
+               struct udev_device *dev)
+{
+  void *device = NULL;
+  const char *syspath;
+
+  syspath = symbols.udev_device_get_syspath (dev);
+
+  if (G_UNLIKELY (syspath == NULL))
+    {
+      g_warning ("trying to remove udev_device with NULL syspath");
+      symbols.udev_device_unref (dev);
+      return;
+    }
+
+  g_debug ("Removing device %s", syspath);
+
+  if (g_hash_table_lookup_extended (self->devices, syspath, NULL, &device))
+    {
+      g_hash_table_steal (self->devices, syspath);
+      _srt_input_device_monitor_emit_removed (SRT_INPUT_DEVICE_MONITOR (self),
+                                              device);
+      g_object_unref (device);
+    }
+}
+
+static gboolean
+srt_udev_input_device_monitor_cb (int fd,
+                                  GIOCondition condition,
+                                  void *user_data)
+{
+  SrtUdevInputDeviceMonitor *self = SRT_UDEV_INPUT_DEVICE_MONITOR (user_data);
+  struct udev_device *dev;
+  const char *action;
+
+  g_return_val_if_fail (SRT_IS_UDEV_INPUT_DEVICE_MONITOR (self), G_SOURCE_REMOVE);
+
+  dev = symbols.udev_monitor_receive_device (self->monitor);
+
+  if (dev == NULL)
+    return G_SOURCE_CONTINUE;
+
+  action = symbols.udev_device_get_action (dev);
+
+  if (g_strcmp0 (action, "remove") == 0)
+    remove_device (self, dev);
+  else
+    add_device (self, dev);
+
+  g_clear_pointer (&dev, symbols.udev_device_unref);
+  return G_SOURCE_CONTINUE;
+}
+
+static void
+srt_udev_input_device_monitor_request_raw_hid (SrtInputDeviceMonitor *monitor)
+{
+  SrtUdevInputDeviceMonitor *self = SRT_UDEV_INPUT_DEVICE_MONITOR (monitor);
+
+  g_return_if_fail (SRT_IS_UDEV_INPUT_DEVICE_MONITOR (monitor));
+  g_return_if_fail (self->state == NOT_STARTED);
+
+  self->want_hidraw = TRUE;
+}
+
+static void
+srt_udev_input_device_monitor_request_evdev (SrtInputDeviceMonitor *monitor)
+{
+  SrtUdevInputDeviceMonitor *self = SRT_UDEV_INPUT_DEVICE_MONITOR (monitor);
+
+  g_return_if_fail (SRT_IS_UDEV_INPUT_DEVICE_MONITOR (monitor));
+  g_return_if_fail (self->state == NOT_STARTED);
+
+  self->want_evdev = TRUE;
+}
+
+static gboolean
+enumerate_cb (gpointer user_data)
+{
+  SrtUdevInputDeviceMonitor *self = SRT_UDEV_INPUT_DEVICE_MONITOR (user_data);
+
+  if (self->want_hidraw || self->want_evdev)
+    {
+      struct udev_enumerate *enumerator = NULL;
+      struct udev_list_entry *devs;
+      struct udev_list_entry *item;
+
+      enumerator = symbols.udev_enumerate_new (self->context);
+
+      if (enumerator == NULL)
+        {
+          g_warning ("udev_enumerate_new: %s", g_strerror (errno));
+          goto out;
+        }
+
+      if (self->want_evdev)
+        symbols.udev_enumerate_add_match_subsystem (enumerator, "input");
+
+      if (self->want_hidraw)
+        symbols.udev_enumerate_add_match_subsystem (enumerator, "hidraw");
+
+      symbols.udev_enumerate_scan_devices (enumerator);
+      devs = symbols.udev_enumerate_get_list_entry (enumerator);
+
+      for (item = devs; item != NULL; item = symbols.udev_list_entry_get_next (item))
+        {
+          const char *syspath = symbols.udev_list_entry_get_name (item);
+          struct udev_device *dev;
+
+          dev = symbols.udev_device_new_from_syspath (self->context, syspath);
+
+          if (dev != NULL)
+            add_device (self, dev);
+          else
+            g_warning ("udev_device_new_from_syspath \"%s\": %s",
+                       syspath, g_strerror (errno));
+
+          symbols.udev_device_unref (dev);
+        }
+
+      symbols.udev_enumerate_unref (enumerator);
+    }
+
+out:
+  _srt_input_device_monitor_emit_all_for_now (SRT_INPUT_DEVICE_MONITOR (self));
+
+  return G_SOURCE_REMOVE;
+}
+
+static gboolean
+srt_udev_input_device_monitor_start (SrtInputDeviceMonitor *monitor,
+                                     GError **error)
+{
+  SrtUdevInputDeviceMonitor *self = SRT_UDEV_INPUT_DEVICE_MONITOR (monitor);
+
+  g_return_val_if_fail (SRT_IS_UDEV_INPUT_DEVICE_MONITOR (monitor), FALSE);
+  g_return_val_if_fail (self->state == NOT_STARTED, FALSE);
+  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+  /* If this assertion fails, g_initable_init() failed or wasn't called */
+  g_return_val_if_fail (symbols.handle != NULL, FALSE);
+
+  self->state = STARTED;
+
+  self->context = symbols.udev_new ();
+
+  if (self->context == NULL)
+    return glnx_throw_errno_prefix (error, "udev_new");
+
+  if ((self->flags & SRT_INPUT_DEVICE_MONITOR_FLAGS_ONCE) == 0
+      && (self->want_hidraw || self->want_evdev))
+    {
+      int fd;
+
+      self->monitor = symbols.udev_monitor_new_from_netlink (self->context, "udev");
+
+      if (self->monitor == NULL)
+        return glnx_throw_errno_prefix (error, "udev_monitor_new_from_netlink");
+
+      if (self->want_evdev)
+        symbols.udev_monitor_filter_add_match_subsystem_devtype (self->monitor, "input", NULL);
+
+      if (self->want_hidraw)
+        symbols.udev_monitor_filter_add_match_subsystem_devtype (self->monitor, "hidraw", NULL);
+
+      symbols.udev_monitor_enable_receiving (self->monitor);
+
+      fd = symbols.udev_monitor_get_fd (self->monitor);
+
+      if (fd < 0)
+        {
+          errno = -fd;
+          return glnx_throw_errno_prefix (error, "udev_monitor_get_fd");
+        }
+
+      self->monitor_source = g_unix_fd_source_new (fd, G_IO_IN);
+      g_source_set_callback (self->monitor_source,
+                             (GSourceFunc) G_CALLBACK (srt_udev_input_device_monitor_cb),
+                             self, NULL);
+      g_source_set_priority (self->monitor_source, G_PRIORITY_DEFAULT);
+      g_source_attach (self->monitor_source, self->monitor_context);
+    }
+
+  /* Make sure the signals for the initial batch of devices are emitted in
+   * the correct main-context */
+  g_main_context_invoke_full (self->monitor_context, G_PRIORITY_DEFAULT,
+                              enumerate_cb, g_object_ref (self),
+                              g_object_unref);
+
+  return TRUE;
+}
+
+static void
+srt_udev_input_device_monitor_stop (SrtInputDeviceMonitor *monitor)
+{
+  SrtUdevInputDeviceMonitor *self = SRT_UDEV_INPUT_DEVICE_MONITOR (monitor);
+
+  self->state = STOPPED;
+
+  if (self->monitor_source != NULL)
+    g_source_destroy (self->monitor_source);
+
+  g_clear_pointer (&self->monitor_source, g_source_unref);
+  g_clear_pointer (&self->monitor_context, g_main_context_unref);
+  g_clear_pointer (&self->monitor, symbols.udev_monitor_unref);
+  g_clear_pointer (&self->context, symbols.udev_unref);
+  g_clear_pointer (&self->devices, g_hash_table_unref);
+}
+
+static void
+srt_udev_input_device_monitor_iface_init (SrtInputDeviceMonitorInterface *iface)
+{
+#define IMPLEMENT(x) iface->x = srt_udev_input_device_monitor_ ## x
+  IMPLEMENT (request_raw_hid);
+  IMPLEMENT (request_evdev);
+  IMPLEMENT (start);
+  IMPLEMENT (stop);
+#undef IMPLEMENT
+}
+
+static void
+srt_udev_input_device_monitor_initable_iface_init (GInitableIface *iface)
+{
+  iface->init = srt_udev_input_device_monitor_initable_init;
+}
diff --git a/sysroot/run-in-sysroot.py b/sysroot/run-in-sysroot.py
index b22b927c63e6f3b4f94a1eebaf56e5edaea4aaa2..73d07ceebda0e76b45a750b95ebb565447089ffe 100755
--- a/sysroot/run-in-sysroot.py
+++ b/sysroot/run-in-sysroot.py
@@ -110,6 +110,8 @@ def main():
         '--tmpfs', '/tmp',
         '--tmpfs', '/var/tmp',
         '--tmpfs', '/home',
+        '--tmpfs', '/run',
+        '--tmpfs', '/run/host',
         '--bind', abs_srcdir, abs_srcdir,
         '--bind', abs_builddir, abs_builddir,
     ]
diff --git a/tests/input-device.c b/tests/input-device.c
index 04c2897c3d8f4f5d11e4761cd75b9df82aacafc8..93ca598510126cccf74b5dd7ccc6f8238e6b0489 100644
--- a/tests/input-device.c
+++ b/tests/input-device.c
@@ -39,7 +39,7 @@
 
 typedef struct
 {
-  enum { MOCK, DIRECT } type;
+  enum { MOCK, DIRECT, UDEV } type;
 } Config;
 
 static const Config defconfig =
@@ -52,6 +52,11 @@ static const Config direct_config =
   .type = DIRECT,
 };
 
+static const Config udev_config =
+{
+  .type = UDEV,
+};
+
 typedef struct
 {
   const Config *config;
@@ -212,6 +217,11 @@ input_device_monitor_new (Fixture *f,
   switch (f->config->type)
     {
       case DIRECT:
+        flags |= SRT_INPUT_DEVICE_MONITOR_FLAGS_DIRECT;
+        return srt_input_device_monitor_new (flags);
+
+      case UDEV:
+        flags |= SRT_INPUT_DEVICE_MONITOR_FLAGS_UDEV;
         return srt_input_device_monitor_new (flags);
 
       case MOCK:
@@ -420,6 +430,10 @@ main (int argc,
               setup, test_input_device_monitor, teardown);
   g_test_add ("/input-device/monitor-once/direct", Fixture, &direct_config,
               setup, test_input_device_monitor_once, teardown);
+  g_test_add ("/input-device/monitor/udev", Fixture, &udev_config,
+              setup, test_input_device_monitor, teardown);
+  g_test_add ("/input-device/monitor-once/udev", Fixture, &udev_config,
+              setup, test_input_device_monitor_once, teardown);
 
   return g_test_run ();
 }