| // SPDX-License-Identifier: GPL-2.0-or-later |
| /* |
| * IP Payload Compression Protocol (IPComp) - RFC3173. |
| * |
| * Copyright (c) 2003 James Morris <jmorris@intercode.com.au> |
| * Copyright (c) 2003-2025 Herbert Xu <herbert@gondor.apana.org.au> |
| * |
| * Todo: |
| * - Tunable compression parameters. |
| * - Compression stats. |
| * - Adaptive compression. |
| */ |
| |
| #include <crypto/acompress.h> |
| #include <linux/err.h> |
| #include <linux/module.h> |
| #include <linux/skbuff_ref.h> |
| #include <linux/slab.h> |
| #include <net/ipcomp.h> |
| #include <net/xfrm.h> |
| |
| #define IPCOMP_SCRATCH_SIZE 65400 |
| |
| struct ipcomp_skb_cb { |
| struct xfrm_skb_cb xfrm; |
| struct acomp_req *req; |
| }; |
| |
| struct ipcomp_data { |
| u16 threshold; |
| struct crypto_acomp *tfm; |
| }; |
| |
| struct ipcomp_req_extra { |
| struct xfrm_state *x; |
| struct scatterlist sg[]; |
| }; |
| |
| static inline struct ipcomp_skb_cb *ipcomp_cb(struct sk_buff *skb) |
| { |
| struct ipcomp_skb_cb *cb = (void *)skb->cb; |
| |
| BUILD_BUG_ON(sizeof(*cb) > sizeof(skb->cb)); |
| return cb; |
| } |
| |
| static int ipcomp_post_acomp(struct sk_buff *skb, int err, int hlen) |
| { |
| struct acomp_req *req = ipcomp_cb(skb)->req; |
| struct ipcomp_req_extra *extra; |
| struct scatterlist *dsg; |
| int len, dlen; |
| |
| if (unlikely(err)) |
| goto out_free_req; |
| |
| extra = acomp_request_extra(req); |
| dsg = extra->sg; |
| dlen = req->dlen; |
| |
| pskb_trim_unique(skb, 0); |
| __skb_put(skb, hlen); |
| |
| /* Only update truesize on input. */ |
| if (!hlen) |
| skb->truesize += dlen; |
| skb->data_len = dlen; |
| skb->len += dlen; |
| |
| do { |
| skb_frag_t *frag; |
| struct page *page; |
| |
| frag = skb_shinfo(skb)->frags + skb_shinfo(skb)->nr_frags; |
| page = sg_page(dsg); |
| dsg = sg_next(dsg); |
| |
| len = PAGE_SIZE; |
| if (dlen < len) |
| len = dlen; |
| |
| skb_frag_fill_page_desc(frag, page, 0, len); |
| |
| skb_shinfo(skb)->nr_frags++; |
| } while ((dlen -= len)); |
| |
| for (; dsg; dsg = sg_next(dsg)) |
| __free_page(sg_page(dsg)); |
| |
| out_free_req: |
| acomp_request_free(req); |
| return err; |
| } |
| |
| static int ipcomp_input_done2(struct sk_buff *skb, int err) |
| { |
| struct ip_comp_hdr *ipch = ip_comp_hdr(skb); |
| const int plen = skb->len; |
| |
| skb->transport_header = skb->network_header + sizeof(*ipch); |
| |
| return ipcomp_post_acomp(skb, err, 0) ?: |
| skb->len < (plen + sizeof(ip_comp_hdr)) ? -EINVAL : |
| ipch->nexthdr; |
| } |
| |
| static void ipcomp_input_done(void *data, int err) |
| { |
| struct sk_buff *skb = data; |
| |
| xfrm_input_resume(skb, ipcomp_input_done2(skb, err)); |
| } |
| |
| static struct acomp_req *ipcomp_setup_req(struct xfrm_state *x, |
| struct sk_buff *skb, int minhead, |
| int dlen) |
| { |
| const int dnfrags = min(MAX_SKB_FRAGS, 16); |
| struct ipcomp_data *ipcd = x->data; |
| struct ipcomp_req_extra *extra; |
| struct scatterlist *sg, *dsg; |
| const int plen = skb->len; |
| struct crypto_acomp *tfm; |
| struct acomp_req *req; |
| int nfrags; |
| int total; |
| int err; |
| int i; |
| |
| ipcomp_cb(skb)->req = NULL; |
| |
| do { |
| struct sk_buff *trailer; |
| |
| if (skb->len > PAGE_SIZE) { |
| if (skb_linearize_cow(skb)) |
| return ERR_PTR(-ENOMEM); |
| nfrags = 1; |
| break; |
| } |
| |
| if (!skb_cloned(skb) && skb_headlen(skb) >= minhead) { |
| if (!skb_is_nonlinear(skb)) { |
| nfrags = 1; |
| break; |
| } else if (!skb_has_frag_list(skb)) { |
| nfrags = skb_shinfo(skb)->nr_frags; |
| nfrags++; |
| break; |
| } |
| } |
| |
| nfrags = skb_cow_data(skb, skb_headlen(skb) < minhead ? |
| minhead - skb_headlen(skb) : 0, |
| &trailer); |
| if (nfrags < 0) |
| return ERR_PTR(nfrags); |
| } while (0); |
| |
| tfm = ipcd->tfm; |
| req = acomp_request_alloc_extra( |
| tfm, sizeof(*extra) + sizeof(*sg) * (nfrags + dnfrags), |
| GFP_ATOMIC); |
| ipcomp_cb(skb)->req = req; |
| if (!req) |
| return ERR_PTR(-ENOMEM); |
| |
| extra = acomp_request_extra(req); |
| extra->x = x; |
| |
| dsg = extra->sg; |
| sg = dsg + dnfrags; |
| sg_init_table(sg, nfrags); |
| err = skb_to_sgvec(skb, sg, 0, plen); |
| if (unlikely(err < 0)) |
| return ERR_PTR(err); |
| |
| sg_init_table(dsg, dnfrags); |
| total = 0; |
| for (i = 0; i < dnfrags && total < dlen; i++) { |
| struct page *page; |
| |
| page = alloc_page(GFP_ATOMIC); |
| if (!page) |
| break; |
| sg_set_page(dsg + i, page, PAGE_SIZE, 0); |
| total += PAGE_SIZE; |
| } |
| if (!i) |
| return ERR_PTR(-ENOMEM); |
| sg_mark_end(dsg + i - 1); |
| dlen = min(dlen, total); |
| |
| acomp_request_set_params(req, sg, dsg, plen, dlen); |
| |
| return req; |
| } |
| |
| static int ipcomp_decompress(struct xfrm_state *x, struct sk_buff *skb) |
| { |
| struct acomp_req *req; |
| int err; |
| |
| req = ipcomp_setup_req(x, skb, 0, IPCOMP_SCRATCH_SIZE); |
| err = PTR_ERR(req); |
| if (IS_ERR(req)) |
| goto out; |
| |
| acomp_request_set_callback(req, 0, ipcomp_input_done, skb); |
| err = crypto_acomp_decompress(req); |
| if (err == -EINPROGRESS) |
| return err; |
| |
| out: |
| return ipcomp_input_done2(skb, err); |
| } |
| |
| int ipcomp_input(struct xfrm_state *x, struct sk_buff *skb) |
| { |
| struct ip_comp_hdr *ipch __maybe_unused; |
| |
| if (!pskb_may_pull(skb, sizeof(*ipch))) |
| return -EINVAL; |
| |
| skb->ip_summed = CHECKSUM_NONE; |
| |
| /* Remove ipcomp header and decompress original payload */ |
| __skb_pull(skb, sizeof(*ipch)); |
| |
| return ipcomp_decompress(x, skb); |
| } |
| EXPORT_SYMBOL_GPL(ipcomp_input); |
| |
| static int ipcomp_output_push(struct sk_buff *skb) |
| { |
| skb_push(skb, -skb_network_offset(skb)); |
| return 0; |
| } |
| |
| static int ipcomp_output_done2(struct xfrm_state *x, struct sk_buff *skb, |
| int err) |
| { |
| struct ip_comp_hdr *ipch; |
| |
| err = ipcomp_post_acomp(skb, err, sizeof(*ipch)); |
| if (err) |
| goto out_ok; |
| |
| /* Install ipcomp header, convert into ipcomp datagram. */ |
| ipch = ip_comp_hdr(skb); |
| ipch->nexthdr = *skb_mac_header(skb); |
| ipch->flags = 0; |
| ipch->cpi = htons((u16 )ntohl(x->id.spi)); |
| *skb_mac_header(skb) = IPPROTO_COMP; |
| out_ok: |
| return ipcomp_output_push(skb); |
| } |
| |
| static void ipcomp_output_done(void *data, int err) |
| { |
| struct ipcomp_req_extra *extra; |
| struct sk_buff *skb = data; |
| struct acomp_req *req; |
| |
| req = ipcomp_cb(skb)->req; |
| extra = acomp_request_extra(req); |
| |
| xfrm_output_resume(skb_to_full_sk(skb), skb, |
| ipcomp_output_done2(extra->x, skb, err)); |
| } |
| |
| static int ipcomp_compress(struct xfrm_state *x, struct sk_buff *skb) |
| { |
| struct ip_comp_hdr *ipch __maybe_unused; |
| struct acomp_req *req; |
| int err; |
| |
| req = ipcomp_setup_req(x, skb, sizeof(*ipch), |
| skb->len - sizeof(*ipch)); |
| err = PTR_ERR(req); |
| if (IS_ERR(req)) |
| goto out; |
| |
| acomp_request_set_callback(req, 0, ipcomp_output_done, skb); |
| err = crypto_acomp_compress(req); |
| if (err == -EINPROGRESS) |
| return err; |
| |
| out: |
| return ipcomp_output_done2(x, skb, err); |
| } |
| |
| int ipcomp_output(struct xfrm_state *x, struct sk_buff *skb) |
| { |
| struct ipcomp_data *ipcd = x->data; |
| |
| if (skb->len < ipcd->threshold) { |
| /* Don't bother compressing */ |
| return ipcomp_output_push(skb); |
| } |
| |
| return ipcomp_compress(x, skb); |
| } |
| EXPORT_SYMBOL_GPL(ipcomp_output); |
| |
| static void ipcomp_free_data(struct ipcomp_data *ipcd) |
| { |
| crypto_free_acomp(ipcd->tfm); |
| } |
| |
| void ipcomp_destroy(struct xfrm_state *x) |
| { |
| struct ipcomp_data *ipcd = x->data; |
| if (!ipcd) |
| return; |
| ipcomp_free_data(ipcd); |
| kfree(ipcd); |
| } |
| EXPORT_SYMBOL_GPL(ipcomp_destroy); |
| |
| int ipcomp_init_state(struct xfrm_state *x, struct netlink_ext_ack *extack) |
| { |
| int err; |
| struct ipcomp_data *ipcd; |
| struct xfrm_algo_desc *calg_desc; |
| |
| err = -EINVAL; |
| if (!x->calg) { |
| NL_SET_ERR_MSG(extack, "Missing required compression algorithm"); |
| goto out; |
| } |
| |
| if (x->encap) { |
| NL_SET_ERR_MSG(extack, "IPComp is not compatible with encapsulation"); |
| goto out; |
| } |
| |
| err = -ENOMEM; |
| ipcd = kzalloc(sizeof(*ipcd), GFP_KERNEL); |
| if (!ipcd) |
| goto out; |
| |
| ipcd->tfm = crypto_alloc_acomp(x->calg->alg_name, 0, 0); |
| if (IS_ERR(ipcd->tfm)) |
| goto error; |
| |
| calg_desc = xfrm_calg_get_byname(x->calg->alg_name, 0); |
| BUG_ON(!calg_desc); |
| ipcd->threshold = calg_desc->uinfo.comp.threshold; |
| x->data = ipcd; |
| err = 0; |
| out: |
| return err; |
| |
| error: |
| ipcomp_free_data(ipcd); |
| kfree(ipcd); |
| goto out; |
| } |
| EXPORT_SYMBOL_GPL(ipcomp_init_state); |
| |
| MODULE_LICENSE("GPL"); |
| MODULE_DESCRIPTION("IP Payload Compression Protocol (IPComp) - RFC3173"); |
| MODULE_AUTHOR("James Morris <jmorris@intercode.com.au>"); |