18c2ecf20Sopenharmony_ci// SPDX-License-Identifier: GPL-2.0-or-later 28c2ecf20Sopenharmony_ci/* 38c2ecf20Sopenharmony_ci * Ethernet interface part of the LG VL600 LTE modem (4G dongle) 48c2ecf20Sopenharmony_ci * 58c2ecf20Sopenharmony_ci * Copyright (C) 2011 Intel Corporation 68c2ecf20Sopenharmony_ci * Author: Andrzej Zaborowski <balrogg@gmail.com> 78c2ecf20Sopenharmony_ci */ 88c2ecf20Sopenharmony_ci#include <linux/etherdevice.h> 98c2ecf20Sopenharmony_ci#include <linux/ethtool.h> 108c2ecf20Sopenharmony_ci#include <linux/mii.h> 118c2ecf20Sopenharmony_ci#include <linux/usb.h> 128c2ecf20Sopenharmony_ci#include <linux/usb/cdc.h> 138c2ecf20Sopenharmony_ci#include <linux/usb/usbnet.h> 148c2ecf20Sopenharmony_ci#include <linux/if_ether.h> 158c2ecf20Sopenharmony_ci#include <linux/if_arp.h> 168c2ecf20Sopenharmony_ci#include <linux/inetdevice.h> 178c2ecf20Sopenharmony_ci#include <linux/module.h> 188c2ecf20Sopenharmony_ci 198c2ecf20Sopenharmony_ci/* 208c2ecf20Sopenharmony_ci * The device has a CDC ACM port for modem control (it claims to be 218c2ecf20Sopenharmony_ci * CDC ACM anyway) and a CDC Ethernet port for actual network data. 228c2ecf20Sopenharmony_ci * It will however ignore data on both ports that is not encapsulated 238c2ecf20Sopenharmony_ci * in a specific way, any data returned is also encapsulated the same 248c2ecf20Sopenharmony_ci * way. The headers don't seem to follow any popular standard. 258c2ecf20Sopenharmony_ci * 268c2ecf20Sopenharmony_ci * This driver adds and strips these headers from the ethernet frames 278c2ecf20Sopenharmony_ci * sent/received from the CDC Ethernet port. The proprietary header 288c2ecf20Sopenharmony_ci * replaces the standard ethernet header in a packet so only actual 298c2ecf20Sopenharmony_ci * ethernet frames are allowed. The headers allow some form of 308c2ecf20Sopenharmony_ci * multiplexing by using non standard values of the .h_proto field. 318c2ecf20Sopenharmony_ci * Windows/Mac drivers do send a couple of such frames to the device 328c2ecf20Sopenharmony_ci * during initialisation, with protocol set to 0x0906 or 0x0b06 and (what 338c2ecf20Sopenharmony_ci * seems to be) a flag in the .dummy_flags. This doesn't seem necessary 348c2ecf20Sopenharmony_ci * for modem operation but can possibly be used for GPS or other funcitons. 358c2ecf20Sopenharmony_ci */ 368c2ecf20Sopenharmony_ci 378c2ecf20Sopenharmony_cistruct vl600_frame_hdr { 388c2ecf20Sopenharmony_ci __le32 len; 398c2ecf20Sopenharmony_ci __le32 serial; 408c2ecf20Sopenharmony_ci __le32 pkt_cnt; 418c2ecf20Sopenharmony_ci __le32 dummy_flags; 428c2ecf20Sopenharmony_ci __le32 dummy; 438c2ecf20Sopenharmony_ci __le32 magic; 448c2ecf20Sopenharmony_ci} __attribute__((packed)); 458c2ecf20Sopenharmony_ci 468c2ecf20Sopenharmony_cistruct vl600_pkt_hdr { 478c2ecf20Sopenharmony_ci __le32 dummy[2]; 488c2ecf20Sopenharmony_ci __le32 len; 498c2ecf20Sopenharmony_ci __be16 h_proto; 508c2ecf20Sopenharmony_ci} __attribute__((packed)); 518c2ecf20Sopenharmony_ci 528c2ecf20Sopenharmony_cistruct vl600_state { 538c2ecf20Sopenharmony_ci struct sk_buff *current_rx_buf; 548c2ecf20Sopenharmony_ci}; 558c2ecf20Sopenharmony_ci 568c2ecf20Sopenharmony_cistatic int vl600_bind(struct usbnet *dev, struct usb_interface *intf) 578c2ecf20Sopenharmony_ci{ 588c2ecf20Sopenharmony_ci int ret; 598c2ecf20Sopenharmony_ci struct vl600_state *s = kzalloc(sizeof(struct vl600_state), GFP_KERNEL); 608c2ecf20Sopenharmony_ci 618c2ecf20Sopenharmony_ci if (!s) 628c2ecf20Sopenharmony_ci return -ENOMEM; 638c2ecf20Sopenharmony_ci 648c2ecf20Sopenharmony_ci ret = usbnet_cdc_bind(dev, intf); 658c2ecf20Sopenharmony_ci if (ret) { 668c2ecf20Sopenharmony_ci kfree(s); 678c2ecf20Sopenharmony_ci return ret; 688c2ecf20Sopenharmony_ci } 698c2ecf20Sopenharmony_ci 708c2ecf20Sopenharmony_ci dev->driver_priv = s; 718c2ecf20Sopenharmony_ci 728c2ecf20Sopenharmony_ci /* ARP packets don't go through, but they're also of no use. The 738c2ecf20Sopenharmony_ci * subnet has only two hosts anyway: us and the gateway / DHCP 748c2ecf20Sopenharmony_ci * server (probably simulated by modem firmware or network operator) 758c2ecf20Sopenharmony_ci * whose address changes everytime we connect to the intarwebz and 768c2ecf20Sopenharmony_ci * who doesn't bother answering ARP requests either. So hardware 778c2ecf20Sopenharmony_ci * addresses have no meaning, the destination and the source of every 788c2ecf20Sopenharmony_ci * packet depend only on whether it is on the IN or OUT endpoint. */ 798c2ecf20Sopenharmony_ci dev->net->flags |= IFF_NOARP; 808c2ecf20Sopenharmony_ci /* IPv6 NDP relies on multicast. Enable it by default. */ 818c2ecf20Sopenharmony_ci dev->net->flags |= IFF_MULTICAST; 828c2ecf20Sopenharmony_ci 838c2ecf20Sopenharmony_ci return ret; 848c2ecf20Sopenharmony_ci} 858c2ecf20Sopenharmony_ci 868c2ecf20Sopenharmony_cistatic void vl600_unbind(struct usbnet *dev, struct usb_interface *intf) 878c2ecf20Sopenharmony_ci{ 888c2ecf20Sopenharmony_ci struct vl600_state *s = dev->driver_priv; 898c2ecf20Sopenharmony_ci 908c2ecf20Sopenharmony_ci dev_kfree_skb(s->current_rx_buf); 918c2ecf20Sopenharmony_ci kfree(s); 928c2ecf20Sopenharmony_ci 938c2ecf20Sopenharmony_ci return usbnet_cdc_unbind(dev, intf); 948c2ecf20Sopenharmony_ci} 958c2ecf20Sopenharmony_ci 968c2ecf20Sopenharmony_cistatic int vl600_rx_fixup(struct usbnet *dev, struct sk_buff *skb) 978c2ecf20Sopenharmony_ci{ 988c2ecf20Sopenharmony_ci struct vl600_frame_hdr *frame; 998c2ecf20Sopenharmony_ci struct vl600_pkt_hdr *packet; 1008c2ecf20Sopenharmony_ci struct ethhdr *ethhdr; 1018c2ecf20Sopenharmony_ci int packet_len, count; 1028c2ecf20Sopenharmony_ci struct sk_buff *buf = skb; 1038c2ecf20Sopenharmony_ci struct sk_buff *clone; 1048c2ecf20Sopenharmony_ci struct vl600_state *s = dev->driver_priv; 1058c2ecf20Sopenharmony_ci 1068c2ecf20Sopenharmony_ci /* Frame lengths are generally 4B multiplies but every couple of 1078c2ecf20Sopenharmony_ci * hours there's an odd number of bytes sized yet correct frame, 1088c2ecf20Sopenharmony_ci * so don't require this. */ 1098c2ecf20Sopenharmony_ci 1108c2ecf20Sopenharmony_ci /* Allow a packet (or multiple packets batched together) to be 1118c2ecf20Sopenharmony_ci * split across many frames. We don't allow a new batch to 1128c2ecf20Sopenharmony_ci * begin in the same frame another one is ending however, and no 1138c2ecf20Sopenharmony_ci * leading or trailing pad bytes. */ 1148c2ecf20Sopenharmony_ci if (s->current_rx_buf) { 1158c2ecf20Sopenharmony_ci frame = (struct vl600_frame_hdr *) s->current_rx_buf->data; 1168c2ecf20Sopenharmony_ci if (skb->len + s->current_rx_buf->len > 1178c2ecf20Sopenharmony_ci le32_to_cpup(&frame->len)) { 1188c2ecf20Sopenharmony_ci netif_err(dev, ifup, dev->net, "Fragment too long\n"); 1198c2ecf20Sopenharmony_ci dev->net->stats.rx_length_errors++; 1208c2ecf20Sopenharmony_ci goto error; 1218c2ecf20Sopenharmony_ci } 1228c2ecf20Sopenharmony_ci 1238c2ecf20Sopenharmony_ci buf = s->current_rx_buf; 1248c2ecf20Sopenharmony_ci skb_put_data(buf, skb->data, skb->len); 1258c2ecf20Sopenharmony_ci } else if (skb->len < 4) { 1268c2ecf20Sopenharmony_ci netif_err(dev, ifup, dev->net, "Frame too short\n"); 1278c2ecf20Sopenharmony_ci dev->net->stats.rx_length_errors++; 1288c2ecf20Sopenharmony_ci goto error; 1298c2ecf20Sopenharmony_ci } 1308c2ecf20Sopenharmony_ci 1318c2ecf20Sopenharmony_ci frame = (struct vl600_frame_hdr *) buf->data; 1328c2ecf20Sopenharmony_ci /* Yes, check that frame->magic == 0x53544448 (or 0x44544d48), 1338c2ecf20Sopenharmony_ci * otherwise we may run out of memory w/a bad packet */ 1348c2ecf20Sopenharmony_ci if (ntohl(frame->magic) != 0x53544448 && 1358c2ecf20Sopenharmony_ci ntohl(frame->magic) != 0x44544d48) 1368c2ecf20Sopenharmony_ci goto error; 1378c2ecf20Sopenharmony_ci 1388c2ecf20Sopenharmony_ci if (buf->len < sizeof(*frame) || 1398c2ecf20Sopenharmony_ci buf->len != le32_to_cpup(&frame->len)) { 1408c2ecf20Sopenharmony_ci /* Save this fragment for later assembly */ 1418c2ecf20Sopenharmony_ci if (s->current_rx_buf) 1428c2ecf20Sopenharmony_ci return 0; 1438c2ecf20Sopenharmony_ci 1448c2ecf20Sopenharmony_ci s->current_rx_buf = skb_copy_expand(skb, 0, 1458c2ecf20Sopenharmony_ci le32_to_cpup(&frame->len), GFP_ATOMIC); 1468c2ecf20Sopenharmony_ci if (!s->current_rx_buf) 1478c2ecf20Sopenharmony_ci dev->net->stats.rx_errors++; 1488c2ecf20Sopenharmony_ci 1498c2ecf20Sopenharmony_ci return 0; 1508c2ecf20Sopenharmony_ci } 1518c2ecf20Sopenharmony_ci 1528c2ecf20Sopenharmony_ci count = le32_to_cpup(&frame->pkt_cnt); 1538c2ecf20Sopenharmony_ci 1548c2ecf20Sopenharmony_ci skb_pull(buf, sizeof(*frame)); 1558c2ecf20Sopenharmony_ci 1568c2ecf20Sopenharmony_ci while (count--) { 1578c2ecf20Sopenharmony_ci if (buf->len < sizeof(*packet)) { 1588c2ecf20Sopenharmony_ci netif_err(dev, ifup, dev->net, "Packet too short\n"); 1598c2ecf20Sopenharmony_ci goto error; 1608c2ecf20Sopenharmony_ci } 1618c2ecf20Sopenharmony_ci 1628c2ecf20Sopenharmony_ci packet = (struct vl600_pkt_hdr *) buf->data; 1638c2ecf20Sopenharmony_ci packet_len = sizeof(*packet) + le32_to_cpup(&packet->len); 1648c2ecf20Sopenharmony_ci if (packet_len > buf->len) { 1658c2ecf20Sopenharmony_ci netif_err(dev, ifup, dev->net, 1668c2ecf20Sopenharmony_ci "Bad packet length stored in header\n"); 1678c2ecf20Sopenharmony_ci goto error; 1688c2ecf20Sopenharmony_ci } 1698c2ecf20Sopenharmony_ci 1708c2ecf20Sopenharmony_ci /* Packet header is same size as the ethernet header 1718c2ecf20Sopenharmony_ci * (sizeof(*packet) == sizeof(*ethhdr)), additionally 1728c2ecf20Sopenharmony_ci * the h_proto field is in the same place so we just leave it 1738c2ecf20Sopenharmony_ci * alone and fill in the remaining fields. 1748c2ecf20Sopenharmony_ci */ 1758c2ecf20Sopenharmony_ci ethhdr = (struct ethhdr *) skb->data; 1768c2ecf20Sopenharmony_ci if (be16_to_cpup(ðhdr->h_proto) == ETH_P_ARP && 1778c2ecf20Sopenharmony_ci buf->len > 0x26) { 1788c2ecf20Sopenharmony_ci /* Copy the addresses from packet contents */ 1798c2ecf20Sopenharmony_ci memcpy(ethhdr->h_source, 1808c2ecf20Sopenharmony_ci &buf->data[sizeof(*ethhdr) + 0x8], 1818c2ecf20Sopenharmony_ci ETH_ALEN); 1828c2ecf20Sopenharmony_ci memcpy(ethhdr->h_dest, 1838c2ecf20Sopenharmony_ci &buf->data[sizeof(*ethhdr) + 0x12], 1848c2ecf20Sopenharmony_ci ETH_ALEN); 1858c2ecf20Sopenharmony_ci } else { 1868c2ecf20Sopenharmony_ci eth_zero_addr(ethhdr->h_source); 1878c2ecf20Sopenharmony_ci memcpy(ethhdr->h_dest, dev->net->dev_addr, ETH_ALEN); 1888c2ecf20Sopenharmony_ci 1898c2ecf20Sopenharmony_ci /* Inbound IPv6 packets have an IPv4 ethertype (0x800) 1908c2ecf20Sopenharmony_ci * for some reason. Peek at the L3 header to check 1918c2ecf20Sopenharmony_ci * for IPv6 packets, and set the ethertype to IPv6 1928c2ecf20Sopenharmony_ci * (0x86dd) so Linux can understand it. 1938c2ecf20Sopenharmony_ci */ 1948c2ecf20Sopenharmony_ci if ((buf->data[sizeof(*ethhdr)] & 0xf0) == 0x60) 1958c2ecf20Sopenharmony_ci ethhdr->h_proto = htons(ETH_P_IPV6); 1968c2ecf20Sopenharmony_ci } 1978c2ecf20Sopenharmony_ci 1988c2ecf20Sopenharmony_ci if (count) { 1998c2ecf20Sopenharmony_ci /* Not the last packet in this batch */ 2008c2ecf20Sopenharmony_ci clone = skb_clone(buf, GFP_ATOMIC); 2018c2ecf20Sopenharmony_ci if (!clone) 2028c2ecf20Sopenharmony_ci goto error; 2038c2ecf20Sopenharmony_ci 2048c2ecf20Sopenharmony_ci skb_trim(clone, packet_len); 2058c2ecf20Sopenharmony_ci usbnet_skb_return(dev, clone); 2068c2ecf20Sopenharmony_ci 2078c2ecf20Sopenharmony_ci skb_pull(buf, (packet_len + 3) & ~3); 2088c2ecf20Sopenharmony_ci } else { 2098c2ecf20Sopenharmony_ci skb_trim(buf, packet_len); 2108c2ecf20Sopenharmony_ci 2118c2ecf20Sopenharmony_ci if (s->current_rx_buf) { 2128c2ecf20Sopenharmony_ci usbnet_skb_return(dev, buf); 2138c2ecf20Sopenharmony_ci s->current_rx_buf = NULL; 2148c2ecf20Sopenharmony_ci return 0; 2158c2ecf20Sopenharmony_ci } 2168c2ecf20Sopenharmony_ci 2178c2ecf20Sopenharmony_ci return 1; 2188c2ecf20Sopenharmony_ci } 2198c2ecf20Sopenharmony_ci } 2208c2ecf20Sopenharmony_ci 2218c2ecf20Sopenharmony_cierror: 2228c2ecf20Sopenharmony_ci if (s->current_rx_buf) { 2238c2ecf20Sopenharmony_ci dev_kfree_skb_any(s->current_rx_buf); 2248c2ecf20Sopenharmony_ci s->current_rx_buf = NULL; 2258c2ecf20Sopenharmony_ci } 2268c2ecf20Sopenharmony_ci dev->net->stats.rx_errors++; 2278c2ecf20Sopenharmony_ci return 0; 2288c2ecf20Sopenharmony_ci} 2298c2ecf20Sopenharmony_ci 2308c2ecf20Sopenharmony_cistatic struct sk_buff *vl600_tx_fixup(struct usbnet *dev, 2318c2ecf20Sopenharmony_ci struct sk_buff *skb, gfp_t flags) 2328c2ecf20Sopenharmony_ci{ 2338c2ecf20Sopenharmony_ci struct sk_buff *ret; 2348c2ecf20Sopenharmony_ci struct vl600_frame_hdr *frame; 2358c2ecf20Sopenharmony_ci struct vl600_pkt_hdr *packet; 2368c2ecf20Sopenharmony_ci static uint32_t serial = 1; 2378c2ecf20Sopenharmony_ci int orig_len = skb->len - sizeof(struct ethhdr); 2388c2ecf20Sopenharmony_ci int full_len = (skb->len + sizeof(struct vl600_frame_hdr) + 3) & ~3; 2398c2ecf20Sopenharmony_ci 2408c2ecf20Sopenharmony_ci frame = (struct vl600_frame_hdr *) skb->data; 2418c2ecf20Sopenharmony_ci if (skb->len > sizeof(*frame) && skb->len == le32_to_cpup(&frame->len)) 2428c2ecf20Sopenharmony_ci return skb; /* Already encapsulated? */ 2438c2ecf20Sopenharmony_ci 2448c2ecf20Sopenharmony_ci if (skb->len < sizeof(struct ethhdr)) 2458c2ecf20Sopenharmony_ci /* Drop, device can only deal with ethernet packets */ 2468c2ecf20Sopenharmony_ci return NULL; 2478c2ecf20Sopenharmony_ci 2488c2ecf20Sopenharmony_ci if (!skb_cloned(skb)) { 2498c2ecf20Sopenharmony_ci int headroom = skb_headroom(skb); 2508c2ecf20Sopenharmony_ci int tailroom = skb_tailroom(skb); 2518c2ecf20Sopenharmony_ci 2528c2ecf20Sopenharmony_ci if (tailroom >= full_len - skb->len - sizeof(*frame) && 2538c2ecf20Sopenharmony_ci headroom >= sizeof(*frame)) 2548c2ecf20Sopenharmony_ci /* There's enough head and tail room */ 2558c2ecf20Sopenharmony_ci goto encapsulate; 2568c2ecf20Sopenharmony_ci 2578c2ecf20Sopenharmony_ci if (headroom + tailroom + skb->len >= full_len) { 2588c2ecf20Sopenharmony_ci /* There's enough total room, just readjust */ 2598c2ecf20Sopenharmony_ci skb->data = memmove(skb->head + sizeof(*frame), 2608c2ecf20Sopenharmony_ci skb->data, skb->len); 2618c2ecf20Sopenharmony_ci skb_set_tail_pointer(skb, skb->len); 2628c2ecf20Sopenharmony_ci goto encapsulate; 2638c2ecf20Sopenharmony_ci } 2648c2ecf20Sopenharmony_ci } 2658c2ecf20Sopenharmony_ci 2668c2ecf20Sopenharmony_ci /* Alloc a new skb with the required size */ 2678c2ecf20Sopenharmony_ci ret = skb_copy_expand(skb, sizeof(struct vl600_frame_hdr), full_len - 2688c2ecf20Sopenharmony_ci skb->len - sizeof(struct vl600_frame_hdr), flags); 2698c2ecf20Sopenharmony_ci dev_kfree_skb_any(skb); 2708c2ecf20Sopenharmony_ci if (!ret) 2718c2ecf20Sopenharmony_ci return ret; 2728c2ecf20Sopenharmony_ci skb = ret; 2738c2ecf20Sopenharmony_ci 2748c2ecf20Sopenharmony_ciencapsulate: 2758c2ecf20Sopenharmony_ci /* Packet header is same size as ethernet packet header 2768c2ecf20Sopenharmony_ci * (sizeof(*packet) == sizeof(struct ethhdr)), additionally the 2778c2ecf20Sopenharmony_ci * h_proto field is in the same place so we just leave it alone and 2788c2ecf20Sopenharmony_ci * overwrite the remaining fields. 2798c2ecf20Sopenharmony_ci */ 2808c2ecf20Sopenharmony_ci packet = (struct vl600_pkt_hdr *) skb->data; 2818c2ecf20Sopenharmony_ci /* The VL600 wants IPv6 packets to have an IPv4 ethertype 2828c2ecf20Sopenharmony_ci * Since this modem only supports IPv4 and IPv6, just set all 2838c2ecf20Sopenharmony_ci * frames to 0x0800 (ETH_P_IP) 2848c2ecf20Sopenharmony_ci */ 2858c2ecf20Sopenharmony_ci packet->h_proto = htons(ETH_P_IP); 2868c2ecf20Sopenharmony_ci memset(&packet->dummy, 0, sizeof(packet->dummy)); 2878c2ecf20Sopenharmony_ci packet->len = cpu_to_le32(orig_len); 2888c2ecf20Sopenharmony_ci 2898c2ecf20Sopenharmony_ci frame = skb_push(skb, sizeof(*frame)); 2908c2ecf20Sopenharmony_ci memset(frame, 0, sizeof(*frame)); 2918c2ecf20Sopenharmony_ci frame->len = cpu_to_le32(full_len); 2928c2ecf20Sopenharmony_ci frame->serial = cpu_to_le32(serial++); 2938c2ecf20Sopenharmony_ci frame->pkt_cnt = cpu_to_le32(1); 2948c2ecf20Sopenharmony_ci 2958c2ecf20Sopenharmony_ci if (skb->len < full_len) /* Pad */ 2968c2ecf20Sopenharmony_ci skb_put(skb, full_len - skb->len); 2978c2ecf20Sopenharmony_ci 2988c2ecf20Sopenharmony_ci return skb; 2998c2ecf20Sopenharmony_ci} 3008c2ecf20Sopenharmony_ci 3018c2ecf20Sopenharmony_cistatic const struct driver_info vl600_info = { 3028c2ecf20Sopenharmony_ci .description = "LG VL600 modem", 3038c2ecf20Sopenharmony_ci .flags = FLAG_RX_ASSEMBLE | FLAG_WWAN, 3048c2ecf20Sopenharmony_ci .bind = vl600_bind, 3058c2ecf20Sopenharmony_ci .unbind = vl600_unbind, 3068c2ecf20Sopenharmony_ci .status = usbnet_cdc_status, 3078c2ecf20Sopenharmony_ci .rx_fixup = vl600_rx_fixup, 3088c2ecf20Sopenharmony_ci .tx_fixup = vl600_tx_fixup, 3098c2ecf20Sopenharmony_ci}; 3108c2ecf20Sopenharmony_ci 3118c2ecf20Sopenharmony_cistatic const struct usb_device_id products[] = { 3128c2ecf20Sopenharmony_ci { 3138c2ecf20Sopenharmony_ci USB_DEVICE_AND_INTERFACE_INFO(0x1004, 0x61aa, USB_CLASS_COMM, 3148c2ecf20Sopenharmony_ci USB_CDC_SUBCLASS_ETHERNET, USB_CDC_PROTO_NONE), 3158c2ecf20Sopenharmony_ci .driver_info = (unsigned long) &vl600_info, 3168c2ecf20Sopenharmony_ci }, 3178c2ecf20Sopenharmony_ci {}, /* End */ 3188c2ecf20Sopenharmony_ci}; 3198c2ecf20Sopenharmony_ciMODULE_DEVICE_TABLE(usb, products); 3208c2ecf20Sopenharmony_ci 3218c2ecf20Sopenharmony_cistatic struct usb_driver lg_vl600_driver = { 3228c2ecf20Sopenharmony_ci .name = "lg-vl600", 3238c2ecf20Sopenharmony_ci .id_table = products, 3248c2ecf20Sopenharmony_ci .probe = usbnet_probe, 3258c2ecf20Sopenharmony_ci .disconnect = usbnet_disconnect, 3268c2ecf20Sopenharmony_ci .suspend = usbnet_suspend, 3278c2ecf20Sopenharmony_ci .resume = usbnet_resume, 3288c2ecf20Sopenharmony_ci .disable_hub_initiated_lpm = 1, 3298c2ecf20Sopenharmony_ci}; 3308c2ecf20Sopenharmony_ci 3318c2ecf20Sopenharmony_cimodule_usb_driver(lg_vl600_driver); 3328c2ecf20Sopenharmony_ci 3338c2ecf20Sopenharmony_ciMODULE_AUTHOR("Anrzej Zaborowski"); 3348c2ecf20Sopenharmony_ciMODULE_DESCRIPTION("LG-VL600 modem's ethernet link"); 3358c2ecf20Sopenharmony_ciMODULE_LICENSE("GPL"); 336