/* * drivers/s390/net/qeth_core_offl.c * * Copyright IBM Corp. 2007 * Author(s): Thomas Spatzier , * Frank Blaschka */ #include #include #include #include #include #include #include #include #include #include #include "qeth_core.h" #include "qeth_core_mpc.h" #include "qeth_core_offl.h" int qeth_eddp_check_buffers_for_context(struct qeth_qdio_out_q *queue, struct qeth_eddp_context *ctx) { int index = queue->next_buf_to_fill; int elements_needed = ctx->num_elements; int elements_in_buffer; int skbs_in_buffer; int buffers_needed = 0; QETH_DBF_TEXT(TRACE, 5, "eddpcbfc"); while (elements_needed > 0) { buffers_needed++; if (atomic_read(&queue->bufs[index].state) != QETH_QDIO_BUF_EMPTY) return -EBUSY; elements_in_buffer = QETH_MAX_BUFFER_ELEMENTS(queue->card) - queue->bufs[index].next_element_to_fill; skbs_in_buffer = elements_in_buffer / ctx->elements_per_skb; elements_needed -= skbs_in_buffer * ctx->elements_per_skb; index = (index + 1) % QDIO_MAX_BUFFERS_PER_Q; } return buffers_needed; } static void qeth_eddp_free_context(struct qeth_eddp_context *ctx) { int i; QETH_DBF_TEXT(TRACE, 5, "eddpfctx"); for (i = 0; i < ctx->num_pages; ++i) free_page((unsigned long)ctx->pages[i]); kfree(ctx->pages); kfree(ctx->elements); kfree(ctx); } static void qeth_eddp_get_context(struct qeth_eddp_context *ctx) { atomic_inc(&ctx->refcnt); } void qeth_eddp_put_context(struct qeth_eddp_context *ctx) { if (atomic_dec_return(&ctx->refcnt) == 0) qeth_eddp_free_context(ctx); } EXPORT_SYMBOL_GPL(qeth_eddp_put_context); void qeth_eddp_buf_release_contexts(struct qeth_qdio_out_buffer *buf) { struct qeth_eddp_context_reference *ref; QETH_DBF_TEXT(TRACE, 6, "eddprctx"); while (!list_empty(&buf->ctx_list)) { ref = list_entry(buf->ctx_list.next, struct qeth_eddp_context_reference, list); qeth_eddp_put_context(ref->ctx); list_del(&ref->list); kfree(ref); } } static int qeth_eddp_buf_ref_context(struct qeth_qdio_out_buffer *buf, struct qeth_eddp_context *ctx) { struct qeth_eddp_context_reference *ref; QETH_DBF_TEXT(TRACE, 6, "eddprfcx"); ref = kmalloc(sizeof(struct qeth_eddp_context_reference), GFP_ATOMIC); if (ref == NULL) return -ENOMEM; qeth_eddp_get_context(ctx); ref->ctx = ctx; list_add_tail(&ref->list, &buf->ctx_list); return 0; } int qeth_eddp_fill_buffer(struct qeth_qdio_out_q *queue, struct qeth_eddp_context *ctx, int index) { struct qeth_qdio_out_buffer *buf = NULL; struct qdio_buffer *buffer; int elements = ctx->num_elements; int element = 0; int flush_cnt = 0; int must_refcnt = 1; int i; QETH_DBF_TEXT(TRACE, 5, "eddpfibu"); while (elements > 0) { buf = &queue->bufs[index]; if (atomic_read(&buf->state) != QETH_QDIO_BUF_EMPTY) { /* normally this should not happen since we checked for * available elements in qeth_check_elements_for_context */ if (element == 0) return -EBUSY; else { QETH_DBF_MESSAGE(2, "could only partially fill" "eddp buffer!\n"); goto out; } } /* check if the whole next skb fits into current buffer */ if ((QETH_MAX_BUFFER_ELEMENTS(queue->card) - buf->next_element_to_fill) < ctx->elements_per_skb){ /* no -> go to next buffer */ atomic_set(&buf->state, QETH_QDIO_BUF_PRIMED); index = (index + 1) % QDIO_MAX_BUFFERS_PER_Q; flush_cnt++; /* new buffer, so we have to add ctx to buffer'ctx_list * and increment ctx's refcnt */ must_refcnt = 1; continue; } if (must_refcnt) { must_refcnt = 0; if (qeth_eddp_buf_ref_context(buf, ctx)) { goto out_check; } } buffer = buf->buffer; /* fill one skb into buffer */ for (i = 0; i < ctx->elements_per_skb; ++i) { if (ctx->elements[element].length != 0) { buffer->element[buf->next_element_to_fill]. addr = ctx->elements[element].addr; buffer->element[buf->next_element_to_fill]. length = ctx->elements[element].length; buffer->element[buf->next_element_to_fill]. flags = ctx->elements[element].flags; buf->next_element_to_fill++; } element++; elements--; } } out_check: if (!queue->do_pack) { QETH_DBF_TEXT(TRACE, 6, "fillbfnp"); /* set state to PRIMED -> will be flushed */ if (buf->next_element_to_fill > 0) { atomic_set(&buf->state, QETH_QDIO_BUF_PRIMED); flush_cnt++; } } else { if (queue->card->options.performance_stats) queue->card->perf_stats.skbs_sent_pack++; QETH_DBF_TEXT(TRACE, 6, "fillbfpa"); if (buf->next_element_to_fill >= QETH_MAX_BUFFER_ELEMENTS(queue->card)) { /* * packed buffer if full -> set state PRIMED * -> will be flushed */ atomic_set(&buf->state, QETH_QDIO_BUF_PRIMED); flush_cnt++; } } out: return flush_cnt; } static void qeth_eddp_create_segment_hdrs(struct qeth_eddp_context *ctx, struct qeth_eddp_data *eddp, int data_len) { u8 *page; int page_remainder; int page_offset; int pkt_len; struct qeth_eddp_element *element; QETH_DBF_TEXT(TRACE, 5, "eddpcrsh"); page = ctx->pages[ctx->offset >> PAGE_SHIFT]; page_offset = ctx->offset % PAGE_SIZE; element = &ctx->elements[ctx->num_elements]; pkt_len = eddp->nhl + eddp->thl + data_len; /* FIXME: layer2 and VLAN !!! */ if (eddp->qh.hdr.l2.id == QETH_HEADER_TYPE_LAYER2) pkt_len += ETH_HLEN; if (eddp->mac.h_proto == __constant_htons(ETH_P_8021Q)) pkt_len += VLAN_HLEN; /* does complete packet fit in current page ? */ page_remainder = PAGE_SIZE - page_offset; if (page_remainder < (sizeof(struct qeth_hdr) + pkt_len)) { /* no -> go to start of next page */ ctx->offset += page_remainder; page = ctx->pages[ctx->offset >> PAGE_SHIFT]; page_offset = 0; } memcpy(page + page_offset, &eddp->qh, sizeof(struct qeth_hdr)); element->addr = page + page_offset; element->length = sizeof(struct qeth_hdr); ctx->offset += sizeof(struct qeth_hdr); page_offset += sizeof(struct qeth_hdr); /* add mac header (?) */ if (eddp->qh.hdr.l2.id == QETH_HEADER_TYPE_LAYER2) { memcpy(page + page_offset, &eddp->mac, ETH_HLEN); element->length += ETH_HLEN; ctx->offset += ETH_HLEN; page_offset += ETH_HLEN; } /* add VLAN tag */ if (eddp->mac.h_proto == __constant_htons(ETH_P_8021Q)) { memcpy(page + page_offset, &eddp->vlan, VLAN_HLEN); element->length += VLAN_HLEN; ctx->offset += VLAN_HLEN; page_offset += VLAN_HLEN; } /* add network header */ memcpy(page + page_offset, (u8 *)&eddp->nh, eddp->nhl); element->length += eddp->nhl; eddp->nh_in_ctx = page + page_offset; ctx->offset += eddp->nhl; page_offset += eddp->nhl; /* add transport header */ memcpy(page + page_offset, (u8 *)&eddp->th, eddp->thl); element->length += eddp->thl; eddp->th_in_ctx = page + page_offset; ctx->offset += eddp->thl; } static void qeth_eddp_copy_data_tcp(char *dst, struct qeth_eddp_data *eddp, int len, __wsum *hcsum) { struct skb_frag_struct *frag; int left_in_frag; int copy_len; u8 *src; QETH_DBF_TEXT(TRACE, 5, "eddpcdtc"); if (skb_shinfo(eddp->skb)->nr_frags == 0) { skb_copy_from_linear_data_offset(eddp->skb, eddp->skb_offset, dst, len); *hcsum = csum_partial(eddp->skb->data + eddp->skb_offset, len, *hcsum); eddp->skb_offset += len; } else { while (len > 0) { if (eddp->frag < 0) { /* we're in skb->data */ left_in_frag = (eddp->skb->len - eddp->skb->data_len) - eddp->skb_offset; src = eddp->skb->data + eddp->skb_offset; } else { frag = &skb_shinfo(eddp->skb)->frags[ eddp->frag]; left_in_frag = frag->size - eddp->frag_offset; src = (u8 *)((page_to_pfn(frag->page) << PAGE_SHIFT) + frag->page_offset + eddp->frag_offset); } if (left_in_frag <= 0) { eddp->frag++; eddp->frag_offset = 0; continue; } copy_len = min(left_in_frag, len); memcpy(dst, src, copy_len); *hcsum = csum_partial(src, copy_len, *hcsum); dst += copy_len; eddp->frag_offset += copy_len; eddp->skb_offset += copy_len; len -= copy_len; } } } static void qeth_eddp_create_segment_data_tcp(struct qeth_eddp_context *ctx, struct qeth_eddp_data *eddp, int data_len, __wsum hcsum) { u8 *page; int page_remainder; int page_offset; struct qeth_eddp_element *element; int first_lap = 1; QETH_DBF_TEXT(TRACE, 5, "eddpcsdt"); page = ctx->pages[ctx->offset >> PAGE_SHIFT]; page_offset = ctx->offset % PAGE_SIZE; element = &ctx->elements[ctx->num_elements]; while (data_len) { page_remainder = PAGE_SIZE - page_offset; if (page_remainder < data_len) { qeth_eddp_copy_data_tcp(page + page_offset, eddp, page_remainder, &hcsum); element->length += page_remainder; if (first_lap) element->flags = SBAL_FLAGS_FIRST_FRAG; else element->flags = SBAL_FLAGS_MIDDLE_FRAG; ctx->num_elements++; element++; data_len -= page_remainder; ctx->offset += page_remainder; page = ctx->pages[ctx->offset >> PAGE_SHIFT]; page_offset = 0; element->addr = page + page_offset; } else { qeth_eddp_copy_data_tcp(page + page_offset, eddp, data_len, &hcsum); element->length += data_len; if (!first_lap) element->flags = SBAL_FLAGS_LAST_FRAG; ctx->num_elements++; ctx->offset += data_len; data_len = 0; } first_lap = 0; } ((struct tcphdr *)eddp->th_in_ctx)->check = csum_fold(hcsum); } static __wsum qeth_eddp_check_tcp4_hdr(struct qeth_eddp_data *eddp, int data_len) { __wsum phcsum; /* pseudo header checksum */ QETH_DBF_TEXT(TRACE, 5, "eddpckt4"); eddp->th.tcp.h.check = 0; /* compute pseudo header checksum */ phcsum = csum_tcpudp_nofold(eddp->nh.ip4.h.saddr, eddp->nh.ip4.h.daddr, eddp->thl + data_len, IPPROTO_TCP, 0); /* compute checksum of tcp header */ return csum_partial(&eddp->th, eddp->thl, phcsum); } static __wsum qeth_eddp_check_tcp6_hdr(struct qeth_eddp_data *eddp, int data_len) { __be32 proto; __wsum phcsum; /* pseudo header checksum */ QETH_DBF_TEXT(TRACE, 5, "eddpckt6"); eddp->th.tcp.h.check = 0; /* compute pseudo header checksum */ phcsum = csum_partial(&eddp->nh.ip6.h.saddr, sizeof(struct in6_addr), 0); phcsum = csum_partial(&eddp->nh.ip6.h.daddr, sizeof(struct in6_addr), phcsum); proto = htonl(IPPROTO_TCP); phcsum = csum_partial(&proto, sizeof(u32), phcsum); return phcsum; } static struct qeth_eddp_data *qeth_eddp_create_eddp_data(struct qeth_hdr *qh, u8 *nh, u8 nhl, u8 *th, u8 thl) { struct qeth_eddp_data *eddp; QETH_DBF_TEXT(TRACE, 5, "eddpcrda"); eddp = kzalloc(sizeof(struct qeth_eddp_data), GFP_ATOMIC); if (eddp) { eddp->nhl = nhl; eddp->thl = thl; memcpy(&eddp->qh, qh, sizeof(struct qeth_hdr)); memcpy(&eddp->nh, nh, nhl); memcpy(&eddp->th, th, thl); eddp->frag = -1; /* initially we're in skb->data */ } return eddp; } static void __qeth_eddp_fill_context_tcp(struct qeth_eddp_context *ctx, struct qeth_eddp_data *eddp) { struct tcphdr *tcph; int data_len; __wsum hcsum; QETH_DBF_TEXT(TRACE, 5, "eddpftcp"); eddp->skb_offset = sizeof(struct qeth_hdr) + eddp->nhl + eddp->thl; if (eddp->qh.hdr.l2.id == QETH_HEADER_TYPE_LAYER2) { eddp->skb_offset += sizeof(struct ethhdr); if (eddp->mac.h_proto == __constant_htons(ETH_P_8021Q)) eddp->skb_offset += VLAN_HLEN; } tcph = tcp_hdr(eddp->skb); while (eddp->skb_offset < eddp->skb->len) { data_len = min((int)skb_shinfo(eddp->skb)->gso_size, (int)(eddp->skb->len - eddp->skb_offset)); /* prepare qdio hdr */ if (eddp->qh.hdr.l2.id == QETH_HEADER_TYPE_LAYER2) { eddp->qh.hdr.l2.pkt_length = data_len + ETH_HLEN + eddp->nhl + eddp->thl; if (eddp->mac.h_proto == __constant_htons(ETH_P_8021Q)) eddp->qh.hdr.l2.pkt_length += VLAN_HLEN; } else eddp->qh.hdr.l3.length = data_len + eddp->nhl + eddp->thl; /* prepare ip hdr */ if (eddp->skb->protocol == htons(ETH_P_IP)) { eddp->nh.ip4.h.tot_len = htons(data_len + eddp->nhl + eddp->thl); eddp->nh.ip4.h.check = 0; eddp->nh.ip4.h.check = ip_fast_csum((u8 *)&eddp->nh.ip4.h, eddp->nh.ip4.h.ihl); } else eddp->nh.ip6.h.payload_len = htons(data_len + eddp->thl); /* prepare tcp hdr */ if (data_len == (eddp->skb->len - eddp->skb_offset)) { /* last segment -> set FIN and PSH flags */ eddp->th.tcp.h.fin = tcph->fin; eddp->th.tcp.h.psh = tcph->psh; } if (eddp->skb->protocol == htons(ETH_P_IP)) hcsum = qeth_eddp_check_tcp4_hdr(eddp, data_len); else hcsum = qeth_eddp_check_tcp6_hdr(eddp, data_len); /* fill the next segment into the context */ qeth_eddp_create_segment_hdrs(ctx, eddp, data_len); qeth_eddp_create_segment_data_tcp(ctx, eddp, data_len, hcsum); if (eddp->skb_offset >= eddp->skb->len) break; /* prepare headers for next round */ if (eddp->skb->protocol == htons(ETH_P_IP)) eddp->nh.ip4.h.id = htons(ntohs(eddp->nh.ip4.h.id) + 1); eddp->th.tcp.h.seq = htonl(ntohl(eddp->th.tcp.h.seq) + data_len); } } static int qeth_eddp_fill_context_tcp(struct qeth_eddp_context *ctx, struct sk_buff *skb, struct qeth_hdr *qhdr) { struct qeth_eddp_data *eddp = NULL; QETH_DBF_TEXT(TRACE, 5, "eddpficx"); /* create our segmentation headers and copy original headers */ if (skb->protocol == htons(ETH_P_IP)) eddp = qeth_eddp_create_eddp_data(qhdr, skb_network_header(skb), ip_hdrlen(skb), skb_transport_header(skb), tcp_hdrlen(skb)); else eddp = qeth_eddp_create_eddp_data(qhdr, skb_network_header(skb), sizeof(struct ipv6hdr), skb_transport_header(skb), tcp_hdrlen(skb)); if (eddp == NULL) { QETH_DBF_TEXT(TRACE, 2, "eddpfcnm"); return -ENOMEM; } if (qhdr->hdr.l2.id == QETH_HEADER_TYPE_LAYER2) { skb_set_mac_header(skb, sizeof(struct qeth_hdr)); memcpy(&eddp->mac, eth_hdr(skb), ETH_HLEN); if (eddp->mac.h_proto == __constant_htons(ETH_P_8021Q)) { eddp->vlan[0] = skb->protocol; eddp->vlan[1] = htons(vlan_tx_tag_get(skb)); } } /* the next flags will only be set on the last segment */ eddp->th.tcp.h.fin = 0; eddp->th.tcp.h.psh = 0; eddp->skb = skb; /* begin segmentation and fill context */ __qeth_eddp_fill_context_tcp(ctx, eddp); kfree(eddp); return 0; } static void qeth_eddp_calc_num_pages(struct qeth_eddp_context *ctx, struct sk_buff *skb, int hdr_len) { int skbs_per_page; QETH_DBF_TEXT(TRACE, 5, "eddpcanp"); /* can we put multiple skbs in one page? */ skbs_per_page = PAGE_SIZE / (skb_shinfo(skb)->gso_size + hdr_len); if (skbs_per_page > 1) { ctx->num_pages = (skb_shinfo(skb)->gso_segs + 1) / skbs_per_page + 1; ctx->elements_per_skb = 1; } else { /* no -> how many elements per skb? */ ctx->elements_per_skb = (skb_shinfo(skb)->gso_size + hdr_len + PAGE_SIZE) >> PAGE_SHIFT; ctx->num_pages = ctx->elements_per_skb * (skb_shinfo(skb)->gso_segs + 1); } ctx->num_elements = ctx->elements_per_skb * (skb_shinfo(skb)->gso_segs + 1); } static struct qeth_eddp_context *qeth_eddp_create_context_generic( struct qeth_card *card, struct sk_buff *skb, int hdr_len) { struct qeth_eddp_context *ctx = NULL; u8 *addr; int i; QETH_DBF_TEXT(TRACE, 5, "creddpcg"); /* create the context and allocate pages */ ctx = kzalloc(sizeof(struct qeth_eddp_context), GFP_ATOMIC); if (ctx == NULL) { QETH_DBF_TEXT(TRACE, 2, "ceddpcn1"); return NULL; } ctx->type = QETH_LARGE_SEND_EDDP; qeth_eddp_calc_num_pages(ctx, skb, hdr_len); if (ctx->elements_per_skb > QETH_MAX_BUFFER_ELEMENTS(card)) { QETH_DBF_TEXT(TRACE, 2, "ceddpcis"); kfree(ctx); return NULL; } ctx->pages = kcalloc(ctx->num_pages, sizeof(u8 *), GFP_ATOMIC); if (ctx->pages == NULL) { QETH_DBF_TEXT(TRACE, 2, "ceddpcn2"); kfree(ctx); return NULL; } for (i = 0; i < ctx->num_pages; ++i) { addr = (u8 *)get_zeroed_page(GFP_ATOMIC); if (addr == NULL) { QETH_DBF_TEXT(TRACE, 2, "ceddpcn3"); ctx->num_pages = i; qeth_eddp_free_context(ctx); return NULL; } ctx->pages[i] = addr; } ctx->elements = kcalloc(ctx->num_elements, sizeof(struct qeth_eddp_element), GFP_ATOMIC); if (ctx->elements == NULL) { QETH_DBF_TEXT(TRACE, 2, "ceddpcn4"); qeth_eddp_free_context(ctx); return NULL; } /* reset num_elements; will be incremented again in fill_buffer to * reflect number of actually used elements */ ctx->num_elements = 0; return ctx; } static struct qeth_eddp_context *qeth_eddp_create_context_tcp( struct qeth_card *card, struct sk_buff *skb, struct qeth_hdr *qhdr) { struct qeth_eddp_context *ctx = NULL; QETH_DBF_TEXT(TRACE, 5, "creddpct"); if (skb->protocol == htons(ETH_P_IP)) ctx = qeth_eddp_create_context_generic(card, skb, (sizeof(struct qeth_hdr) + ip_hdrlen(skb) + tcp_hdrlen(skb))); else if (skb->protocol == htons(ETH_P_IPV6)) ctx = qeth_eddp_create_context_generic(card, skb, sizeof(struct qeth_hdr) + sizeof(struct ipv6hdr) + tcp_hdrlen(skb)); else QETH_DBF_TEXT(TRACE, 2, "cetcpinv"); if (ctx == NULL) { QETH_DBF_TEXT(TRACE, 2, "creddpnl"); return NULL; } if (qeth_eddp_fill_context_tcp(ctx, skb, qhdr)) { QETH_DBF_TEXT(TRACE, 2, "ceddptfe"); qeth_eddp_free_context(ctx); return NULL; } atomic_set(&ctx->refcnt, 1); return ctx; } struct qeth_eddp_context *qeth_eddp_create_context(struct qeth_card *card, struct sk_buff *skb, struct qeth_hdr *qhdr, unsigned char sk_protocol) { QETH_DBF_TEXT(TRACE, 5, "creddpc"); switch (sk_protocol) { case IPPROTO_TCP: return qeth_eddp_create_context_tcp(card, skb, qhdr); default: QETH_DBF_TEXT(TRACE, 2, "eddpinvp"); } return NULL; } EXPORT_SYMBOL_GPL(qeth_eddp_create_context); void qeth_tso_fill_header(struct qeth_card *card, struct qeth_hdr *qhdr, struct sk_buff *skb) { struct qeth_hdr_tso *hdr = (struct qeth_hdr_tso *)qhdr; struct tcphdr *tcph = tcp_hdr(skb); struct iphdr *iph = ip_hdr(skb); struct ipv6hdr *ip6h = ipv6_hdr(skb); QETH_DBF_TEXT(TRACE, 5, "tsofhdr"); /*fix header to TSO values ...*/ hdr->hdr.hdr.l3.id = QETH_HEADER_TYPE_TSO; /*set values which are fix for the first approach ...*/ hdr->ext.hdr_tot_len = (__u16) sizeof(struct qeth_hdr_ext_tso); hdr->ext.imb_hdr_no = 1; hdr->ext.hdr_type = 1; hdr->ext.hdr_version = 1; hdr->ext.hdr_len = 28; /*insert non-fix values */ hdr->ext.mss = skb_shinfo(skb)->gso_size; hdr->ext.dg_hdr_len = (__u16)(iph->ihl*4 + tcph->doff*4); hdr->ext.payload_len = (__u16)(skb->len - hdr->ext.dg_hdr_len - sizeof(struct qeth_hdr_tso)); tcph->check = 0; if (skb->protocol == ETH_P_IPV6) { ip6h->payload_len = 0; tcph->check = ~csum_ipv6_magic(&ip6h->saddr, &ip6h->daddr, 0, IPPROTO_TCP, 0); } else { /*OSA want us to set these values ...*/ tcph->check = ~csum_tcpudp_magic(iph->saddr, iph->daddr, 0, IPPROTO_TCP, 0); iph->tot_len = 0; iph->check = 0; } } EXPORT_SYMBOL_GPL(qeth_tso_fill_header); void qeth_tx_csum(struct sk_buff *skb) { int tlen; if (skb->protocol == htons(ETH_P_IP)) { tlen = ntohs(ip_hdr(skb)->tot_len) - (ip_hdr(skb)->ihl << 2); switch (ip_hdr(skb)->protocol) { case IPPROTO_TCP: tcp_hdr(skb)->check = 0; tcp_hdr(skb)->check = csum_tcpudp_magic( ip_hdr(skb)->saddr, ip_hdr(skb)->daddr, tlen, ip_hdr(skb)->protocol, skb_checksum(skb, skb_transport_offset(skb), tlen, 0)); break; case IPPROTO_UDP: udp_hdr(skb)->check = 0; udp_hdr(skb)->check = csum_tcpudp_magic( ip_hdr(skb)->saddr, ip_hdr(skb)->daddr, tlen, ip_hdr(skb)->protocol, skb_checksum(skb, skb_transport_offset(skb), tlen, 0)); break; } } else if (skb->protocol == htons(ETH_P_IPV6)) { switch (ipv6_hdr(skb)->nexthdr) { case IPPROTO_TCP: tcp_hdr(skb)->check = 0; tcp_hdr(skb)->check = csum_ipv6_magic( &ipv6_hdr(skb)->saddr, &ipv6_hdr(skb)->daddr, ipv6_hdr(skb)->payload_len, ipv6_hdr(skb)->nexthdr, skb_checksum(skb, skb_transport_offset(skb), ipv6_hdr(skb)->payload_len, 0)); break; case IPPROTO_UDP: udp_hdr(skb)->check = 0; udp_hdr(skb)->check = csum_ipv6_magic( &ipv6_hdr(skb)->saddr, &ipv6_hdr(skb)->daddr, ipv6_hdr(skb)->payload_len, ipv6_hdr(skb)->nexthdr, skb_checksum(skb, skb_transport_offset(skb), ipv6_hdr(skb)->payload_len, 0)); break; } } } EXPORT_SYMBOL_GPL(qeth_tx_csum);