You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1061 lines
28 KiB
1061 lines
28 KiB
/* Copyright (c) 2013-2018, The Linux Foundation. All rights reserved.
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 2 and
|
|
* only version 2 as published by the Free Software Foundation.
|
|
*
|
|
* 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.
|
|
*
|
|
*
|
|
* RMNET Data virtual network driver
|
|
*/
|
|
|
|
#include <linux/types.h>
|
|
#include <linux/rmnet_data.h>
|
|
#include <linux/msm_rmnet.h>
|
|
#include <linux/etherdevice.h>
|
|
#include <linux/if_arp.h>
|
|
#include <linux/spinlock.h>
|
|
#include <net/pkt_sched.h>
|
|
#include <linux/atomic.h>
|
|
#include <linux/net_map.h>
|
|
#include "rmnet_data_config.h"
|
|
#include "rmnet_data_handlers.h"
|
|
#include "rmnet_data_private.h"
|
|
#include "rmnet_map.h"
|
|
#include "rmnet_data_vnd.h"
|
|
#include "rmnet_data_stats.h"
|
|
#include "rmnet_data_trace.h"
|
|
|
|
RMNET_LOG_MODULE(RMNET_DATA_LOGMASK_VND);
|
|
|
|
#define RMNET_MAP_FLOW_NUM_TC_HANDLE 3
|
|
#define RMNET_VND_UF_ACTION_ADD 0
|
|
#define RMNET_VND_UF_ACTION_DEL 1
|
|
enum {
|
|
RMNET_VND_UPDATE_FLOW_OK,
|
|
RMNET_VND_UPDATE_FLOW_NO_ACTION,
|
|
RMNET_VND_UPDATE_FLOW_NO_MORE_ROOM,
|
|
RMNET_VND_UPDATE_FLOW_NO_VALID_LEFT
|
|
};
|
|
|
|
struct net_device *rmnet_devices[RMNET_DATA_MAX_VND];
|
|
|
|
struct rmnet_map_flow_mapping_s {
|
|
struct list_head list;
|
|
u32 map_flow_id;
|
|
u32 tc_flow_valid[RMNET_MAP_FLOW_NUM_TC_HANDLE];
|
|
u32 tc_flow_id[RMNET_MAP_FLOW_NUM_TC_HANDLE];
|
|
atomic_t v4_seq;
|
|
atomic_t v6_seq;
|
|
};
|
|
|
|
struct rmnet_vnd_private_s {
|
|
u32 qos_version;
|
|
struct rmnet_logical_ep_conf_s local_ep;
|
|
|
|
rwlock_t flow_map_lock;
|
|
struct list_head flow_head;
|
|
struct rmnet_map_flow_mapping_s root_flow;
|
|
};
|
|
|
|
#define RMNET_VND_FC_QUEUED 0
|
|
#define RMNET_VND_FC_NOT_ENABLED 1
|
|
#define RMNET_VND_FC_KMALLOC_ERR 2
|
|
|
|
/* Helper Functions */
|
|
|
|
/* rmnet_vnd_add_qos_header() - Adds QoS header to front of skb->data
|
|
* @skb: Socket buffer ("packet") to modify
|
|
* @dev: Egress interface
|
|
*
|
|
* Does not check for sufficient headroom! Caller must make sure there is enough
|
|
* headroom.
|
|
*/
|
|
static void rmnet_vnd_add_qos_header(struct sk_buff *skb,
|
|
struct net_device *dev,
|
|
uint32_t qos_version)
|
|
{
|
|
struct QMI_QOS_HDR_S *qmih;
|
|
struct qmi_qos_hdr8_s *qmi8h;
|
|
|
|
if (qos_version & RMNET_IOCTL_QOS_MODE_6) {
|
|
qmih = (struct QMI_QOS_HDR_S *)
|
|
skb_push(skb, sizeof(struct QMI_QOS_HDR_S));
|
|
qmih->version = 1;
|
|
qmih->flags = 0;
|
|
qmih->flow_id = skb->mark;
|
|
} else if (qos_version & RMNET_IOCTL_QOS_MODE_8) {
|
|
qmi8h = (struct qmi_qos_hdr8_s *)
|
|
skb_push(skb, sizeof(struct qmi_qos_hdr8_s));
|
|
/* Flags are 0 always */
|
|
qmi8h->hdr.version = 0;
|
|
qmi8h->hdr.flags = 0;
|
|
memset(qmi8h->reserved, 0, sizeof(qmi8h->reserved));
|
|
qmi8h->hdr.flow_id = skb->mark;
|
|
} else {
|
|
LOGD("%s(): Bad QoS version configured\n", __func__);
|
|
}
|
|
}
|
|
|
|
/* Network Device Operations */
|
|
|
|
/* rmnet_vnd_start_xmit() - Transmit NDO callback
|
|
* @skb: Socket buffer ("packet") being sent from network stack
|
|
* @dev: Virtual Network Device
|
|
*
|
|
* Standard network driver operations hook to transmit packets on virtual
|
|
* network device. Called by network stack. Packet is not transmitted directly
|
|
* from here; instead it is given to the rmnet egress handler.
|
|
*
|
|
* Return:
|
|
* - NETDEV_TX_OK under all cirumstances (cannot block/fail)
|
|
*/
|
|
static netdev_tx_t rmnet_vnd_start_xmit(struct sk_buff *skb,
|
|
struct net_device *dev)
|
|
{
|
|
struct rmnet_vnd_private_s *dev_conf;
|
|
|
|
trace_rmnet_vnd_start_xmit(skb);
|
|
dev_conf = (struct rmnet_vnd_private_s *)netdev_priv(dev);
|
|
if (dev_conf->local_ep.egress_dev) {
|
|
/* QoS header should come after MAP header */
|
|
if (dev_conf->qos_version)
|
|
rmnet_vnd_add_qos_header(skb,
|
|
dev,
|
|
dev_conf->qos_version);
|
|
skb_orphan(skb);
|
|
rmnet_data_egress_handler(skb, &dev_conf->local_ep);
|
|
} else {
|
|
dev->stats.tx_dropped++;
|
|
rmnet_kfree_skb(skb, RMNET_STATS_SKBFREE_VND_NO_EGRESS);
|
|
}
|
|
return NETDEV_TX_OK;
|
|
}
|
|
|
|
/* rmnet_vnd_change_mtu() - Change MTU NDO callback
|
|
* @dev: Virtual network device
|
|
* @new_mtu: New MTU value to set (in bytes)
|
|
*
|
|
* Standard network driver operations hook to set the MTU. Called by kernel to
|
|
* set the device MTU. Checks if desired MTU is less than zero or greater than
|
|
* RMNET_DATA_MAX_PACKET_SIZE;
|
|
*
|
|
* Return:
|
|
* - 0 if successful
|
|
* - -EINVAL if new_mtu is out of range
|
|
*/
|
|
static int rmnet_vnd_change_mtu(struct net_device *dev, int new_mtu)
|
|
{
|
|
if (new_mtu < 0 || new_mtu > RMNET_DATA_MAX_PACKET_SIZE)
|
|
return -EINVAL;
|
|
|
|
dev->mtu = new_mtu;
|
|
return 0;
|
|
}
|
|
|
|
#ifdef CONFIG_RMNET_DATA_FC
|
|
static int _rmnet_vnd_do_qos_ioctl(struct net_device *dev,
|
|
struct ifreq *ifr,
|
|
int cmd)
|
|
{
|
|
struct rmnet_vnd_private_s *dev_conf;
|
|
int rc, qdisc_len = 0;
|
|
struct rmnet_ioctl_data_s ioctl_data;
|
|
|
|
rc = 0;
|
|
dev_conf = (struct rmnet_vnd_private_s *)netdev_priv(dev);
|
|
|
|
switch (cmd) {
|
|
case RMNET_IOCTL_SET_QOS_ENABLE:
|
|
if (!capable(CAP_NET_ADMIN))
|
|
return -EPERM;
|
|
LOGM("RMNET_IOCTL_SET_QOS_ENABLE on %s", dev->name);
|
|
if (!dev_conf->qos_version)
|
|
dev_conf->qos_version = RMNET_IOCTL_QOS_MODE_6;
|
|
break;
|
|
|
|
case RMNET_IOCTL_SET_QOS_DISABLE:
|
|
if (!capable(CAP_NET_ADMIN))
|
|
return -EPERM;
|
|
LOGM("RMNET_IOCTL_SET_QOS_DISABLE on %s", dev->name);
|
|
dev_conf->qos_version = 0;
|
|
break;
|
|
|
|
case RMNET_IOCTL_GET_QOS: /* Get QoS header state */
|
|
LOGM("RMNET_IOCTL_GET_QOS on %s", dev->name);
|
|
ioctl_data.u.operation_mode = (dev_conf->qos_version ==
|
|
RMNET_IOCTL_QOS_MODE_6);
|
|
if (copy_to_user(ifr->ifr_ifru.ifru_data, &ioctl_data,
|
|
sizeof(struct rmnet_ioctl_data_s)))
|
|
rc = -EFAULT;
|
|
break;
|
|
|
|
case RMNET_IOCTL_FLOW_ENABLE:
|
|
if (!capable(CAP_NET_ADMIN))
|
|
return -EPERM;
|
|
LOGL("RMNET_IOCTL_FLOW_ENABLE on %s", dev->name);
|
|
if (copy_from_user(&ioctl_data, ifr->ifr_ifru.ifru_data,
|
|
sizeof(struct rmnet_ioctl_data_s))) {
|
|
rc = -EFAULT;
|
|
break;
|
|
}
|
|
qdisc_len = tc_qdisc_flow_control(dev,
|
|
ioctl_data.u.tcm_handle, 1);
|
|
trace_rmnet_fc_qmi(ioctl_data.u.tcm_handle, qdisc_len, 1);
|
|
break;
|
|
|
|
case RMNET_IOCTL_FLOW_DISABLE:
|
|
if (!capable(CAP_NET_ADMIN))
|
|
return -EPERM;
|
|
LOGL("RMNET_IOCTL_FLOW_DISABLE on %s", dev->name);
|
|
if (copy_from_user(&ioctl_data, ifr->ifr_ifru.ifru_data,
|
|
sizeof(struct rmnet_ioctl_data_s))) {
|
|
rc = -EFAULT;
|
|
break;
|
|
}
|
|
qdisc_len = tc_qdisc_flow_control(dev,
|
|
ioctl_data.u.tcm_handle, 0);
|
|
trace_rmnet_fc_qmi(ioctl_data.u.tcm_handle, qdisc_len, 0);
|
|
break;
|
|
|
|
default:
|
|
rc = -EINVAL;
|
|
}
|
|
|
|
return rc;
|
|
}
|
|
|
|
struct rmnet_vnd_fc_work {
|
|
struct work_struct work;
|
|
struct net_device *dev;
|
|
u32 tc_handle;
|
|
int enable;
|
|
};
|
|
|
|
static void _rmnet_vnd_wq_flow_control(struct work_struct *work)
|
|
{
|
|
struct rmnet_vnd_fc_work *fcwork;
|
|
int qdisc_len = 0;
|
|
|
|
fcwork = (struct rmnet_vnd_fc_work *)work;
|
|
|
|
rtnl_lock();
|
|
qdisc_len = tc_qdisc_flow_control(fcwork->dev, fcwork->tc_handle,
|
|
fcwork->enable);
|
|
trace_rmnet_fc_map(fcwork->tc_handle, qdisc_len, fcwork->enable);
|
|
rtnl_unlock();
|
|
|
|
LOGL("[%s] handle:%08X enable:%d",
|
|
fcwork->dev->name, fcwork->tc_handle, fcwork->enable);
|
|
|
|
kfree(work);
|
|
}
|
|
|
|
static int _rmnet_vnd_do_flow_control(struct net_device *dev,
|
|
u32 tc_handle,
|
|
int enable)
|
|
{
|
|
struct rmnet_vnd_fc_work *fcwork;
|
|
|
|
fcwork = kmalloc(sizeof(*fcwork), GFP_ATOMIC);
|
|
if (!fcwork)
|
|
return RMNET_VND_FC_KMALLOC_ERR;
|
|
memset(fcwork, 0, sizeof(struct rmnet_vnd_fc_work));
|
|
|
|
INIT_WORK((struct work_struct *)fcwork, _rmnet_vnd_wq_flow_control);
|
|
fcwork->dev = dev;
|
|
fcwork->tc_handle = tc_handle;
|
|
fcwork->enable = enable;
|
|
|
|
schedule_work((struct work_struct *)fcwork);
|
|
return RMNET_VND_FC_QUEUED;
|
|
}
|
|
#else
|
|
static int _rmnet_vnd_do_qos_ioctl(struct net_device *dev,
|
|
struct ifreq *ifr,
|
|
int cmd)
|
|
{
|
|
return -EINVAL;
|
|
}
|
|
|
|
static inline int _rmnet_vnd_do_flow_control(struct net_device *dev,
|
|
u32 tc_handle,
|
|
int enable)
|
|
{
|
|
LOGD("[%s] called with no QoS support", dev->name);
|
|
return RMNET_VND_FC_NOT_ENABLED;
|
|
}
|
|
#endif /* CONFIG_RMNET_DATA_FC */
|
|
|
|
static int rmnet_vnd_ioctl_extended(struct net_device *dev, struct ifreq *ifr)
|
|
{
|
|
struct rmnet_vnd_private_s *dev_conf;
|
|
struct rmnet_ioctl_extended_s ext_cmd;
|
|
int rc = 0;
|
|
|
|
dev_conf = (struct rmnet_vnd_private_s *)netdev_priv(dev);
|
|
|
|
rc = copy_from_user(&ext_cmd, ifr->ifr_ifru.ifru_data,
|
|
sizeof(struct rmnet_ioctl_extended_s));
|
|
if (rc) {
|
|
LOGM("%s(): copy_from_user() failed\n", __func__);
|
|
return rc;
|
|
}
|
|
|
|
switch (ext_cmd.extended_ioctl) {
|
|
case RMNET_IOCTL_GET_SUPPORTED_FEATURES:
|
|
ext_cmd.u.data = 0;
|
|
break;
|
|
|
|
case RMNET_IOCTL_GET_DRIVER_NAME:
|
|
strlcpy(ext_cmd.u.if_name, "rmnet_data",
|
|
sizeof(ext_cmd.u.if_name));
|
|
break;
|
|
|
|
case RMNET_IOCTL_GET_SUPPORTED_QOS_MODES:
|
|
ext_cmd.u.data = RMNET_IOCTL_QOS_MODE_6
|
|
| RMNET_IOCTL_QOS_MODE_8;
|
|
break;
|
|
|
|
case RMNET_IOCTL_GET_QOS_VERSION:
|
|
ext_cmd.u.data = dev_conf->qos_version;
|
|
break;
|
|
|
|
case RMNET_IOCTL_SET_QOS_VERSION:
|
|
if (!capable(CAP_NET_ADMIN))
|
|
return -EPERM;
|
|
if (ext_cmd.u.data == RMNET_IOCTL_QOS_MODE_6 ||
|
|
ext_cmd.u.data == RMNET_IOCTL_QOS_MODE_8 ||
|
|
ext_cmd.u.data == 0) {
|
|
dev_conf->qos_version = ext_cmd.u.data;
|
|
} else {
|
|
rc = -EINVAL;
|
|
goto done;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
rc = -EINVAL;
|
|
goto done;
|
|
}
|
|
|
|
rc = copy_to_user(ifr->ifr_ifru.ifru_data, &ext_cmd,
|
|
sizeof(struct rmnet_ioctl_extended_s));
|
|
if (rc)
|
|
LOGM("%s(): copy_to_user() failed\n", __func__);
|
|
|
|
done:
|
|
return rc;
|
|
}
|
|
|
|
/* rmnet_vnd_ioctl() - IOCTL NDO callback
|
|
* @dev: Virtual network device
|
|
* @ifreq: User data
|
|
* @cmd: IOCTL command value
|
|
*
|
|
* Standard network driver operations hook to process IOCTLs. Called by kernel
|
|
* to process non-stanard IOCTLs for device
|
|
*
|
|
* Return:
|
|
* - 0 if successful
|
|
* - -EINVAL if unknown IOCTL
|
|
*/
|
|
static int rmnet_vnd_ioctl(struct net_device *dev, struct ifreq *ifr, int cmd)
|
|
{
|
|
struct rmnet_vnd_private_s *dev_conf;
|
|
int rc;
|
|
struct rmnet_ioctl_data_s ioctl_data;
|
|
|
|
rc = 0;
|
|
dev_conf = (struct rmnet_vnd_private_s *)netdev_priv(dev);
|
|
|
|
rc = _rmnet_vnd_do_qos_ioctl(dev, ifr, cmd);
|
|
if (rc != -EINVAL)
|
|
return rc;
|
|
rc = 0; /* Reset rc as it may contain -EINVAL from above */
|
|
|
|
switch (cmd) {
|
|
case RMNET_IOCTL_OPEN: /* Do nothing. Support legacy behavior */
|
|
LOGM("RMNET_IOCTL_OPEN on %s (ignored)", dev->name);
|
|
break;
|
|
|
|
case RMNET_IOCTL_CLOSE: /* Do nothing. Support legacy behavior */
|
|
LOGM("RMNET_IOCTL_CLOSE on %s (ignored)", dev->name);
|
|
break;
|
|
|
|
case RMNET_IOCTL_SET_LLP_ETHERNET:
|
|
LOGM("RMNET_IOCTL_SET_LLP_ETHERNET on %s (no support)",
|
|
dev->name);
|
|
rc = -EINVAL;
|
|
break;
|
|
|
|
case RMNET_IOCTL_SET_LLP_IP: /* Do nothing. Support legacy behavior */
|
|
LOGM("RMNET_IOCTL_SET_LLP_IP on %s (ignored)", dev->name);
|
|
break;
|
|
|
|
case RMNET_IOCTL_GET_LLP: /* Always return IP mode */
|
|
LOGM("RMNET_IOCTL_GET_LLP on %s", dev->name);
|
|
ioctl_data.u.operation_mode = RMNET_MODE_LLP_IP;
|
|
if (copy_to_user(ifr->ifr_ifru.ifru_data, &ioctl_data,
|
|
sizeof(struct rmnet_ioctl_data_s)))
|
|
rc = -EFAULT;
|
|
break;
|
|
|
|
case RMNET_IOCTL_EXTENDED:
|
|
rc = rmnet_vnd_ioctl_extended(dev, ifr);
|
|
break;
|
|
|
|
default:
|
|
LOGM("Unknown IOCTL 0x%08X", cmd);
|
|
rc = -EINVAL;
|
|
}
|
|
|
|
return rc;
|
|
}
|
|
|
|
static const struct net_device_ops rmnet_data_vnd_ops = {
|
|
.ndo_init = 0,
|
|
.ndo_start_xmit = rmnet_vnd_start_xmit,
|
|
.ndo_do_ioctl = rmnet_vnd_ioctl,
|
|
.ndo_change_mtu = rmnet_vnd_change_mtu,
|
|
.ndo_set_mac_address = 0,
|
|
.ndo_validate_addr = 0,
|
|
};
|
|
|
|
/* rmnet_vnd_setup() - net_device initialization callback
|
|
* @dev: Virtual network device
|
|
*
|
|
* Called by kernel whenever a new rmnet_data<n> device is created. Sets MTU,
|
|
* flags, ARP type, needed headroom, etc...
|
|
*/
|
|
static void rmnet_vnd_setup(struct net_device *dev)
|
|
{
|
|
struct rmnet_vnd_private_s *dev_conf;
|
|
|
|
LOGM("Setting up device %s", dev->name);
|
|
|
|
/* Clear out private data */
|
|
dev_conf = (struct rmnet_vnd_private_s *)netdev_priv(dev);
|
|
memset(dev_conf, 0, sizeof(struct rmnet_vnd_private_s));
|
|
|
|
dev->netdev_ops = &rmnet_data_vnd_ops;
|
|
dev->mtu = RMNET_DATA_DFLT_PACKET_SIZE;
|
|
dev->needed_headroom = RMNET_DATA_NEEDED_HEADROOM;
|
|
random_ether_addr(dev->dev_addr);
|
|
dev->tx_queue_len = RMNET_DATA_TX_QUEUE_LEN;
|
|
|
|
/* Raw IP mode */
|
|
dev->header_ops = 0; /* No header */
|
|
dev->type = ARPHRD_RAWIP;
|
|
dev->hard_header_len = 0;
|
|
dev->flags &= ~(IFF_BROADCAST | IFF_MULTICAST);
|
|
|
|
/* Flow control */
|
|
rwlock_init(&dev_conf->flow_map_lock);
|
|
INIT_LIST_HEAD(&dev_conf->flow_head);
|
|
}
|
|
|
|
/* rmnet_vnd_setup() - net_device initialization helper function
|
|
* @dev: Virtual network device
|
|
*
|
|
* Called during device initialization. Disables GRO.
|
|
*/
|
|
static void rmnet_vnd_disable_offload(struct net_device *dev)
|
|
{
|
|
dev->wanted_features &= ~NETIF_F_GRO;
|
|
__netdev_update_features(dev);
|
|
}
|
|
|
|
/* Exposed API */
|
|
|
|
/* rmnet_vnd_exit() - Shutdown cleanup hook
|
|
*
|
|
* Called by RmNet main on module unload. Cleans up data structures and
|
|
* unregisters/frees net_devices.
|
|
*/
|
|
void rmnet_vnd_exit(void)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < RMNET_DATA_MAX_VND; i++)
|
|
if (rmnet_devices[i]) {
|
|
unregister_netdev(rmnet_devices[i]);
|
|
free_netdev(rmnet_devices[i]);
|
|
}
|
|
}
|
|
|
|
/* rmnet_vnd_init() - Init hook
|
|
*
|
|
* Called by RmNet main on module load. Initializes data structures
|
|
*/
|
|
int rmnet_vnd_init(void)
|
|
{
|
|
memset(rmnet_devices, 0,
|
|
sizeof(struct net_device *) * RMNET_DATA_MAX_VND);
|
|
return 0;
|
|
}
|
|
|
|
/* rmnet_vnd_create_dev() - Create a new virtual network device node.
|
|
* @id: Virtual device node id
|
|
* @new_device: Pointer to newly created device node
|
|
* @prefix: Device name prefix
|
|
*
|
|
* Allocates structures for new virtual network devices. Sets the name of the
|
|
* new device and registers it with the network stack. Device will appear in
|
|
* ifconfig list after this is called. If the prefix is null, then
|
|
* RMNET_DATA_DEV_NAME_STR will be assumed.
|
|
*
|
|
* Return:
|
|
* - 0 if successful
|
|
* - RMNET_CONFIG_BAD_ARGUMENTS if id is out of range or prefix is too long
|
|
* - RMNET_CONFIG_DEVICE_IN_USE if id already in use
|
|
* - RMNET_CONFIG_NOMEM if net_device allocation failed
|
|
* - RMNET_CONFIG_UNKNOWN_ERROR if register_netdevice() fails
|
|
*/
|
|
int rmnet_vnd_create_dev(int id, struct net_device **new_device,
|
|
const char *prefix, int use_name)
|
|
{
|
|
struct net_device *dev;
|
|
char dev_prefix[IFNAMSIZ];
|
|
int p, rc = 0;
|
|
|
|
if (id < 0 || id >= RMNET_DATA_MAX_VND) {
|
|
*new_device = 0;
|
|
return RMNET_CONFIG_BAD_ARGUMENTS;
|
|
}
|
|
|
|
if (rmnet_devices[id] != 0) {
|
|
*new_device = 0;
|
|
return RMNET_CONFIG_DEVICE_IN_USE;
|
|
}
|
|
|
|
if (!prefix && !use_name)
|
|
p = scnprintf(dev_prefix, IFNAMSIZ, "%s%%d",
|
|
RMNET_DATA_DEV_NAME_STR);
|
|
else if (prefix && use_name)
|
|
p = scnprintf(dev_prefix, IFNAMSIZ, "%s", prefix);
|
|
else if (prefix && !use_name)
|
|
p = scnprintf(dev_prefix, IFNAMSIZ, "%s%%d", prefix);
|
|
else
|
|
return RMNET_CONFIG_BAD_ARGUMENTS;
|
|
|
|
if (p >= (IFNAMSIZ - 1)) {
|
|
LOGE("Specified prefix longer than IFNAMSIZ");
|
|
return RMNET_CONFIG_BAD_ARGUMENTS;
|
|
}
|
|
|
|
dev = alloc_netdev(sizeof(struct rmnet_vnd_private_s),
|
|
dev_prefix,
|
|
use_name ? NET_NAME_UNKNOWN : NET_NAME_ENUM,
|
|
rmnet_vnd_setup);
|
|
if (!dev) {
|
|
LOGE("Failed to to allocate netdev for id %d", id);
|
|
*new_device = 0;
|
|
return RMNET_CONFIG_NOMEM;
|
|
}
|
|
|
|
if (!prefix) {
|
|
/* Configuring DL checksum offload on rmnet_data interfaces */
|
|
dev->hw_features = NETIF_F_RXCSUM;
|
|
/* Configuring UL checksum offload on rmnet_data interfaces */
|
|
dev->hw_features |= NETIF_F_IP_CSUM | NETIF_F_IPV6_CSUM;
|
|
/* Configuring GRO on rmnet_data interfaces */
|
|
dev->hw_features |= NETIF_F_GRO;
|
|
/* Configuring Scatter-Gather on rmnet_data interfaces */
|
|
dev->hw_features |= NETIF_F_SG;
|
|
/* Configuring GSO on rmnet_data interfaces */
|
|
dev->hw_features |= NETIF_F_GSO;
|
|
dev->hw_features |= NETIF_F_GSO_UDP_TUNNEL;
|
|
dev->hw_features |= NETIF_F_GSO_UDP_TUNNEL_CSUM;
|
|
}
|
|
|
|
rc = register_netdevice(dev);
|
|
if (rc != 0) {
|
|
LOGE("Failed to to register netdev [%s]", dev->name);
|
|
free_netdev(dev);
|
|
*new_device = 0;
|
|
rc = RMNET_CONFIG_UNKNOWN_ERROR;
|
|
} else {
|
|
rmnet_devices[id] = dev;
|
|
*new_device = dev;
|
|
LOGM("Registered device %s", dev->name);
|
|
}
|
|
|
|
rmnet_vnd_disable_offload(dev);
|
|
|
|
return rc;
|
|
}
|
|
|
|
/* rmnet_vnd_free_dev() - free a virtual network device node.
|
|
* @id: Virtual device node id
|
|
*
|
|
* Unregisters the virtual network device node and frees it.
|
|
* unregister_netdev locks the rtnl mutex, so the mutex must not be locked
|
|
* by the caller of the function. unregister_netdev enqueues the request to
|
|
* unregister the device into a TODO queue. The requests in the TODO queue
|
|
* are only done after rtnl mutex is unlocked, therefore free_netdev has to
|
|
* called after unlocking rtnl mutex.
|
|
*
|
|
* Return:
|
|
* - 0 if successful
|
|
* - RMNET_CONFIG_NO_SUCH_DEVICE if id is invalid or not in range
|
|
* - RMNET_CONFIG_DEVICE_IN_USE if device has logical ep that wasn't unset
|
|
*/
|
|
int rmnet_vnd_free_dev(int id)
|
|
{
|
|
struct rmnet_logical_ep_conf_s *epconfig_l;
|
|
struct net_device *dev;
|
|
|
|
rtnl_lock();
|
|
if ((id < 0) || (id >= RMNET_DATA_MAX_VND) || !rmnet_devices[id]) {
|
|
rtnl_unlock();
|
|
LOGM("Invalid id [%d]", id);
|
|
return RMNET_CONFIG_NO_SUCH_DEVICE;
|
|
}
|
|
|
|
epconfig_l = rmnet_vnd_get_le_config(rmnet_devices[id]);
|
|
if (epconfig_l && epconfig_l->refcount) {
|
|
rtnl_unlock();
|
|
return RMNET_CONFIG_DEVICE_IN_USE;
|
|
}
|
|
|
|
dev = rmnet_devices[id];
|
|
rmnet_devices[id] = 0;
|
|
rtnl_unlock();
|
|
|
|
if (dev) {
|
|
unregister_netdev(dev);
|
|
free_netdev(dev);
|
|
return 0;
|
|
} else {
|
|
return RMNET_CONFIG_NO_SUCH_DEVICE;
|
|
}
|
|
}
|
|
|
|
/* rmnet_vnd_get_name() - Gets the string name of a VND based on ID
|
|
* @id: Virtual device node id
|
|
* @name: Buffer to store name of virtual device node
|
|
* @name_len: Length of name buffer
|
|
*
|
|
* Copies the name of the virtual device node into the users buffer. Will throw
|
|
* an error if the buffer is null, or too small to hold the device name.
|
|
*
|
|
* Return:
|
|
* - 0 if successful
|
|
* - -EINVAL if name is null
|
|
* - -EINVAL if id is invalid or not in range
|
|
* - -EINVAL if name is too small to hold things
|
|
*/
|
|
int rmnet_vnd_get_name(int id, char *name, int name_len)
|
|
{
|
|
int p;
|
|
|
|
if (!name) {
|
|
LOGM("%s", "Bad arguments; name buffer null");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if ((id < 0) || (id >= RMNET_DATA_MAX_VND) || !rmnet_devices[id]) {
|
|
LOGM("Invalid id [%d]", id);
|
|
return -EINVAL;
|
|
}
|
|
|
|
p = strlcpy(name, rmnet_devices[id]->name, name_len);
|
|
if (p >= name_len) {
|
|
LOGM("Buffer to small (%d) to fit device name", name_len);
|
|
return -EINVAL;
|
|
}
|
|
LOGL("Found mapping [%d]->\"%s\"", id, name);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* rmnet_vnd_is_vnd() - Determine if net_device is RmNet owned virtual devices
|
|
* @dev: Network device to test
|
|
*
|
|
* Searches through list of known RmNet virtual devices. This function is O(n)
|
|
* and should not be used in the data path.
|
|
*
|
|
* Return:
|
|
* - 0 if device is not RmNet virtual device
|
|
* - 1 if device is RmNet virtual device
|
|
*/
|
|
int rmnet_vnd_is_vnd(struct net_device *dev)
|
|
{
|
|
/* This is not an efficient search, but, this will only be called in
|
|
* a configuration context, and the list is small.
|
|
*/
|
|
int i;
|
|
|
|
if (!dev)
|
|
return 0;
|
|
|
|
for (i = 0; i < RMNET_DATA_MAX_VND; i++)
|
|
if (dev == rmnet_devices[i])
|
|
return i + 1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* rmnet_vnd_get_le_config() - Get the logical endpoint configuration
|
|
* @dev: Virtual device node
|
|
*
|
|
* Gets the logical endpoint configuration for a RmNet virtual network device
|
|
* node. Caller should confirm that devices is a RmNet VND before calling.
|
|
*
|
|
* Return:
|
|
* - Pointer to logical endpoint configuration structure
|
|
* - 0 (null) if dev is null
|
|
*/
|
|
struct rmnet_logical_ep_conf_s *rmnet_vnd_get_le_config(struct net_device *dev)
|
|
{
|
|
struct rmnet_vnd_private_s *dev_conf;
|
|
|
|
if (!dev)
|
|
return 0;
|
|
|
|
dev_conf = (struct rmnet_vnd_private_s *)netdev_priv(dev);
|
|
if (!dev_conf)
|
|
return 0;
|
|
|
|
return &dev_conf->local_ep;
|
|
}
|
|
|
|
/* _rmnet_vnd_get_flow_map() - Gets object representing a MAP flow handle
|
|
* @dev_conf: Private configuration structure for virtual network device
|
|
* @map_flow: MAP flow handle IF
|
|
*
|
|
* Loops through available flow mappings and compares the MAP flow handle.
|
|
* Returns when mapping is found.
|
|
*
|
|
* Return:
|
|
* - Null if no mapping was found
|
|
* - Pointer to mapping otherwise
|
|
*/
|
|
static struct rmnet_map_flow_mapping_s *_rmnet_vnd_get_flow_map
|
|
(struct rmnet_vnd_private_s *dev_conf,
|
|
u32 map_flow)
|
|
{
|
|
struct list_head *p;
|
|
struct rmnet_map_flow_mapping_s *itm;
|
|
|
|
list_for_each(p, &dev_conf->flow_head) {
|
|
itm = list_entry(p, struct rmnet_map_flow_mapping_s, list);
|
|
|
|
if (unlikely(!itm))
|
|
return 0;
|
|
|
|
if (itm->map_flow_id == map_flow)
|
|
return itm;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* _rmnet_vnd_update_flow_map() - Add or remove individual TC flow handles
|
|
* @action: One of RMNET_VND_UF_ACTION_ADD / RMNET_VND_UF_ACTION_DEL
|
|
* @itm: Flow mapping object
|
|
* @map_flow: TC flow handle
|
|
*
|
|
* RMNET_VND_UF_ACTION_ADD:
|
|
* Will check for a free mapping slot in the mapping object. If one is found,
|
|
* valid for that slot will be set to 1 and the value will be set.
|
|
*
|
|
* RMNET_VND_UF_ACTION_DEL:
|
|
* Will check for matching tc handle. If found, valid for that slot will be
|
|
* set to 0 and the value will also be zeroed.
|
|
*
|
|
* Return:
|
|
* - RMNET_VND_UPDATE_FLOW_OK tc flow handle is added/removed ok
|
|
* - RMNET_VND_UPDATE_FLOW_NO_MORE_ROOM if there are no more tc handles
|
|
* - RMNET_VND_UPDATE_FLOW_NO_VALID_LEFT if flow mapping is now empty
|
|
* - RMNET_VND_UPDATE_FLOW_NO_ACTION if no action was taken
|
|
*/
|
|
static int _rmnet_vnd_update_flow_map(u8 action,
|
|
struct rmnet_map_flow_mapping_s *itm,
|
|
u32 tc_flow)
|
|
{
|
|
int rc, i, j;
|
|
|
|
rc = RMNET_VND_UPDATE_FLOW_OK;
|
|
|
|
switch (action) {
|
|
case RMNET_VND_UF_ACTION_ADD:
|
|
rc = RMNET_VND_UPDATE_FLOW_NO_MORE_ROOM;
|
|
for (i = 0; i < RMNET_MAP_FLOW_NUM_TC_HANDLE; i++) {
|
|
if (itm->tc_flow_valid[i] == 0) {
|
|
itm->tc_flow_valid[i] = 1;
|
|
itm->tc_flow_id[i] = tc_flow;
|
|
rc = RMNET_VND_UPDATE_FLOW_OK;
|
|
LOGD("{%pK}->tc_flow_id[%d]=%08X",
|
|
itm, i, tc_flow);
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case RMNET_VND_UF_ACTION_DEL:
|
|
j = 0;
|
|
rc = RMNET_VND_UPDATE_FLOW_OK;
|
|
for (i = 0; i < RMNET_MAP_FLOW_NUM_TC_HANDLE; i++) {
|
|
if (itm->tc_flow_valid[i] == 1) {
|
|
if (itm->tc_flow_id[i] == tc_flow) {
|
|
itm->tc_flow_valid[i] = 0;
|
|
itm->tc_flow_id[i] = 0;
|
|
j++;
|
|
LOGD("{%pK}->tc_flow_id[%d]=0", itm, i);
|
|
}
|
|
} else {
|
|
j++;
|
|
}
|
|
}
|
|
if (j == RMNET_MAP_FLOW_NUM_TC_HANDLE)
|
|
rc = RMNET_VND_UPDATE_FLOW_NO_VALID_LEFT;
|
|
break;
|
|
|
|
default:
|
|
rc = RMNET_VND_UPDATE_FLOW_NO_ACTION;
|
|
break;
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
/* rmnet_vnd_add_tc_flow() - Add a MAP/TC flow handle mapping
|
|
* @id: Virtual network device ID
|
|
* @map_flow: MAP flow handle
|
|
* @tc_flow: TC flow handle
|
|
*
|
|
* Checkes for an existing flow mapping object corresponding to map_flow. If one
|
|
* is found, then it will try to add to the existing mapping object. Otherwise,
|
|
* a new mapping object is created.
|
|
*
|
|
* Return:
|
|
* - RMNET_CONFIG_OK if successful
|
|
* - RMNET_CONFIG_TC_HANDLE_FULL if there is no more room in the map object
|
|
* - RMNET_CONFIG_NOMEM failed to allocate a new map object
|
|
*/
|
|
int rmnet_vnd_add_tc_flow(u32 id, u32 map_flow, u32 tc_flow)
|
|
{
|
|
struct rmnet_map_flow_mapping_s *itm;
|
|
struct net_device *dev;
|
|
struct rmnet_vnd_private_s *dev_conf;
|
|
int r;
|
|
unsigned long flags;
|
|
|
|
if ((id < 0) || (id >= RMNET_DATA_MAX_VND) || !rmnet_devices[id]) {
|
|
LOGM("Invalid VND id [%d]", id);
|
|
return RMNET_CONFIG_NO_SUCH_DEVICE;
|
|
}
|
|
|
|
dev = rmnet_devices[id];
|
|
dev_conf = (struct rmnet_vnd_private_s *)netdev_priv(dev);
|
|
|
|
if (!dev_conf)
|
|
return RMNET_CONFIG_NO_SUCH_DEVICE;
|
|
|
|
write_lock_irqsave(&dev_conf->flow_map_lock, flags);
|
|
itm = _rmnet_vnd_get_flow_map(dev_conf, map_flow);
|
|
if (itm) {
|
|
r = _rmnet_vnd_update_flow_map(RMNET_VND_UF_ACTION_ADD,
|
|
itm, tc_flow);
|
|
if (r != RMNET_VND_UPDATE_FLOW_OK) {
|
|
write_unlock_irqrestore(&dev_conf->flow_map_lock,
|
|
flags);
|
|
return RMNET_CONFIG_TC_HANDLE_FULL;
|
|
}
|
|
write_unlock_irqrestore(&dev_conf->flow_map_lock, flags);
|
|
return RMNET_CONFIG_OK;
|
|
}
|
|
write_unlock_irqrestore(&dev_conf->flow_map_lock, flags);
|
|
|
|
itm = kmalloc(sizeof(*itm), GFP_KERNEL);
|
|
|
|
if (!itm) {
|
|
LOGM("%s", "Failure allocating flow mapping");
|
|
return RMNET_CONFIG_NOMEM;
|
|
}
|
|
memset(itm, 0, sizeof(struct rmnet_map_flow_mapping_s));
|
|
|
|
itm->map_flow_id = map_flow;
|
|
itm->tc_flow_valid[0] = 1;
|
|
itm->tc_flow_id[0] = tc_flow;
|
|
|
|
/* How can we dynamically init these safely? Kernel only provides static
|
|
* initializers for atomic_t
|
|
*/
|
|
itm->v4_seq.counter = 0; /* Init is broken: ATOMIC_INIT(0); */
|
|
itm->v6_seq.counter = 0; /* Init is broken: ATOMIC_INIT(0); */
|
|
|
|
write_lock_irqsave(&dev_conf->flow_map_lock, flags);
|
|
list_add(&itm->list, &dev_conf->flow_head);
|
|
write_unlock_irqrestore(&dev_conf->flow_map_lock, flags);
|
|
|
|
LOGD("Created flow mapping [%s][0x%08X][0x%08X]@%pK",
|
|
dev->name, itm->map_flow_id, itm->tc_flow_id[0], itm);
|
|
|
|
return RMNET_CONFIG_OK;
|
|
}
|
|
|
|
/* rmnet_vnd_del_tc_flow() - Delete a MAP/TC flow handle mapping
|
|
* @id: Virtual network device ID
|
|
* @map_flow: MAP flow handle
|
|
* @tc_flow: TC flow handle
|
|
*
|
|
* Checkes for an existing flow mapping object corresponding to map_flow. If one
|
|
* is found, then it will try to remove the existing tc_flow mapping. If the
|
|
* mapping object no longer contains any mappings, then it is freed. Otherwise
|
|
* the mapping object is left in the list
|
|
*
|
|
* Return:
|
|
* - RMNET_CONFIG_OK if successful or if there was no such tc_flow
|
|
* - RMNET_CONFIG_INVALID_REQUEST if there is no such map_flow
|
|
*/
|
|
int rmnet_vnd_del_tc_flow(u32 id, u32 map_flow, u32 tc_flow)
|
|
{
|
|
struct rmnet_vnd_private_s *dev_conf;
|
|
struct net_device *dev;
|
|
struct rmnet_map_flow_mapping_s *itm;
|
|
int r;
|
|
unsigned long flags;
|
|
int rc = RMNET_CONFIG_OK;
|
|
|
|
if ((id < 0) || (id >= RMNET_DATA_MAX_VND) || !rmnet_devices[id]) {
|
|
LOGM("Invalid VND id [%d]", id);
|
|
return RMNET_CONFIG_NO_SUCH_DEVICE;
|
|
}
|
|
|
|
dev = rmnet_devices[id];
|
|
dev_conf = (struct rmnet_vnd_private_s *)netdev_priv(dev);
|
|
|
|
if (!dev_conf)
|
|
return RMNET_CONFIG_NO_SUCH_DEVICE;
|
|
|
|
r = RMNET_VND_UPDATE_FLOW_NO_ACTION;
|
|
write_lock_irqsave(&dev_conf->flow_map_lock, flags);
|
|
itm = _rmnet_vnd_get_flow_map(dev_conf, map_flow);
|
|
if (!itm) {
|
|
rc = RMNET_CONFIG_INVALID_REQUEST;
|
|
} else {
|
|
r = _rmnet_vnd_update_flow_map(RMNET_VND_UF_ACTION_DEL,
|
|
itm, tc_flow);
|
|
if (r == RMNET_VND_UPDATE_FLOW_NO_VALID_LEFT)
|
|
list_del(&itm->list);
|
|
}
|
|
write_unlock_irqrestore(&dev_conf->flow_map_lock, flags);
|
|
|
|
if (r == RMNET_VND_UPDATE_FLOW_NO_VALID_LEFT) {
|
|
if (itm)
|
|
LOGD("Removed flow mapping [%s][0x%08X]@%pK",
|
|
dev->name, itm->map_flow_id, itm);
|
|
kfree(itm);
|
|
}
|
|
|
|
return rc;
|
|
}
|
|
|
|
/* rmnet_data_vnd_do_flow_control() - Process flow control request
|
|
* @dev: Virtual network device node to do lookup on
|
|
* @map_flow_id: Flow ID from MAP message
|
|
* @v4_seq: pointer to IPv4 indication sequence number
|
|
* @v6_seq: pointer to IPv6 indication sequence number
|
|
* @enable: boolean to enable/disable flow.
|
|
*
|
|
* Return:
|
|
* - 0 if successful
|
|
* - 1 if no mapping is found
|
|
* - 2 if dev is not RmNet virtual network device node
|
|
*/
|
|
int rmnet_data_vnd_do_flow_control(struct net_device *dev,
|
|
u32 map_flow_id,
|
|
u16 v4_seq,
|
|
u16 v6_seq,
|
|
int enable)
|
|
{
|
|
struct rmnet_vnd_private_s *dev_conf;
|
|
struct rmnet_map_flow_mapping_s *itm;
|
|
int do_fc, error, i;
|
|
|
|
error = 0;
|
|
do_fc = 0;
|
|
|
|
if (unlikely(!dev))
|
|
return 2;
|
|
|
|
if (!rmnet_vnd_is_vnd(dev))
|
|
return 2;
|
|
|
|
dev_conf = (struct rmnet_vnd_private_s *)netdev_priv(dev);
|
|
|
|
if (unlikely(!dev_conf))
|
|
return 2;
|
|
|
|
read_lock(&dev_conf->flow_map_lock);
|
|
if (map_flow_id == 0xFFFFFFFF) {
|
|
itm = &dev_conf->root_flow;
|
|
goto nolookup;
|
|
}
|
|
|
|
itm = _rmnet_vnd_get_flow_map(dev_conf, map_flow_id);
|
|
|
|
if (!itm) {
|
|
LOGL("Got flow control request for unknown flow %08X",
|
|
map_flow_id);
|
|
goto fcdone;
|
|
}
|
|
|
|
nolookup:
|
|
if (v4_seq == 0 || v4_seq >= atomic_read(&itm->v4_seq)) {
|
|
atomic_set(&itm->v4_seq, v4_seq);
|
|
if (map_flow_id == 0xFFFFFFFF) {
|
|
LOGD("Setting VND TX queue state to %d", enable);
|
|
/* Although we expect similar number of enable/disable
|
|
* commands, optimize for the disable. That is more
|
|
* latency sensitive than enable
|
|
*/
|
|
if (unlikely(enable))
|
|
netif_wake_queue(dev);
|
|
else
|
|
netif_stop_queue(dev);
|
|
trace_rmnet_fc_map(0xFFFFFFFF, 0, enable);
|
|
goto fcdone;
|
|
}
|
|
for (i = 0; i < RMNET_MAP_FLOW_NUM_TC_HANDLE; i++) {
|
|
if (itm->tc_flow_valid[i] == 1) {
|
|
LOGD("Found [%s][0x%08X][%d:0x%08X]",
|
|
dev->name, itm->map_flow_id, i,
|
|
itm->tc_flow_id[i]);
|
|
|
|
_rmnet_vnd_do_flow_control(dev,
|
|
itm->tc_flow_id[i],
|
|
enable);
|
|
}
|
|
}
|
|
} else {
|
|
LOGD("Internal seq(%hd) higher than called(%hd)",
|
|
atomic_read(&itm->v4_seq), v4_seq);
|
|
}
|
|
|
|
fcdone:
|
|
read_unlock(&dev_conf->flow_map_lock);
|
|
|
|
return error;
|
|
}
|
|
|
|
/* rmnet_vnd_get_by_id() - Get VND by array index ID
|
|
* @id: Virtual network deice id [0:RMNET_DATA_MAX_VND]
|
|
*
|
|
* Return:
|
|
* - 0 if no device or ID out of range
|
|
* - otherwise return pointer to VND net_device struct
|
|
*/
|
|
struct net_device *rmnet_vnd_get_by_id(int id)
|
|
{
|
|
if (id < 0 || id >= RMNET_DATA_MAX_VND) {
|
|
LOGE("Bug; VND ID out of bounds");
|
|
return 0;
|
|
}
|
|
return rmnet_devices[id];
|
|
}
|
|
|