| // 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>"); |