diff options
Diffstat (limited to 'src/mm-filter.c')
-rw-r--r-- | src/mm-filter.c | 550 |
1 files changed, 550 insertions, 0 deletions
diff --git a/src/mm-filter.c b/src/mm-filter.c new file mode 100644 index 00000000..b7a9d2f8 --- /dev/null +++ b/src/mm-filter.c @@ -0,0 +1,550 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details: + * + * Copyright (C) 2017 Aleksander Morgado <aleksander@aleksander.es> + */ + +#include <config.h> +#include <string.h> + +#include <ModemManager.h> +#include <ModemManager-tags.h> + +#include "mm-daemon-enums-types.h" +#include "mm-filter.h" +#include "mm-log-object.h" + +#define FILTER_PORT_MAYBE_FORBIDDEN "maybe-forbidden" + +static void log_object_iface_init (MMLogObjectInterface *iface); + +G_DEFINE_TYPE_EXTENDED (MMFilter, mm_filter, G_TYPE_OBJECT, 0, + G_IMPLEMENT_INTERFACE (MM_TYPE_LOG_OBJECT, log_object_iface_init)) + +enum { + PROP_0, + PROP_ENABLED_RULES, + LAST_PROP +}; + +struct _MMFilterPrivate { + MMFilterRule enabled_rules; + GList *plugin_whitelist_tags; + GArray *plugin_whitelist_vendor_ids; + GArray *plugin_whitelist_product_ids; +}; + +/*****************************************************************************/ + +void +mm_filter_register_plugin_whitelist_tag (MMFilter *self, + const gchar *tag) +{ + if (!g_list_find_custom (self->priv->plugin_whitelist_tags, tag, (GCompareFunc) g_strcmp0)) { + mm_obj_dbg (self, "registered plugin whitelist tag: %s", tag); + self->priv->plugin_whitelist_tags = g_list_prepend (self->priv->plugin_whitelist_tags, g_strdup (tag)); + } +} + +void +mm_filter_register_plugin_whitelist_vendor_id (MMFilter *self, + guint16 vid) +{ + guint i; + + if (!self->priv->plugin_whitelist_vendor_ids) + self->priv->plugin_whitelist_vendor_ids = g_array_sized_new (FALSE, FALSE, sizeof (guint16), 64); + + for (i = 0; i < self->priv->plugin_whitelist_vendor_ids->len; i++) { + guint16 item; + + item = g_array_index (self->priv->plugin_whitelist_vendor_ids, guint16, i); + if (item == vid) + return; + } + + g_array_append_val (self->priv->plugin_whitelist_vendor_ids, vid); + mm_obj_dbg (self, "registered plugin whitelist vendor id: %04x", vid); +} + +void +mm_filter_register_plugin_whitelist_product_id (MMFilter *self, + guint16 vid, + guint16 pid) +{ + mm_uint16_pair new_item; + guint i; + + if (!self->priv->plugin_whitelist_product_ids) + self->priv->plugin_whitelist_product_ids = g_array_sized_new (FALSE, FALSE, sizeof (mm_uint16_pair), 10); + + for (i = 0; i < self->priv->plugin_whitelist_product_ids->len; i++) { + mm_uint16_pair *item; + + item = &g_array_index (self->priv->plugin_whitelist_product_ids, mm_uint16_pair, i); + if (item->l == vid && item->r == pid) + return; + } + + new_item.l = vid; + new_item.r = pid; + g_array_append_val (self->priv->plugin_whitelist_product_ids, new_item); + mm_obj_dbg (self, "registered plugin whitelist product id: %04x:%04x", vid, pid); +} + +/*****************************************************************************/ + +gboolean +mm_filter_port (MMFilter *self, + MMKernelDevice *port, + gboolean manual_scan) +{ + const gchar *subsystem; + const gchar *name; + + subsystem = mm_kernel_device_get_subsystem (port); + name = mm_kernel_device_get_name (port); + + /* If the device is explicitly whitelisted, we process every port. Also + * allow specifying this flag per-port instead of for the full device, e.g. + * for platform tty ports where there's only one port anyway. */ + if ((self->priv->enabled_rules & MM_FILTER_RULE_EXPLICIT_WHITELIST) && + (mm_kernel_device_get_global_property_as_boolean (port, ID_MM_DEVICE_PROCESS) || + mm_kernel_device_get_property_as_boolean (port, ID_MM_DEVICE_PROCESS))) { + mm_obj_dbg (self, "(%s/%s) port allowed: device is whitelisted", subsystem, name); + return TRUE; + } + + /* If the device is explicitly blacklisted, we ignore every port. */ + if ((self->priv->enabled_rules & MM_FILTER_RULE_EXPLICIT_BLACKLIST) && + (mm_kernel_device_get_global_property_as_boolean (port, ID_MM_DEVICE_IGNORE))) { + mm_obj_dbg (self, "(%s/%s): port filtered: device is blacklisted", subsystem, name); + return FALSE; + } + + /* If the device is whitelisted by a plugin, we allow it. */ + if (self->priv->enabled_rules & MM_FILTER_RULE_PLUGIN_WHITELIST) { + GList *l; + guint16 vid = 0; + guint16 pid = 0; + + for (l = self->priv->plugin_whitelist_tags; l; l = g_list_next (l)) { + if (mm_kernel_device_get_global_property_as_boolean (port, (const gchar *)(l->data)) || + mm_kernel_device_get_property_as_boolean (port, (const gchar *)(l->data))) { + mm_obj_dbg (self, "(%s/%s) port allowed: device is whitelisted by plugin (tag)", subsystem, name); + return TRUE; + } + } + + vid = mm_kernel_device_get_physdev_vid (port); + if (vid) + pid = mm_kernel_device_get_physdev_pid (port); + + if (vid && pid && self->priv->plugin_whitelist_product_ids) { + guint i; + + for (i = 0; i < self->priv->plugin_whitelist_product_ids->len; i++) { + mm_uint16_pair *item; + + item = &g_array_index (self->priv->plugin_whitelist_product_ids, mm_uint16_pair, i); + if (item->l == vid && item->r == pid) { + mm_obj_dbg (self, "(%s/%s) port allowed: device is whitelisted by plugin (vid/pid)", subsystem, name); + return TRUE; + } + } + } + + if (vid && self->priv->plugin_whitelist_vendor_ids) { + guint i; + + for (i = 0; i < self->priv->plugin_whitelist_vendor_ids->len; i++) { + guint16 item; + + item = g_array_index (self->priv->plugin_whitelist_vendor_ids, guint16, i); + if (item == vid) { + mm_obj_dbg (self, "(%s/%s) port allowed: device is whitelisted by plugin (vid)", subsystem, name); + return TRUE; + } + } + } + } + + /* If this is a QRTR device, we always allow it. This check comes before + * checking for VIRTUAL since qrtr devices don't have a sysfs path, and the + * check for VIRTUAL will return FALSE. */ + if ((self->priv->enabled_rules & MM_FILTER_RULE_QRTR) && + g_str_equal (subsystem, "qrtr")) { + mm_obj_dbg (self, "(%s/%s) port allowed: qrtr device", subsystem, name); + return TRUE; + } + + /* If this is a virtual device, don't allow it */ + if ((self->priv->enabled_rules & MM_FILTER_RULE_VIRTUAL) && + (!mm_kernel_device_get_physdev_sysfs_path (port))) { + mm_obj_dbg (self, "(%s/%s) port filtered: virtual device", subsystem, name); + return FALSE; + } + + /* If this is a net device, we always allow it */ + if ((self->priv->enabled_rules & MM_FILTER_RULE_NET) && + (g_strcmp0 (subsystem, "net") == 0)) { + mm_obj_dbg (self, "(%s/%s) port allowed: net device", subsystem, name); + return TRUE; + } + + /* If this is a cdc-wdm device, we always allow it */ + if ((self->priv->enabled_rules & MM_FILTER_RULE_USBMISC) && + (g_strcmp0 (subsystem, "usbmisc") == 0)) { + mm_obj_dbg (self, "(%s/%s) port allowed: usbmisc device", subsystem, name); + return TRUE; + } + + /* If this is a rpmsg channel device, we always allow it */ + if ((self->priv->enabled_rules & MM_FILTER_RULE_RPMSG) && + (g_strcmp0 (subsystem, "rpmsg") == 0)) { + mm_obj_dbg (self, "(%s/%s) port allowed: rpmsg device", subsystem, name); + return TRUE; + } + + /* If this is a wwan port/device, we always allow it */ + if ((self->priv->enabled_rules & MM_FILTER_RULE_WWAN) && + (g_strcmp0 (subsystem, "wwan") == 0)) { + mm_obj_dbg (self, "(%s/%s) port allowed: wwan device", subsystem, name); + return TRUE; + } + + /* If this is a tty device, we may allow it */ + if ((self->priv->enabled_rules & MM_FILTER_RULE_TTY) && + (g_strcmp0 (subsystem, "tty") == 0)) { + const gchar *physdev_subsystem; + const gchar *driver; + + /* Mixed blacklist/whitelist rules */ + + /* If the physdev is a 'platform' or 'pnp' device that's not whitelisted, ignore it */ + physdev_subsystem = mm_kernel_device_get_physdev_subsystem (port); + if ((self->priv->enabled_rules & MM_FILTER_RULE_TTY_PLATFORM_DRIVER) && + (!g_strcmp0 (physdev_subsystem, "platform") || + !g_strcmp0 (physdev_subsystem, "pci") || + !g_strcmp0 (physdev_subsystem, "pnp") || + !g_strcmp0 (physdev_subsystem, "sdio"))) { + mm_obj_dbg (self, "(%s/%s): port filtered: tty platform driver", subsystem, name); + return FALSE; + } + + /* Whitelist rules last */ + + /* If the TTY kernel driver is one expected modem kernel driver, allow it */ + driver = mm_kernel_device_get_driver (port); + if ((self->priv->enabled_rules & MM_FILTER_RULE_TTY_DRIVER) && + (!g_strcmp0 (driver, "option1") || + !g_strcmp0 (driver, "qcserial") || + !g_strcmp0 (driver, "qcaux") || + !g_strcmp0 (driver, "nozomi") || + !g_strcmp0 (driver, "sierra"))) { + mm_obj_dbg (self, "(%s/%s): port allowed: modem-specific kernel driver detected", subsystem, name); + return TRUE; + } + + /* + * If the TTY kernel driver is cdc-acm and the interface is not + * class=2/subclass=2/protocol=[1-6], forbidden. + * + * Otherwise, we'll require the modem to have more ports other + * than the ttyACM one (see mm_filter_device_and_port()), because + * there are lots of Arduino devices out there exposing a single + * ttyACM port and wrongly claiming AT protocol support... + * + * Class definitions for Communication Devices 1.2 + * Communications Interface Class Control Protocol Codes: + * 00h | USB specification | No class specific protocol required + * 01h | ITU-T V.250 | AT Commands: V.250 etc + * 02h | PCCA-101 | AT Commands defined by PCCA-101 + * 03h | PCCA-101 | AT Commands defined by PCCA-101 & Annex O + * 04h | GSM 7.07 | AT Commands defined by GSM 07.07 + * 05h | 3GPP 27.07 | AT Commands defined by 3GPP 27.007 + * 06h | C-S0017-0 | AT Commands defined by TIA for CDMA + * 07h | USB EEM | Ethernet Emulation Model + * 08h-FDh | | RESERVED (future use) + * FEh | | External Protocol: Commands defined by Command Set Functional Descriptor + * FFh | USB Specification | Vendor-specific + */ + if ((self->priv->enabled_rules & MM_FILTER_RULE_TTY_ACM_INTERFACE) && + (!g_strcmp0 (driver, "cdc_acm")) && + ((mm_kernel_device_get_interface_class (port) != 2) || + (mm_kernel_device_get_interface_subclass (port) != 2) || + (mm_kernel_device_get_interface_protocol (port) < 1) || + (mm_kernel_device_get_interface_protocol (port) > 6))) { + mm_obj_dbg (self, "(%s/%s): port filtered: cdc-acm interface is not AT-capable", subsystem, name); + return FALSE; + } + + /* Default forbidden? flag the port as maybe-forbidden, and go on */ + if (self->priv->enabled_rules & MM_FILTER_RULE_TTY_DEFAULT_FORBIDDEN) { + g_object_set_data (G_OBJECT (port), FILTER_PORT_MAYBE_FORBIDDEN, GUINT_TO_POINTER (TRUE)); + return TRUE; + } + + g_assert_not_reached (); + } + + /* Otherwise forbidden */ + mm_obj_dbg (self, "(%s/%s) port filtered: forbidden port type", subsystem, name); + return FALSE; +} + +/*****************************************************************************/ + +static gboolean +device_has_net_port (MMDevice *device) +{ + GList *l; + + for (l = mm_device_peek_port_probe_list (device); l; l = g_list_next (l)) { + if (!g_strcmp0 (mm_port_probe_get_port_subsys (MM_PORT_PROBE (l->data)), "net")) + return TRUE; + } + return FALSE; +} + +static gboolean +device_has_multiple_ports (MMDevice *device) +{ + return (g_list_length (mm_device_peek_port_probe_list (device)) > 1); +} + +gboolean +mm_filter_device_and_port (MMFilter *self, + MMDevice *device, + MMKernelDevice *port) +{ + const gchar *subsystem; + const gchar *name; + const gchar *driver; + + /* If it wasn't flagged as maybe forbidden, there's nothing to do */ + if (!GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (port), FILTER_PORT_MAYBE_FORBIDDEN))) + return TRUE; + + subsystem = mm_kernel_device_get_subsystem (port); + name = mm_kernel_device_get_name (port); + + /* Check whether this device holds a NET port in addition to this TTY */ + if ((self->priv->enabled_rules & MM_FILTER_RULE_TTY_WITH_NET) && + device_has_net_port (device)) { + mm_obj_dbg (self, "(%s/%s): port allowed: device also exports a net interface", subsystem, name); + return TRUE; + } + + /* Check whether this device holds any other port in addition to the ttyACM port */ + driver = mm_kernel_device_get_driver (port); + if ((self->priv->enabled_rules & MM_FILTER_RULE_TTY_ACM_INTERFACE) && + (!g_strcmp0 (driver, "cdc_acm")) && + device_has_multiple_ports (device)) { + mm_obj_dbg (self, "(%s/%s): port allowed: device exports multiple interfaces", subsystem, name); + return TRUE; + } + + mm_obj_dbg (self, "(%s/%s) port filtered: forbidden", subsystem, name); + return FALSE; +} + +/*****************************************************************************/ +/* Use filter rule names as environment variables to control them on startup: + * - MM_FILTER_RULE_XXX=1 to explicitly enable the rule. + * - MM_FILTER_RULE_XXX=0 to explicitly disable the rule. + */ + +static MMFilterRule +filter_rule_env_process (MMFilterRule enabled_rules) +{ + MMFilterRule updated_rules = enabled_rules; + GFlagsClass *flags_class; + guint i; + + flags_class = g_type_class_ref (MM_TYPE_FILTER_RULE); + + for (i = 0; (1 << i) & MM_FILTER_RULE_ALL; i++) { + GFlagsValue *flags_value; + const gchar *env_value; + + flags_value = g_flags_get_first_value (flags_class, (1 << i)); + g_assert (flags_value); + + env_value = g_getenv (flags_value->value_name); + if (!env_value) + continue; + + if (g_str_equal (env_value, "0")) + updated_rules &= ~(1 << i); + else if (g_str_equal (env_value, "1")) + updated_rules |= (1 << i); + } + + g_type_class_unref (flags_class); + + return updated_rules; +} + +/*****************************************************************************/ + +gboolean +mm_filter_check_rule_enabled (MMFilter *self, + MMFilterRule rule) +{ + return !!(self->priv->enabled_rules & rule); +} + +/*****************************************************************************/ + +static gchar * +log_object_build_id (MMLogObject *_self) +{ + return g_strdup ("filter"); +} + +/*****************************************************************************/ + +/* If TTY rule enabled, DEFAULT_FORBIDDEN must be set. */ +#define VALIDATE_RULE_TTY(rules) (!(rules & MM_FILTER_RULE_TTY) || (rules & (MM_FILTER_RULE_TTY_DEFAULT_FORBIDDEN))) + +MMFilter * +mm_filter_new (MMFilterRule enabled_rules, + GError **error) +{ + MMFilter *self; + MMFilterRule updated_rules; + + /* The input enabled rules are coming from predefined filter profiles. */ + g_assert (VALIDATE_RULE_TTY (enabled_rules)); + updated_rules = filter_rule_env_process (enabled_rules); + if (!VALIDATE_RULE_TTY (updated_rules)) { + g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS, + "Invalid rules after processing envvars"); + return NULL; + } + + self = g_object_new (MM_TYPE_FILTER, + MM_FILTER_ENABLED_RULES, updated_rules, + NULL); + +#define RULE_ENABLED_STR(flag) ((self->priv->enabled_rules & flag) ? "yes" : "no") + + mm_obj_dbg (self, "created"); + mm_obj_dbg (self, " explicit whitelist: %s", RULE_ENABLED_STR (MM_FILTER_RULE_EXPLICIT_WHITELIST)); + mm_obj_dbg (self, " explicit blacklist: %s", RULE_ENABLED_STR (MM_FILTER_RULE_EXPLICIT_BLACKLIST)); + mm_obj_dbg (self, " plugin whitelist: %s", RULE_ENABLED_STR (MM_FILTER_RULE_PLUGIN_WHITELIST)); + mm_obj_dbg (self, " qrtr devices allowed: %s", RULE_ENABLED_STR (MM_FILTER_RULE_QRTR)); + mm_obj_dbg (self, " virtual devices forbidden: %s", RULE_ENABLED_STR (MM_FILTER_RULE_VIRTUAL)); + mm_obj_dbg (self, " net devices allowed: %s", RULE_ENABLED_STR (MM_FILTER_RULE_NET)); + mm_obj_dbg (self, " usbmisc devices allowed: %s", RULE_ENABLED_STR (MM_FILTER_RULE_USBMISC)); + mm_obj_dbg (self, " rpmsg devices allowed: %s", RULE_ENABLED_STR (MM_FILTER_RULE_RPMSG)); + mm_obj_dbg (self, " wwan devices allowed: %s", RULE_ENABLED_STR (MM_FILTER_RULE_WWAN)); + if (self->priv->enabled_rules & MM_FILTER_RULE_TTY) { + mm_obj_dbg (self, " tty devices:"); + mm_obj_dbg (self, " platform driver check: %s", RULE_ENABLED_STR (MM_FILTER_RULE_TTY_PLATFORM_DRIVER)); + mm_obj_dbg (self, " driver check: %s", RULE_ENABLED_STR (MM_FILTER_RULE_TTY_DRIVER)); + mm_obj_dbg (self, " cdc-acm interface check: %s", RULE_ENABLED_STR (MM_FILTER_RULE_TTY_ACM_INTERFACE)); + mm_obj_dbg (self, " with net check: %s", RULE_ENABLED_STR (MM_FILTER_RULE_TTY_WITH_NET)); + if (self->priv->enabled_rules & MM_FILTER_RULE_TTY_DEFAULT_FORBIDDEN) + mm_obj_dbg (self, " default: forbidden"); + else + g_assert_not_reached (); + } else + mm_obj_dbg (self, " tty devices: no"); + +#undef RULE_ENABLED_STR + + return self; +} + +static void +set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + MMFilter *self = MM_FILTER (object); + + switch (prop_id) { + case PROP_ENABLED_RULES: + self->priv->enabled_rules = g_value_get_flags (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + MMFilter *self = MM_FILTER (object); + + switch (prop_id) { + case PROP_ENABLED_RULES: + g_value_set_flags (value, self->priv->enabled_rules); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +mm_filter_init (MMFilter *self) +{ + self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, MM_TYPE_FILTER, MMFilterPrivate); +} + +static void +finalize (GObject *object) +{ + MMFilter *self = MM_FILTER (object); + + g_clear_pointer (&self->priv->plugin_whitelist_vendor_ids, g_array_unref); + g_clear_pointer (&self->priv->plugin_whitelist_product_ids, g_array_unref); + g_list_free_full (self->priv->plugin_whitelist_tags, g_free); + + G_OBJECT_CLASS (mm_filter_parent_class)->finalize (object); +} + +static void +log_object_iface_init (MMLogObjectInterface *iface) +{ + iface->build_id = log_object_build_id; +} + +static void +mm_filter_class_init (MMFilterClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + g_type_class_add_private (object_class, sizeof (MMFilterPrivate)); + + /* Virtual methods */ + object_class->set_property = set_property; + object_class->get_property = get_property; + object_class->finalize = finalize; + + g_object_class_install_property ( + object_class, PROP_ENABLED_RULES, + g_param_spec_flags (MM_FILTER_ENABLED_RULES, + "Enabled rules", + "Mask of rules enabled in the filter", + MM_TYPE_FILTER_RULE, + MM_FILTER_RULE_NONE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); +} |