net: usbnet: support 64bit stats
[sfrench/cifs-2.6.git] / drivers / net / usb / qmi_wwan.c
index 8056745506832867165f03ae0b24c2f1a578d849..adbed261cc8aed15138fb6090258988c6c112a85 100644 (file)
@@ -58,12 +58,198 @@ struct qmi_wwan_state {
 
 enum qmi_wwan_flags {
        QMI_WWAN_FLAG_RAWIP = 1 << 0,
+       QMI_WWAN_FLAG_MUX = 1 << 1,
 };
 
 enum qmi_wwan_quirks {
        QMI_WWAN_QUIRK_DTR = 1 << 0,    /* needs "set DTR" request */
 };
 
+struct qmimux_hdr {
+       u8 pad;
+       u8 mux_id;
+       __be16 pkt_len;
+};
+
+struct qmimux_priv {
+       struct net_device *real_dev;
+       u8 mux_id;
+};
+
+static int qmimux_open(struct net_device *dev)
+{
+       struct qmimux_priv *priv = netdev_priv(dev);
+       struct net_device *real_dev = priv->real_dev;
+
+       if (!(priv->real_dev->flags & IFF_UP))
+               return -ENETDOWN;
+
+       if (netif_carrier_ok(real_dev))
+               netif_carrier_on(dev);
+       return 0;
+}
+
+static int qmimux_stop(struct net_device *dev)
+{
+       netif_carrier_off(dev);
+       return 0;
+}
+
+static netdev_tx_t qmimux_start_xmit(struct sk_buff *skb, struct net_device *dev)
+{
+       struct qmimux_priv *priv = netdev_priv(dev);
+       unsigned int len = skb->len;
+       struct qmimux_hdr *hdr;
+
+       hdr = (struct qmimux_hdr *)skb_push(skb, sizeof(struct qmimux_hdr));
+       hdr->pad = 0;
+       hdr->mux_id = priv->mux_id;
+       hdr->pkt_len = cpu_to_be16(len);
+       skb->dev = priv->real_dev;
+       return dev_queue_xmit(skb);
+}
+
+static const struct net_device_ops qmimux_netdev_ops = {
+       .ndo_open       = qmimux_open,
+       .ndo_stop       = qmimux_stop,
+       .ndo_start_xmit = qmimux_start_xmit,
+};
+
+static void qmimux_setup(struct net_device *dev)
+{
+       dev->header_ops      = NULL;  /* No header */
+       dev->type            = ARPHRD_NONE;
+       dev->hard_header_len = 0;
+       dev->addr_len        = 0;
+       dev->flags           = IFF_POINTOPOINT | IFF_NOARP | IFF_MULTICAST;
+       dev->netdev_ops      = &qmimux_netdev_ops;
+       dev->destructor      = free_netdev;
+}
+
+static struct net_device *qmimux_find_dev(struct usbnet *dev, u8 mux_id)
+{
+       struct qmimux_priv *priv;
+       struct list_head *iter;
+       struct net_device *ldev;
+
+       rcu_read_lock();
+       netdev_for_each_upper_dev_rcu(dev->net, ldev, iter) {
+               priv = netdev_priv(ldev);
+               if (priv->mux_id == mux_id) {
+                       rcu_read_unlock();
+                       return ldev;
+               }
+       }
+       rcu_read_unlock();
+       return NULL;
+}
+
+static bool qmimux_has_slaves(struct usbnet *dev)
+{
+       return !list_empty(&dev->net->adj_list.upper);
+}
+
+static int qmimux_rx_fixup(struct usbnet *dev, struct sk_buff *skb)
+{
+       unsigned int len, offset = sizeof(struct qmimux_hdr);
+       struct qmimux_hdr *hdr;
+       struct net_device *net;
+       struct sk_buff *skbn;
+
+       while (offset < skb->len) {
+               hdr = (struct qmimux_hdr *)skb->data;
+               len = be16_to_cpu(hdr->pkt_len);
+
+               /* drop the packet, bogus length */
+               if (offset + len > skb->len)
+                       return 0;
+
+               /* control packet, we do not know what to do */
+               if (hdr->pad & 0x80)
+                       goto skip;
+
+               net = qmimux_find_dev(dev, hdr->mux_id);
+               if (!net)
+                       goto skip;
+               skbn = netdev_alloc_skb(net, len);
+               if (!skbn)
+                       return 0;
+               skbn->dev = net;
+
+               switch (skb->data[offset] & 0xf0) {
+               case 0x40:
+                       skbn->protocol = htons(ETH_P_IP);
+                       break;
+               case 0x60:
+                       skbn->protocol = htons(ETH_P_IPV6);
+                       break;
+               default:
+                       /* not ip - do not know what to do */
+                       goto skip;
+               }
+
+               memcpy(skb_put(skbn, len), skb->data + offset, len);
+               if (netif_rx(skbn) != NET_RX_SUCCESS)
+                       return 0;
+
+skip:
+               offset += len + sizeof(struct qmimux_hdr);
+       }
+       return 1;
+}
+
+static int qmimux_register_device(struct net_device *real_dev, u8 mux_id)
+{
+       struct net_device *new_dev;
+       struct qmimux_priv *priv;
+       int err;
+
+       new_dev = alloc_netdev(sizeof(struct qmimux_priv),
+                              "qmimux%d", NET_NAME_UNKNOWN, qmimux_setup);
+       if (!new_dev)
+               return -ENOBUFS;
+
+       dev_net_set(new_dev, dev_net(real_dev));
+       priv = netdev_priv(new_dev);
+       priv->mux_id = mux_id;
+       priv->real_dev = real_dev;
+
+       err = register_netdevice(new_dev);
+       if (err < 0)
+               goto out_free_newdev;
+
+       /* Account for reference in struct qmimux_priv_priv */
+       dev_hold(real_dev);
+
+       err = netdev_upper_dev_link(real_dev, new_dev);
+       if (err)
+               goto out_unregister_netdev;
+
+       netif_stacked_transfer_operstate(real_dev, new_dev);
+
+       return 0;
+
+out_unregister_netdev:
+       unregister_netdevice(new_dev);
+       dev_put(real_dev);
+
+out_free_newdev:
+       free_netdev(new_dev);
+       return err;
+}
+
+static void qmimux_unregister_device(struct net_device *dev)
+{
+       struct qmimux_priv *priv = netdev_priv(dev);
+       struct net_device *real_dev = priv->real_dev;
+
+       netdev_upper_dev_unlink(real_dev, dev);
+       unregister_netdevice(dev);
+
+       /* Get rid of the reference to real_dev */
+       dev_put(real_dev);
+}
+
 static void qmi_wwan_netdev_setup(struct net_device *net)
 {
        struct usbnet *dev = netdev_priv(net);
@@ -137,10 +323,114 @@ err:
        return ret;
 }
 
+static ssize_t add_mux_show(struct device *d, struct device_attribute *attr, char *buf)
+{
+       struct net_device *dev = to_net_dev(d);
+       struct qmimux_priv *priv;
+       struct list_head *iter;
+       struct net_device *ldev;
+       ssize_t count = 0;
+
+       rcu_read_lock();
+       netdev_for_each_upper_dev_rcu(dev, ldev, iter) {
+               priv = netdev_priv(ldev);
+               count += scnprintf(&buf[count], PAGE_SIZE - count,
+                                  "0x%02x\n", priv->mux_id);
+       }
+       rcu_read_unlock();
+       return count;
+}
+
+static ssize_t add_mux_store(struct device *d,  struct device_attribute *attr, const char *buf, size_t len)
+{
+       struct usbnet *dev = netdev_priv(to_net_dev(d));
+       struct qmi_wwan_state *info = (void *)&dev->data;
+       u8 mux_id;
+       int ret;
+
+       if (kstrtou8(buf, 0, &mux_id))
+               return -EINVAL;
+
+       /* mux_id [1 - 0x7f] range empirically found */
+       if (mux_id < 1 || mux_id > 0x7f)
+               return -EINVAL;
+
+       if (!rtnl_trylock())
+               return restart_syscall();
+
+       if (qmimux_find_dev(dev, mux_id)) {
+               netdev_err(dev->net, "mux_id already present\n");
+               ret = -EINVAL;
+               goto err;
+       }
+
+       /* we don't want to modify a running netdev */
+       if (netif_running(dev->net)) {
+               netdev_err(dev->net, "Cannot change a running device\n");
+               ret = -EBUSY;
+               goto err;
+       }
+
+       ret = qmimux_register_device(dev->net, mux_id);
+       if (!ret) {
+               info->flags |= QMI_WWAN_FLAG_MUX;
+               ret = len;
+       }
+err:
+       rtnl_unlock();
+       return ret;
+}
+
+static ssize_t del_mux_show(struct device *d, struct device_attribute *attr, char *buf)
+{
+       return add_mux_show(d, attr, buf);
+}
+
+static ssize_t del_mux_store(struct device *d,  struct device_attribute *attr, const char *buf, size_t len)
+{
+       struct usbnet *dev = netdev_priv(to_net_dev(d));
+       struct qmi_wwan_state *info = (void *)&dev->data;
+       struct net_device *del_dev;
+       u8 mux_id;
+       int ret = 0;
+
+       if (kstrtou8(buf, 0, &mux_id))
+               return -EINVAL;
+
+       if (!rtnl_trylock())
+               return restart_syscall();
+
+       /* we don't want to modify a running netdev */
+       if (netif_running(dev->net)) {
+               netdev_err(dev->net, "Cannot change a running device\n");
+               ret = -EBUSY;
+               goto err;
+       }
+
+       del_dev = qmimux_find_dev(dev, mux_id);
+       if (!del_dev) {
+               netdev_err(dev->net, "mux_id not present\n");
+               ret = -EINVAL;
+               goto err;
+       }
+       qmimux_unregister_device(del_dev);
+
+       if (!qmimux_has_slaves(dev))
+               info->flags &= ~QMI_WWAN_FLAG_MUX;
+       ret = len;
+err:
+       rtnl_unlock();
+       return ret;
+}
+
 static DEVICE_ATTR_RW(raw_ip);
+static DEVICE_ATTR_RW(add_mux);
+static DEVICE_ATTR_RW(del_mux);
 
 static struct attribute *qmi_wwan_sysfs_attrs[] = {
        &dev_attr_raw_ip.attr,
+       &dev_attr_add_mux.attr,
+       &dev_attr_del_mux.attr,
        NULL,
 };
 
@@ -184,6 +474,9 @@ static int qmi_wwan_rx_fixup(struct usbnet *dev, struct sk_buff *skb)
        if (skb->len < dev->net->hard_header_len)
                return 0;
 
+       if (info->flags & QMI_WWAN_FLAG_MUX)
+               return qmimux_rx_fixup(dev, skb);
+
        switch (skb->data[0] & 0xf0) {
        case 0x40:
                proto = htons(ETH_P_IP);
@@ -249,6 +542,7 @@ static const struct net_device_ops qmi_wwan_netdev_ops = {
        .ndo_start_xmit         = usbnet_start_xmit,
        .ndo_tx_timeout         = usbnet_tx_timeout,
        .ndo_change_mtu         = usbnet_change_mtu,
+       .ndo_get_stats64        = usbnet_get_stats64,
        .ndo_set_mac_address    = qmi_wwan_mac_addr,
        .ndo_validate_addr      = eth_validate_addr,
 };
@@ -580,6 +874,10 @@ static const struct usb_device_id products[] = {
                USB_VENDOR_AND_INTERFACE_INFO(HUAWEI_VENDOR_ID, USB_CLASS_VENDOR_SPEC, 0x01, 0x69),
                .driver_info        = (unsigned long)&qmi_wwan_info,
        },
+       {       /* Motorola Mapphone devices with MDM6600 */
+               USB_VENDOR_AND_INTERFACE_INFO(0x22b8, USB_CLASS_VENDOR_SPEC, 0xfb, 0xff),
+               .driver_info        = (unsigned long)&qmi_wwan_info,
+       },
 
        /* 2. Combined interface devices matching on class+protocol */
        {       /* Huawei E367 and possibly others in "Windows mode" */
@@ -925,6 +1223,8 @@ static const struct usb_device_id products[] = {
        {QMI_FIXED_INTF(0x413c, 0x81a9, 8)},    /* Dell Wireless 5808e Gobi(TM) 4G LTE Mobile Broadband Card */
        {QMI_FIXED_INTF(0x413c, 0x81b1, 8)},    /* Dell Wireless 5809e Gobi(TM) 4G LTE Mobile Broadband Card */
        {QMI_FIXED_INTF(0x413c, 0x81b3, 8)},    /* Dell Wireless 5809e Gobi(TM) 4G LTE Mobile Broadband Card (rev3) */
+       {QMI_FIXED_INTF(0x413c, 0x81b6, 8)},    /* Dell Wireless 5811e */
+       {QMI_FIXED_INTF(0x413c, 0x81b6, 10)},   /* Dell Wireless 5811e */
        {QMI_FIXED_INTF(0x03f0, 0x4e1d, 8)},    /* HP lt4111 LTE/EV-DO/HSPA+ Gobi 4G Module */
        {QMI_FIXED_INTF(0x22de, 0x9061, 3)},    /* WeTelecom WPD-600N */
        {QMI_FIXED_INTF(0x1e0e, 0x9001, 5)},    /* SIMCom 7230E */
@@ -1030,11 +1330,33 @@ static int qmi_wwan_probe(struct usb_interface *intf,
        return usbnet_probe(intf, id);
 }
 
+static void qmi_wwan_disconnect(struct usb_interface *intf)
+{
+       struct usbnet *dev = usb_get_intfdata(intf);
+       struct qmi_wwan_state *info = (void *)&dev->data;
+       struct list_head *iter;
+       struct net_device *ldev;
+
+       if (info->flags & QMI_WWAN_FLAG_MUX) {
+               if (!rtnl_trylock()) {
+                       restart_syscall();
+                       return;
+               }
+               rcu_read_lock();
+               netdev_for_each_upper_dev_rcu(dev->net, ldev, iter)
+                       qmimux_unregister_device(ldev);
+               rcu_read_unlock();
+               rtnl_unlock();
+               info->flags &= ~QMI_WWAN_FLAG_MUX;
+       }
+       usbnet_disconnect(intf);
+}
+
 static struct usb_driver qmi_wwan_driver = {
        .name                 = "qmi_wwan",
        .id_table             = products,
        .probe                = qmi_wwan_probe,
-       .disconnect           = usbnet_disconnect,
+       .disconnect           = qmi_wwan_disconnect,
        .suspend              = qmi_wwan_suspend,
        .resume               = qmi_wwan_resume,
        .reset_resume         = qmi_wwan_resume,