763 lines
23 KiB
C
763 lines
23 KiB
C
/*
|
|
* Copyright (c) 2013-2015, 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 MAP protocol
|
|
*
|
|
*/
|
|
|
|
#include <linux/module.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/skbuff.h>
|
|
#include <linux/netdevice.h>
|
|
#include <linux/rmnet_data.h>
|
|
#include <linux/spinlock.h>
|
|
#include <linux/workqueue.h>
|
|
#include <linux/time.h>
|
|
#include <linux/net_map.h>
|
|
#include <linux/ip.h>
|
|
#include <linux/ipv6.h>
|
|
#include <linux/udp.h>
|
|
#include <linux/tcp.h>
|
|
#include <linux/in.h>
|
|
#include <net/ip.h>
|
|
#include <net/checksum.h>
|
|
#include <net/ip6_checksum.h>
|
|
#include "rmnet_data_config.h"
|
|
#include "rmnet_map.h"
|
|
#include "rmnet_data_private.h"
|
|
#include "rmnet_data_stats.h"
|
|
#include "rmnet_data_trace.h"
|
|
|
|
RMNET_LOG_MODULE(RMNET_DATA_LOGMASK_MAPD);
|
|
|
|
/* ***************** Local Definitions ************************************** */
|
|
|
|
long agg_time_limit __read_mostly = 1000000L;
|
|
module_param(agg_time_limit, long, S_IRUGO | S_IWUSR);
|
|
MODULE_PARM_DESC(agg_time_limit, "Maximum time packets sit in the agg buf");
|
|
|
|
long agg_bypass_time __read_mostly = 10000000L;
|
|
module_param(agg_bypass_time, long, S_IRUGO | S_IWUSR);
|
|
MODULE_PARM_DESC(agg_bypass_time, "Skip agg when apart spaced more than this");
|
|
|
|
|
|
struct agg_work {
|
|
struct delayed_work work;
|
|
struct rmnet_phys_ep_conf_s *config;
|
|
};
|
|
|
|
#define RMNET_MAP_DEAGGR_SPACING 64
|
|
#define RMNET_MAP_DEAGGR_HEADROOM (RMNET_MAP_DEAGGR_SPACING/2)
|
|
/******************************************************************************/
|
|
|
|
/**
|
|
* rmnet_map_add_map_header() - Adds MAP header to front of skb->data
|
|
* @skb: Socket buffer ("packet") to modify
|
|
* @hdrlen: Number of bytes of header data which should not be included in
|
|
* MAP length field
|
|
* @pad: Specify if padding the MAP packet to make it 4 byte aligned is
|
|
* necessary
|
|
*
|
|
* Padding is calculated and set appropriately in MAP header. Mux ID is
|
|
* initialized to 0.
|
|
*
|
|
* Return:
|
|
* - Pointer to MAP structure
|
|
* - 0 (null) if insufficient headroom
|
|
* - 0 (null) if insufficient tailroom for padding bytes
|
|
*
|
|
* todo: Parameterize skb alignment
|
|
*/
|
|
struct rmnet_map_header_s *rmnet_map_add_map_header(struct sk_buff *skb,
|
|
int hdrlen, int pad)
|
|
{
|
|
uint32_t padding, map_datalen;
|
|
uint8_t *padbytes;
|
|
struct rmnet_map_header_s *map_header;
|
|
|
|
if (skb_headroom(skb) < sizeof(struct rmnet_map_header_s))
|
|
return 0;
|
|
|
|
map_datalen = skb->len - hdrlen;
|
|
map_header = (struct rmnet_map_header_s *)
|
|
skb_push(skb, sizeof(struct rmnet_map_header_s));
|
|
memset(map_header, 0, sizeof(struct rmnet_map_header_s));
|
|
|
|
if (pad == RMNET_MAP_NO_PAD_BYTES) {
|
|
map_header->pkt_len = htons(map_datalen);
|
|
return map_header;
|
|
}
|
|
|
|
padding = ALIGN(map_datalen, 4) - map_datalen;
|
|
|
|
if (padding == 0)
|
|
goto done;
|
|
|
|
if ((skb->dev->features & NETIF_F_GSO) &&
|
|
skb_is_nonlinear(skb) && unlikely((padding != 0))) {
|
|
LOGE("pad:%d required for non linear skb", padding);
|
|
BUG();
|
|
}
|
|
|
|
if (skb_tailroom(skb) < padding)
|
|
return 0;
|
|
|
|
padbytes = (uint8_t *) skb_put(skb, padding);
|
|
LOGD("pad: %d", padding);
|
|
memset(padbytes, 0, padding);
|
|
|
|
done:
|
|
map_header->pkt_len = htons(map_datalen + padding);
|
|
map_header->pad_len = padding&0x3F;
|
|
|
|
return map_header;
|
|
}
|
|
|
|
/**
|
|
* rmnet_map_deaggregate() - Deaggregates a single packet
|
|
* @skb: Source socket buffer containing multiple MAP frames
|
|
* @config: Physical endpoint configuration of the ingress device
|
|
*
|
|
* A whole new buffer is allocated for each portion of an aggregated frame.
|
|
* Caller should keep calling deaggregate() on the source skb until 0 is
|
|
* returned, indicating that there are no more packets to deaggregate. Caller
|
|
* is responsible for freeing the original skb.
|
|
*
|
|
* Return:
|
|
* - Pointer to new skb
|
|
* - 0 (null) if no more aggregated packets
|
|
*/
|
|
struct sk_buff *rmnet_map_deaggregate(struct sk_buff *skb,
|
|
struct rmnet_phys_ep_conf_s *config)
|
|
{
|
|
struct sk_buff *skbn;
|
|
struct rmnet_map_header_s *maph;
|
|
uint32_t packet_len;
|
|
|
|
if (skb->len == 0)
|
|
return 0;
|
|
|
|
maph = (struct rmnet_map_header_s *) skb->data;
|
|
packet_len = ntohs(maph->pkt_len) + sizeof(struct rmnet_map_header_s);
|
|
|
|
if ((config->ingress_data_format & RMNET_INGRESS_FORMAT_MAP_CKSUMV3) ||
|
|
(config->ingress_data_format & RMNET_INGRESS_FORMAT_MAP_CKSUMV4))
|
|
packet_len += sizeof(struct rmnet_map_dl_checksum_trailer_s);
|
|
|
|
if ((((int)skb->len) - ((int)packet_len)) < 0) {
|
|
LOGM("%s", "Got malformed packet. Dropping");
|
|
return 0;
|
|
}
|
|
|
|
skbn = alloc_skb(packet_len + RMNET_MAP_DEAGGR_SPACING, GFP_ATOMIC);
|
|
if (!skbn)
|
|
return 0;
|
|
|
|
skbn->dev = skb->dev;
|
|
skb_reserve(skbn, RMNET_MAP_DEAGGR_HEADROOM);
|
|
skb_put(skbn, packet_len);
|
|
memcpy(skbn->data, skb->data, packet_len);
|
|
skb_pull(skb, packet_len);
|
|
|
|
|
|
/* Some hardware can send us empty frames. Catch them */
|
|
if (ntohs(maph->pkt_len) == 0) {
|
|
LOGD("Dropping empty MAP frame");
|
|
rmnet_kfree_skb(skbn, RMNET_STATS_SKBFREE_DEAGG_DATA_LEN_0);
|
|
return 0;
|
|
}
|
|
|
|
return skbn;
|
|
}
|
|
|
|
/**
|
|
* rmnet_map_flush_packet_queue() - Transmits aggregeted frame on timeout
|
|
* @work: struct agg_work containing delayed work and skb to flush
|
|
*
|
|
* This function is scheduled to run in a specified number of jiffies after
|
|
* the last frame transmitted by the network stack. When run, the buffer
|
|
* containing aggregated packets is finally transmitted on the underlying link.
|
|
*
|
|
*/
|
|
static void rmnet_map_flush_packet_queue(struct work_struct *work)
|
|
{
|
|
struct agg_work *real_work;
|
|
struct rmnet_phys_ep_conf_s *config;
|
|
unsigned long flags;
|
|
struct sk_buff *skb;
|
|
int rc, agg_count = 0;
|
|
|
|
skb = 0;
|
|
real_work = (struct agg_work *)work;
|
|
config = real_work->config;
|
|
LOGD("%s", "Entering flush thread");
|
|
spin_lock_irqsave(&config->agg_lock, flags);
|
|
if (likely(config->agg_state == RMNET_MAP_TXFER_SCHEDULED)) {
|
|
/* Buffer may have already been shipped out */
|
|
if (likely(config->agg_skb)) {
|
|
rmnet_stats_agg_pkts(config->agg_count);
|
|
if (config->agg_count > 1)
|
|
LOGL("Agg count: %d", config->agg_count);
|
|
skb = config->agg_skb;
|
|
agg_count = config->agg_count;
|
|
config->agg_skb = 0;
|
|
config->agg_count = 0;
|
|
memset(&(config->agg_time), 0, sizeof(struct timespec));
|
|
}
|
|
config->agg_state = RMNET_MAP_AGG_IDLE;
|
|
} else {
|
|
/* How did we get here? */
|
|
LOGE("Ran queued command when state %s",
|
|
"is idle. State machine likely broken");
|
|
}
|
|
|
|
spin_unlock_irqrestore(&config->agg_lock, flags);
|
|
if (skb) {
|
|
trace_rmnet_map_flush_packet_queue(skb, agg_count);
|
|
rc = dev_queue_xmit(skb);
|
|
rmnet_stats_queue_xmit(rc, RMNET_STATS_QUEUE_XMIT_AGG_TIMEOUT);
|
|
}
|
|
kfree(work);
|
|
}
|
|
|
|
/**
|
|
* rmnet_map_aggregate() - Software aggregates multiple packets.
|
|
* @skb: current packet being transmitted
|
|
* @config: Physical endpoint configuration of the ingress device
|
|
*
|
|
* Aggregates multiple SKBs into a single large SKB for transmission. MAP
|
|
* protocol is used to separate the packets in the buffer. This funcion consumes
|
|
* the argument SKB and should not be further processed by any other function.
|
|
*/
|
|
void rmnet_map_aggregate(struct sk_buff *skb,
|
|
struct rmnet_phys_ep_conf_s *config) {
|
|
uint8_t *dest_buff;
|
|
struct agg_work *work;
|
|
unsigned long flags;
|
|
struct sk_buff *agg_skb;
|
|
struct timespec diff, last;
|
|
int size, rc, agg_count = 0;
|
|
|
|
|
|
if (!skb || !config)
|
|
BUG();
|
|
size = config->egress_agg_size-skb->len;
|
|
|
|
if (size < 2000) {
|
|
LOGL("Invalid length %d", size);
|
|
return;
|
|
}
|
|
|
|
new_packet:
|
|
spin_lock_irqsave(&config->agg_lock, flags);
|
|
|
|
memcpy(&last, &(config->agg_last), sizeof(struct timespec));
|
|
getnstimeofday(&(config->agg_last));
|
|
|
|
if (!config->agg_skb) {
|
|
/* Check to see if we should agg first. If the traffic is very
|
|
* sparse, don't aggregate. We will need to tune this later
|
|
*/
|
|
diff = timespec_sub(config->agg_last, last);
|
|
|
|
if ((diff.tv_sec > 0) || (diff.tv_nsec > agg_bypass_time)) {
|
|
spin_unlock_irqrestore(&config->agg_lock, flags);
|
|
LOGL("delta t: %ld.%09lu\tcount: bypass", diff.tv_sec,
|
|
diff.tv_nsec);
|
|
rmnet_stats_agg_pkts(1);
|
|
trace_rmnet_map_aggregate(skb, 0);
|
|
rc = dev_queue_xmit(skb);
|
|
rmnet_stats_queue_xmit(rc,
|
|
RMNET_STATS_QUEUE_XMIT_AGG_SKIP);
|
|
return;
|
|
}
|
|
|
|
config->agg_skb = skb_copy_expand(skb, 0, size, GFP_ATOMIC);
|
|
if (!config->agg_skb) {
|
|
config->agg_skb = 0;
|
|
config->agg_count = 0;
|
|
memset(&(config->agg_time), 0, sizeof(struct timespec));
|
|
spin_unlock_irqrestore(&config->agg_lock, flags);
|
|
rmnet_stats_agg_pkts(1);
|
|
trace_rmnet_map_aggregate(skb, 0);
|
|
rc = dev_queue_xmit(skb);
|
|
rmnet_stats_queue_xmit(rc,
|
|
RMNET_STATS_QUEUE_XMIT_AGG_CPY_EXP_FAIL);
|
|
return;
|
|
}
|
|
config->agg_count = 1;
|
|
getnstimeofday(&(config->agg_time));
|
|
trace_rmnet_start_aggregation(skb);
|
|
rmnet_kfree_skb(skb, RMNET_STATS_SKBFREE_AGG_CPY_EXPAND);
|
|
goto schedule;
|
|
}
|
|
diff = timespec_sub(config->agg_last, config->agg_time);
|
|
|
|
if (skb->len > (config->egress_agg_size - config->agg_skb->len)
|
|
|| (config->agg_count >= config->egress_agg_count)
|
|
|| (diff.tv_sec > 0) || (diff.tv_nsec > agg_time_limit)) {
|
|
rmnet_stats_agg_pkts(config->agg_count);
|
|
agg_skb = config->agg_skb;
|
|
agg_count = config->agg_count;
|
|
config->agg_skb = 0;
|
|
config->agg_count = 0;
|
|
memset(&(config->agg_time), 0, sizeof(struct timespec));
|
|
spin_unlock_irqrestore(&config->agg_lock, flags);
|
|
LOGL("delta t: %ld.%09lu\tcount: %d", diff.tv_sec,
|
|
diff.tv_nsec, agg_count);
|
|
trace_rmnet_map_aggregate(skb, agg_count);
|
|
rc = dev_queue_xmit(agg_skb);
|
|
rmnet_stats_queue_xmit(rc,
|
|
RMNET_STATS_QUEUE_XMIT_AGG_FILL_BUFFER);
|
|
goto new_packet;
|
|
}
|
|
|
|
dest_buff = skb_put(config->agg_skb, skb->len);
|
|
memcpy(dest_buff, skb->data, skb->len);
|
|
config->agg_count++;
|
|
rmnet_kfree_skb(skb, RMNET_STATS_SKBFREE_AGG_INTO_BUFF);
|
|
|
|
schedule:
|
|
if (config->agg_state != RMNET_MAP_TXFER_SCHEDULED) {
|
|
work = kmalloc(sizeof(*work), GFP_ATOMIC);
|
|
if (!work) {
|
|
LOGE("Failed to allocate work item for packet %s",
|
|
"transfer. DATA PATH LIKELY BROKEN!");
|
|
config->agg_state = RMNET_MAP_AGG_IDLE;
|
|
spin_unlock_irqrestore(&config->agg_lock, flags);
|
|
return;
|
|
}
|
|
INIT_DELAYED_WORK((struct delayed_work *)work,
|
|
rmnet_map_flush_packet_queue);
|
|
work->config = config;
|
|
config->agg_state = RMNET_MAP_TXFER_SCHEDULED;
|
|
schedule_delayed_work((struct delayed_work *)work, 1);
|
|
}
|
|
spin_unlock_irqrestore(&config->agg_lock, flags);
|
|
return;
|
|
}
|
|
|
|
|
|
/* ***************** Checksum Offload ************************************** */
|
|
|
|
static inline uint16_t *rmnet_map_get_checksum_field(unsigned char protocol,
|
|
const void *txporthdr)
|
|
{
|
|
uint16_t *check = 0;
|
|
switch (protocol) {
|
|
case IPPROTO_TCP:
|
|
check = &(((struct tcphdr *)txporthdr)->check);
|
|
break;
|
|
|
|
case IPPROTO_UDP:
|
|
check = &(((struct udphdr *)txporthdr)->check);
|
|
break;
|
|
|
|
default:
|
|
check = 0;
|
|
break;
|
|
}
|
|
|
|
return check;
|
|
}
|
|
|
|
static inline uint16_t rmnet_map_add_checksums(uint16_t val1, uint16_t val2)
|
|
{
|
|
int sum = val1+val2;
|
|
sum = (((sum&0xFFFF0000)>>16) + sum) & 0x0000FFFF;
|
|
return (uint16_t) (sum&0x0000FFFF);
|
|
}
|
|
|
|
static inline uint16_t rmnet_map_subtract_checksums(uint16_t val1,
|
|
uint16_t val2)
|
|
{
|
|
return rmnet_map_add_checksums(val1, ~val2);
|
|
}
|
|
|
|
/**
|
|
* rmnet_map_validate_ipv4_packet_checksum() - Validates TCP/UDP checksum
|
|
* value for IPv4 packet
|
|
* @map_payload: Pointer to the beginning of the map payload
|
|
* @cksum_trailer: Pointer to the checksum trailer
|
|
*
|
|
* Validates the TCP/UDP checksum for the packet using the checksum value
|
|
* from the checksum trailer added to the packet.
|
|
* The validation formula is the following:
|
|
* 1. Performs 1's complement over the checksum value from the trailer
|
|
* 2. Computes 1's complement checksum over IPv4 header and subtracts it from
|
|
* the value from step 1
|
|
* 3. Computes 1's complement checksum over IPv4 pseudo header and adds it to
|
|
* the value from step 2
|
|
* 4. Subtracts the checksum value from the TCP/UDP header from the value from
|
|
* step 3
|
|
* 5. Compares the value from step 4 to the checksum value from the TCP/UDP
|
|
* header
|
|
*
|
|
* Fragmentation and tunneling are not supported.
|
|
*
|
|
* Return: 0 is validation succeeded.
|
|
*/
|
|
static int rmnet_map_validate_ipv4_packet_checksum(unsigned char *map_payload,
|
|
struct rmnet_map_dl_checksum_trailer_s *cksum_trailer)
|
|
{
|
|
struct iphdr *ip4h;
|
|
uint16_t *checksum_field;
|
|
void *txporthdr;
|
|
uint16_t pseudo_checksum;
|
|
uint16_t ip_hdr_checksum;
|
|
uint16_t checksum_value;
|
|
uint16_t ip_payload_checksum;
|
|
uint16_t ip_pseudo_payload_checksum;
|
|
uint16_t checksum_value_final;
|
|
|
|
ip4h = (struct iphdr *) map_payload;
|
|
if ((ntohs(ip4h->frag_off) & IP_MF)
|
|
|| ((ntohs(ip4h->frag_off) & IP_OFFSET) > 0))
|
|
return RMNET_MAP_CHECKSUM_FRAGMENTED_PACKET;
|
|
|
|
txporthdr = map_payload + ip4h->ihl*4;
|
|
|
|
checksum_field = rmnet_map_get_checksum_field(ip4h->protocol,
|
|
txporthdr);
|
|
|
|
if (unlikely(!checksum_field))
|
|
return RMNET_MAP_CHECKSUM_ERR_UNKNOWN_TRANSPORT;
|
|
|
|
/* RFC 768 - Skip IPv4 UDP packets where sender checksum field is 0 */
|
|
if ((*checksum_field == 0) && (ip4h->protocol == IPPROTO_UDP))
|
|
return RMNET_MAP_CHECKSUM_SKIPPED;
|
|
|
|
checksum_value = ~ntohs(cksum_trailer->checksum_value);
|
|
ip_hdr_checksum = ~ip_fast_csum(ip4h, (int)ip4h->ihl);
|
|
ip_payload_checksum = rmnet_map_subtract_checksums(checksum_value,
|
|
ip_hdr_checksum);
|
|
|
|
pseudo_checksum = ~ntohs(csum_tcpudp_magic(ip4h->saddr, ip4h->daddr,
|
|
(uint16_t)(ntohs(ip4h->tot_len) - ip4h->ihl*4),
|
|
(uint16_t)ip4h->protocol, 0));
|
|
ip_pseudo_payload_checksum = rmnet_map_add_checksums(
|
|
ip_payload_checksum, pseudo_checksum);
|
|
|
|
checksum_value_final = ~rmnet_map_subtract_checksums(
|
|
ip_pseudo_payload_checksum, ntohs(*checksum_field));
|
|
|
|
if (unlikely(checksum_value_final == 0)) {
|
|
switch (ip4h->protocol) {
|
|
case IPPROTO_UDP:
|
|
/* RFC 768 */
|
|
LOGD("DL4 1's complement rule for UDP checksum 0");
|
|
checksum_value_final = ~checksum_value_final;
|
|
break;
|
|
|
|
case IPPROTO_TCP:
|
|
if (*checksum_field == 0xFFFF) {
|
|
LOGD(
|
|
"DL4 Non-RFC compliant TCP checksum found");
|
|
checksum_value_final = ~checksum_value_final;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
LOGD(
|
|
"DL4 cksum: ~HW: %04X, field: %04X, pseudo header: %04X, final: %04X",
|
|
~ntohs(cksum_trailer->checksum_value), ntohs(*checksum_field),
|
|
pseudo_checksum, checksum_value_final);
|
|
|
|
if (checksum_value_final == ntohs(*checksum_field))
|
|
return RMNET_MAP_CHECKSUM_OK;
|
|
else
|
|
return RMNET_MAP_CHECKSUM_VALIDATION_FAILED;
|
|
}
|
|
|
|
/**
|
|
* rmnet_map_validate_ipv6_packet_checksum() - Validates TCP/UDP checksum
|
|
* value for IPv6 packet
|
|
* @map_payload: Pointer to the beginning of the map payload
|
|
* @cksum_trailer: Pointer to the checksum trailer
|
|
*
|
|
* Validates the TCP/UDP checksum for the packet using the checksum value
|
|
* from the checksum trailer added to the packet.
|
|
* The validation formula is the following:
|
|
* 1. Performs 1's complement over the checksum value from the trailer
|
|
* 2. Computes 1's complement checksum over IPv6 header and subtracts it from
|
|
* the value from step 1
|
|
* 3. Computes 1's complement checksum over IPv6 pseudo header and adds it to
|
|
* the value from step 2
|
|
* 4. Subtracts the checksum value from the TCP/UDP header from the value from
|
|
* step 3
|
|
* 5. Compares the value from step 4 to the checksum value from the TCP/UDP
|
|
* header
|
|
*
|
|
* Fragmentation, extension headers and tunneling are not supported.
|
|
*
|
|
* Return: 0 is validation succeeded.
|
|
*/
|
|
static int rmnet_map_validate_ipv6_packet_checksum(unsigned char *map_payload,
|
|
struct rmnet_map_dl_checksum_trailer_s *cksum_trailer)
|
|
{
|
|
struct ipv6hdr *ip6h;
|
|
uint16_t *checksum_field;
|
|
void *txporthdr;
|
|
uint16_t pseudo_checksum;
|
|
uint16_t ip_hdr_checksum;
|
|
uint16_t checksum_value;
|
|
uint16_t ip_payload_checksum;
|
|
uint16_t ip_pseudo_payload_checksum;
|
|
uint16_t checksum_value_final;
|
|
uint32_t length;
|
|
|
|
ip6h = (struct ipv6hdr *) map_payload;
|
|
|
|
txporthdr = map_payload + sizeof(struct ipv6hdr);
|
|
checksum_field = rmnet_map_get_checksum_field(ip6h->nexthdr,
|
|
txporthdr);
|
|
|
|
if (unlikely(!checksum_field))
|
|
return RMNET_MAP_CHECKSUM_ERR_UNKNOWN_TRANSPORT;
|
|
|
|
checksum_value = ~ntohs(cksum_trailer->checksum_value);
|
|
ip_hdr_checksum = ~ntohs(ip_compute_csum(ip6h,
|
|
(int)(txporthdr - (void *)map_payload)));
|
|
ip_payload_checksum = rmnet_map_subtract_checksums(checksum_value,
|
|
ip_hdr_checksum);
|
|
|
|
length = (ip6h->nexthdr == IPPROTO_UDP) ?
|
|
ntohs(((struct udphdr *)txporthdr)->len) :
|
|
ntohs(ip6h->payload_len);
|
|
pseudo_checksum = ~ntohs(csum_ipv6_magic(&ip6h->saddr, &ip6h->daddr,
|
|
length, ip6h->nexthdr, 0));
|
|
ip_pseudo_payload_checksum = rmnet_map_add_checksums(
|
|
ip_payload_checksum, pseudo_checksum);
|
|
|
|
checksum_value_final = ~rmnet_map_subtract_checksums(
|
|
ip_pseudo_payload_checksum, ntohs(*checksum_field));
|
|
|
|
if (unlikely(checksum_value_final == 0)) {
|
|
switch (ip6h->nexthdr) {
|
|
case IPPROTO_UDP:
|
|
/* RFC 2460 section 8.1 */
|
|
LOGD("DL6 One's complement rule for UDP checksum 0");
|
|
checksum_value_final = ~checksum_value_final;
|
|
break;
|
|
|
|
case IPPROTO_TCP:
|
|
if (*checksum_field == 0xFFFF) {
|
|
LOGD(
|
|
"DL6 Non-RFC compliant TCP checksum found");
|
|
checksum_value_final = ~checksum_value_final;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
LOGD(
|
|
"DL6 cksum: ~HW: %04X, field: %04X, pseudo header: %04X, final: %04X",
|
|
~ntohs(cksum_trailer->checksum_value), ntohs(*checksum_field),
|
|
pseudo_checksum, checksum_value_final);
|
|
|
|
if (checksum_value_final == ntohs(*checksum_field))
|
|
return RMNET_MAP_CHECKSUM_OK;
|
|
else
|
|
return RMNET_MAP_CHECKSUM_VALIDATION_FAILED;
|
|
}
|
|
|
|
/**
|
|
* rmnet_map_checksum_downlink_packet() - Validates checksum on
|
|
* a downlink packet
|
|
* @skb: Pointer to the packet's skb.
|
|
*
|
|
* Validates packet checksums. Function takes a pointer to
|
|
* the beginning of a buffer which contains the entire MAP
|
|
* frame: MAP header + IP payload + padding + checksum trailer.
|
|
* Currently, only IPv4 and IPv6 are supported along with
|
|
* TCP & UDP. Fragmented or tunneled packets are not supported.
|
|
*
|
|
* Return:
|
|
* - RMNET_MAP_CHECKSUM_OK: Validation of checksum succeeded.
|
|
* - RMNET_MAP_CHECKSUM_ERR_BAD_BUFFER: Skb buffer given is corrupted.
|
|
* - RMNET_MAP_CHECKSUM_VALID_FLAG_NOT_SET: Valid flag is not set in the
|
|
* checksum trailer.
|
|
* - RMNET_MAP_CHECKSUM_FRAGMENTED_PACKET: The packet is a fragment.
|
|
* - RMNET_MAP_CHECKSUM_ERR_UNKNOWN_TRANSPORT: The transport header is
|
|
* not TCP/UDP.
|
|
* - RMNET_MAP_CHECKSUM_ERR_UNKNOWN_IP_VERSION: Unrecognized IP header.
|
|
* - RMNET_MAP_CHECKSUM_VALIDATION_FAILED: In case the validation failed.
|
|
*/
|
|
int rmnet_map_checksum_downlink_packet(struct sk_buff *skb)
|
|
{
|
|
struct rmnet_map_dl_checksum_trailer_s *cksum_trailer;
|
|
unsigned int data_len;
|
|
unsigned char *map_payload;
|
|
unsigned char ip_version;
|
|
|
|
data_len = RMNET_MAP_GET_LENGTH(skb);
|
|
|
|
if (unlikely(skb->len < (sizeof(struct rmnet_map_header_s) + data_len +
|
|
sizeof(struct rmnet_map_dl_checksum_trailer_s))))
|
|
return RMNET_MAP_CHECKSUM_ERR_BAD_BUFFER;
|
|
|
|
cksum_trailer = (struct rmnet_map_dl_checksum_trailer_s *)
|
|
(skb->data + data_len
|
|
+ sizeof(struct rmnet_map_header_s));
|
|
|
|
if (unlikely(!ntohs(cksum_trailer->valid)))
|
|
return RMNET_MAP_CHECKSUM_VALID_FLAG_NOT_SET;
|
|
|
|
map_payload = (unsigned char *)(skb->data
|
|
+ sizeof(struct rmnet_map_header_s));
|
|
|
|
ip_version = (*map_payload & 0xF0) >> 4;
|
|
if (ip_version == 0x04)
|
|
return rmnet_map_validate_ipv4_packet_checksum(map_payload,
|
|
cksum_trailer);
|
|
else if (ip_version == 0x06)
|
|
return rmnet_map_validate_ipv6_packet_checksum(map_payload,
|
|
cksum_trailer);
|
|
|
|
return RMNET_MAP_CHECKSUM_ERR_UNKNOWN_IP_VERSION;
|
|
}
|
|
|
|
static void rmnet_map_fill_ipv4_packet_ul_checksum_header(void *iphdr,
|
|
struct rmnet_map_ul_checksum_header_s *ul_header, struct sk_buff *skb)
|
|
{
|
|
struct iphdr *ip4h = (struct iphdr *)iphdr;
|
|
unsigned short *hdr = (unsigned short *)ul_header;
|
|
|
|
ul_header->checksum_start_offset = htons((unsigned short)
|
|
(skb_transport_header(skb) - (unsigned char *)iphdr));
|
|
ul_header->checksum_insert_offset = skb->csum_offset;
|
|
ul_header->cks_en = 1;
|
|
if (ip4h->protocol == IPPROTO_UDP)
|
|
ul_header->udp_ip4_ind = 1;
|
|
else
|
|
ul_header->udp_ip4_ind = 0;
|
|
/* Changing checksum_insert_offset to network order */
|
|
hdr++;
|
|
*hdr = htons(*hdr);
|
|
skb->ip_summed = CHECKSUM_NONE;
|
|
}
|
|
|
|
static void rmnet_map_fill_ipv6_packet_ul_checksum_header(void *iphdr,
|
|
struct rmnet_map_ul_checksum_header_s *ul_header, struct sk_buff *skb)
|
|
{
|
|
unsigned short *hdr = (unsigned short *)ul_header;
|
|
|
|
ul_header->checksum_start_offset = htons((unsigned short)
|
|
(skb_transport_header(skb) - (unsigned char *)iphdr));
|
|
ul_header->checksum_insert_offset = skb->csum_offset;
|
|
ul_header->cks_en = 1;
|
|
ul_header->udp_ip4_ind = 0;
|
|
/* Changing checksum_insert_offset to network order */
|
|
hdr++;
|
|
*hdr = htons(*hdr);
|
|
skb->ip_summed = CHECKSUM_NONE;
|
|
}
|
|
|
|
static void rmnet_map_complement_ipv4_txporthdr_csum_field(void *iphdr)
|
|
{
|
|
struct iphdr *ip4h = (struct iphdr *)iphdr;
|
|
void *txporthdr;
|
|
uint16_t *csum;
|
|
|
|
txporthdr = iphdr + ip4h->ihl*4;
|
|
|
|
if ((ip4h->protocol == IPPROTO_TCP) ||
|
|
(ip4h->protocol == IPPROTO_UDP)) {
|
|
csum = (uint16_t *)rmnet_map_get_checksum_field(ip4h->protocol,
|
|
txporthdr);
|
|
*csum = ~(*csum);
|
|
}
|
|
}
|
|
|
|
static void rmnet_map_complement_ipv6_txporthdr_csum_field(void *ip6hdr)
|
|
{
|
|
struct ipv6hdr *ip6h = (struct ipv6hdr *)ip6hdr;
|
|
void *txporthdr;
|
|
uint16_t *csum;
|
|
|
|
txporthdr = ip6hdr + sizeof(struct ipv6hdr);
|
|
|
|
if ((ip6h->nexthdr == IPPROTO_TCP) || (ip6h->nexthdr == IPPROTO_UDP)) {
|
|
csum = (uint16_t *)rmnet_map_get_checksum_field(ip6h->nexthdr,
|
|
txporthdr);
|
|
*csum = ~(*csum);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* rmnet_map_checksum_uplink_packet() - Generates UL checksum
|
|
* meta info header
|
|
* @skb: Pointer to the packet's skb.
|
|
*
|
|
* Generates UL checksum meta info header for IPv4 and IPv6 over TCP and UDP
|
|
* packets that are supported for UL checksum offload.
|
|
*
|
|
* Return:
|
|
* - RMNET_MAP_CHECKSUM_OK: Validation of checksum succeeded.
|
|
* - RMNET_MAP_CHECKSUM_ERR_UNKNOWN_IP_VERSION: Unrecognized IP header.
|
|
* - RMNET_MAP_CHECKSUM_SW: Unsupported packet for UL checksum offload.
|
|
*/
|
|
int rmnet_map_checksum_uplink_packet(struct sk_buff *skb,
|
|
struct net_device *orig_dev, uint32_t egress_data_format)
|
|
{
|
|
unsigned char ip_version;
|
|
struct rmnet_map_ul_checksum_header_s *ul_header;
|
|
void *iphdr;
|
|
int ret;
|
|
|
|
ul_header = (struct rmnet_map_ul_checksum_header_s *)
|
|
skb_push(skb, sizeof(struct rmnet_map_ul_checksum_header_s));
|
|
|
|
if (unlikely(!(orig_dev->features &
|
|
(NETIF_F_V4_CSUM | NETIF_F_V6_CSUM)))) {
|
|
ret = RMNET_MAP_CHECKSUM_SW;
|
|
goto sw_checksum;
|
|
}
|
|
|
|
if (skb->ip_summed == CHECKSUM_PARTIAL) {
|
|
iphdr = (char *)ul_header +
|
|
sizeof(struct rmnet_map_ul_checksum_header_s);
|
|
ip_version = (*(char *)iphdr & 0xF0) >> 4;
|
|
if (ip_version == 0x04) {
|
|
rmnet_map_fill_ipv4_packet_ul_checksum_header(iphdr,
|
|
ul_header, skb);
|
|
if (egress_data_format &
|
|
RMNET_EGRESS_FORMAT_MAP_CKSUMV4)
|
|
rmnet_map_complement_ipv4_txporthdr_csum_field(
|
|
iphdr);
|
|
return RMNET_MAP_CHECKSUM_OK;
|
|
} else if (ip_version == 0x06) {
|
|
rmnet_map_fill_ipv6_packet_ul_checksum_header(iphdr,
|
|
ul_header, skb);
|
|
if (egress_data_format &
|
|
RMNET_EGRESS_FORMAT_MAP_CKSUMV4)
|
|
rmnet_map_complement_ipv6_txporthdr_csum_field(
|
|
iphdr);
|
|
return RMNET_MAP_CHECKSUM_OK;
|
|
} else {
|
|
ret = RMNET_MAP_CHECKSUM_ERR_UNKNOWN_IP_VERSION;
|
|
goto sw_checksum;
|
|
}
|
|
} else {
|
|
ret = RMNET_MAP_CHECKSUM_SW;
|
|
goto sw_checksum;
|
|
}
|
|
|
|
sw_checksum:
|
|
ul_header->checksum_start_offset = 0;
|
|
ul_header->checksum_insert_offset = 0;
|
|
ul_header->cks_en = 0;
|
|
ul_header->udp_ip4_ind = 0;
|
|
return ret;
|
|
}
|