root/sys/netinet/ip_ipcomp.c
/* $OpenBSD: ip_ipcomp.c,v 1.96 2025/12/11 05:06:02 dlg Exp $ */

/*
 * Copyright (c) 2001 Jean-Jacques Bernard-Gundol (jj@wabbitt.org)
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *   derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/* IP payload compression protocol (IPComp), see RFC 2393 */

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/mbuf.h>
#include <sys/socket.h>

#include <net/if.h>
#include <net/if_var.h>
#include <net/bpf.h>

#include <netinet/in.h>
#include <netinet/ip.h>

#ifdef INET6
#include <netinet/ip6.h>
#endif                          /* INET6 */

#include <netinet/ip_ipsp.h>
#include <netinet/ip_ipcomp.h>
#include <net/pfkeyv2.h>
#include <net/if_enc.h>

#include <crypto/cryptodev.h>
#include <crypto/xform.h>

#include "bpfilter.h"

#ifdef ENCDEBUG
#define DPRINTF(fmt, args...)                                           \
        do {                                                            \
                if (atomic_load_int(&encdebug))                         \
                        printf("%s: " fmt "\n", __func__, ## args);     \
        } while (0)
#else
#define DPRINTF(fmt, args...)                                           \
        do { } while (0)
#endif

/*
 * ipcomp_attach() is called from the transformation code
 */
int
ipcomp_attach(void)
{
        return 0;
}

/*
 * ipcomp_init() is called when an CPI is being set up.
 */
int
ipcomp_init(struct tdb *tdbp, const struct xformsw *xsp, struct ipsecinit *ii)
{
        const struct comp_algo *tcomp = NULL;
        struct cryptoini cric;
        int error;

        switch (ii->ii_compalg) {
        case SADB_X_CALG_DEFLATE:
                tcomp = &comp_algo_deflate;
                break;
        default:
                DPRINTF("unsupported compression algorithm %d specified",
                    ii->ii_compalg);
                return EINVAL;
        }

        tdbp->tdb_compalgxform = tcomp;

        DPRINTF("initialized TDB with ipcomp algorithm %s", tcomp->name);

        tdbp->tdb_xform = xsp;

        /* Initialize crypto session */
        memset(&cric, 0, sizeof(cric));
        cric.cri_alg = tdbp->tdb_compalgxform->type;

        KERNEL_LOCK();
        error = crypto_newsession(&tdbp->tdb_cryptoid, &cric, 0);
        KERNEL_UNLOCK();
        return error;
}

/*
 * ipcomp_zeroize() used when IPCA is deleted
 */
int
ipcomp_zeroize(struct tdb *tdbp)
{
        int error;

        KERNEL_LOCK();
        error = crypto_freesession(tdbp->tdb_cryptoid);
        KERNEL_UNLOCK();
        tdbp->tdb_cryptoid = 0;
        return error;
}

/*
 * ipcomp_input() gets called to uncompress an input packet
 */
int
ipcomp_input(struct mbuf **mp, struct tdb *tdb, int skip, int protoff,
    struct netstack *ns)
{
        const struct comp_algo *ipcompx = tdb->tdb_compalgxform;
        struct mbuf *m = *mp;
        struct cryptodesc *crdc = NULL;
        struct cryptop *crp;
        int hlen, error, clen, roff;
        u_int8_t nproto;
        u_int64_t ibytes;
        struct mbuf *m1, *mo;
        struct ipcomp  *ipcomp;
        caddr_t addr;
#ifdef ENCDEBUG
        char buf[INET6_ADDRSTRLEN];
#endif

        hlen = IPCOMP_HLENGTH;

        /* Get crypto descriptors */
        crp = crypto_getreq(1);
        if (crp == NULL) {
                DPRINTF("failed to acquire crypto descriptors");
                ipcompstat_inc(ipcomps_crypto);
                goto drop;
        }
        crdc = &crp->crp_desc[0];

        crdc->crd_skip = skip + hlen;
        crdc->crd_len = m->m_pkthdr.len - (skip + hlen);
        crdc->crd_inject = skip;

        /* Decompression operation */
        crdc->crd_alg = ipcompx->type;

        /* Crypto operation descriptor */
        crp->crp_ilen = m->m_pkthdr.len - (skip + hlen);
        crp->crp_flags = CRYPTO_F_IMBUF;
        crp->crp_buf = (caddr_t)m;
        crp->crp_sid = tdb->tdb_cryptoid;

        while ((error = crypto_invoke(crp)) == EAGAIN) {
                /* Reset the session ID */
                if (tdb->tdb_cryptoid != 0)
                        tdb->tdb_cryptoid = crp->crp_sid;
        }
        if (error) {
                DPRINTF("crypto error %d", error);
                ipsecstat_inc(ipsec_noxform);
                goto drop;
        }

        clen = crp->crp_olen;

        /* Release the crypto descriptors */
        crypto_freereq(crp);
        crp = NULL;

        /* update the counters */
        ibytes = m->m_pkthdr.len - (skip + hlen);
        tdb->tdb_cur_bytes += ibytes;
        tdbstat_add(tdb, tdb_ibytes, ibytes);
        ipcompstat_add(ipcomps_ibytes, ibytes);

        /* Hard expiration */
        if ((tdb->tdb_flags & TDBF_BYTES) &&
            (tdb->tdb_cur_bytes >= tdb->tdb_exp_bytes)) {
                ipsecstat_inc(ipsec_exctdb);
                pfkeyv2_expire(tdb, SADB_EXT_LIFETIME_HARD);
                tdb_delete(tdb);
                goto drop;
        }
        /* Notify on soft expiration */
        mtx_enter(&tdb->tdb_mtx);
        if ((tdb->tdb_flags & TDBF_SOFT_BYTES) &&
            (tdb->tdb_cur_bytes >= tdb->tdb_soft_bytes)) {
                tdb->tdb_flags &= ~TDBF_SOFT_BYTES;  /* Turn off checking */
                mtx_leave(&tdb->tdb_mtx);
                /* may sleep in solock() for the pfkey socket */
                pfkeyv2_expire(tdb, SADB_EXT_LIFETIME_SOFT);
        } else
                mtx_leave(&tdb->tdb_mtx);

        /* In case it's not done already, adjust the size of the mbuf chain */
        m->m_pkthdr.len = clen + hlen + skip;

        if (m->m_len < skip + hlen &&
            (m = *mp = m_pullup(m, skip + hlen)) == NULL) {
                ipcompstat_inc(ipcomps_hdrops);
                goto drop;
        }

        /* Find the beginning of the IPCOMP header */
        m1 = m_getptr(m, skip, &roff);
        if (m1 == NULL) {
                DPRINTF("bad mbuf chain, IPCA %s/%08x",
                    ipsp_address(&tdb->tdb_dst, buf, sizeof(buf)),
                    ntohl(tdb->tdb_spi));
                ipcompstat_inc(ipcomps_hdrops);
                goto drop;
        }
        /* Keep the next protocol field */
        addr = (caddr_t) mtod(m, struct ip *) + skip;
        ipcomp = (struct ipcomp *) addr;
        nproto = ipcomp->ipcomp_nh;

        /* Remove the IPCOMP header from the mbuf */
        if (roff == 0) {
                /* The IPCOMP header is at the beginning of m1 */
                m_adj(m1, hlen);
                /*
                 * If m1 is the first mbuf, it has set M_PKTHDR and m_adj()
                 * has already adjusted the packet header length for us.
                 */
                if (m1 != m)
                        m->m_pkthdr.len -= hlen;
        } else if (roff + hlen >= m1->m_len) {
                int adjlen;

                if (roff + hlen > m1->m_len) {
                        adjlen = roff + hlen - m1->m_len;

                        /* Adjust the next mbuf by the remainder */
                        m_adj(m1->m_next, adjlen);

                        /*
                         * The second mbuf is guaranteed not to have a
                         * pkthdr...
                         */
                        m->m_pkthdr.len -= adjlen;
                }
                /* Now, let's unlink the mbuf chain for a second... */
                mo = m1->m_next;
                m1->m_next = NULL;

                /* ...and trim the end of the first part of the chain...sick */
                adjlen = m1->m_len - roff;
                m_adj(m1, -adjlen);
                /*
                 * If m1 is the first mbuf, it has set M_PKTHDR and m_adj()
                 * has already adjusted the packet header length for us.
                 */
                if (m1 != m)
                        m->m_pkthdr.len -= adjlen;

                /* Finally, let's relink */
                m1->m_next = mo;
        } else {
                memmove(mtod(m1, u_char *) + roff,
                    mtod(m1, u_char *) + roff + hlen,
                    m1->m_len - (roff + hlen));
                m1->m_len -= hlen;
                m->m_pkthdr.len -= hlen;
        }

        /* Restore the Next Protocol field */
        m_copyback(m, protoff, sizeof(u_int8_t), &nproto, M_NOWAIT);

        /* Back to generic IPsec input processing */
        return ipsec_common_input_cb(mp, tdb, skip, protoff, ns);

 drop:
        m_freemp(mp);
        crypto_freereq(crp);
        return IPPROTO_DONE;
}

/*
 * IPComp output routine, called by ipsp_process_packet()
 */
int
ipcomp_output(struct mbuf *m, struct tdb *tdb, int skip, int protoff)
{
        const struct comp_algo *ipcompx = tdb->tdb_compalgxform;
        int error, hlen, ilen, olen, rlen, roff;
        struct cryptodesc *crdc = NULL;
        struct cryptop *crp = NULL;
        struct mbuf *mi, *mo;
        struct ip *ip;
        u_int16_t cpi;
#ifdef INET6
        struct ip6_hdr *ip6;
#endif
#ifdef ENCDEBUG
        char buf[INET6_ADDRSTRLEN];
#endif
#if NBPFILTER > 0
        struct ifnet *encif;
        struct ipcomp  *ipcomp;

        if ((encif = enc_getif(0, tdb->tdb_tap)) != NULL) {
                encif->if_opackets++;
                encif->if_obytes += m->m_pkthdr.len;

                if (encif->if_bpf) {
                        struct enchdr hdr = {
                                .af = htonl(tdb->tdb_dst.sa.sa_family),
                                .spi = tdb->tdb_spi,
                        };

                        bpf_mtap_hdr(encif->if_bpf, (char *)&hdr,
                            ENC_HDRLEN, m, BPF_DIRECTION_OUT);
                }
        }
#endif
        hlen = IPCOMP_HLENGTH;

        ipcompstat_inc(ipcomps_output);

        switch (tdb->tdb_dst.sa.sa_family) {
        case AF_INET:
                /* Check for IPv4 maximum packet size violations */
                /*
                 * Since compression is going to reduce the size, no need to
                 * worry
                 */
                if (m->m_pkthdr.len + hlen > IP_MAXPACKET) {
                        DPRINTF("packet in IPCA %s/%08x got too big",
                            ipsp_address(&tdb->tdb_dst, buf, sizeof(buf)),
                            ntohl(tdb->tdb_spi));
                        ipcompstat_inc(ipcomps_toobig);
                        error = EMSGSIZE;
                        goto drop;
                }
                break;

#ifdef INET6
        case AF_INET6:
                /* Check for IPv6 maximum packet size violations */
                if (m->m_pkthdr.len + hlen > IPV6_MAXPACKET) {
                        DPRINTF("packet in IPCA %s/%08x got too big",
                            ipsp_address(&tdb->tdb_dst, buf, sizeof(buf)),
                            ntohl(tdb->tdb_spi));
                        ipcompstat_inc(ipcomps_toobig);
                        error = EMSGSIZE;
                        goto drop;
                }
                break;
#endif /* INET6 */

        default:
                DPRINTF("unknown/unsupported protocol family %d, IPCA %s/%08x",
                    tdb->tdb_dst.sa.sa_family,
                    ipsp_address(&tdb->tdb_dst, buf, sizeof(buf)),
                    ntohl(tdb->tdb_spi));
                ipcompstat_inc(ipcomps_nopf);
                error = EPFNOSUPPORT;
                goto drop;
        }

        /* Update the counters */
        tdb->tdb_cur_bytes += m->m_pkthdr.len - skip;
        ipcompstat_add(ipcomps_obytes, m->m_pkthdr.len - skip);

        /* Hard byte expiration */
        if ((tdb->tdb_flags & TDBF_BYTES) &&
            (tdb->tdb_cur_bytes >= tdb->tdb_exp_bytes)) {
                ipsecstat_inc(ipsec_exctdb);
                pfkeyv2_expire(tdb, SADB_EXT_LIFETIME_HARD);
                tdb_delete(tdb);
                error = EINVAL;
                goto drop;
        }

        /* Soft byte expiration */
        mtx_enter(&tdb->tdb_mtx);
        if ((tdb->tdb_flags & TDBF_SOFT_BYTES) &&
            (tdb->tdb_cur_bytes >= tdb->tdb_soft_bytes)) {
                tdb->tdb_flags &= ~TDBF_SOFT_BYTES;  /* Turn off checking */
                mtx_leave(&tdb->tdb_mtx);
                /* may sleep in solock() for the pfkey socket */
                pfkeyv2_expire(tdb, SADB_EXT_LIFETIME_SOFT);
        } else
                mtx_leave(&tdb->tdb_mtx);

        /*
         * Loop through mbuf chain; if we find a readonly mbuf,
         * copy the packet.
         */
        mi = m;
        while (mi != NULL && !M_READONLY(mi))
                mi = mi->m_next;

        if (mi != NULL) {
                struct mbuf *n = m_dup_pkt(m, 0, M_DONTWAIT);

                if (n == NULL) {
                        DPRINTF("bad mbuf chain, IPCA %s/%08x",
                            ipsp_address(&tdb->tdb_dst, buf, sizeof(buf)),
                            ntohl(tdb->tdb_spi));
                        ipcompstat_inc(ipcomps_hdrops);
                        error = ENOBUFS;
                        goto drop;
                }

                m_freem(m);
                m = n;
        }
        /* Ok now, we can pass to the crypto processing */

        /* Get crypto descriptors */
        crp = crypto_getreq(1);
        if (crp == NULL) {
                DPRINTF("failed to acquire crypto descriptors");
                ipcompstat_inc(ipcomps_crypto);
                error = ENOBUFS;
                goto drop;
        }
        crdc = &crp->crp_desc[0];

        /* Compression descriptor */
        crdc->crd_skip = skip;
        crdc->crd_len = m->m_pkthdr.len - skip;
        crdc->crd_flags = CRD_F_COMP;
        crdc->crd_inject = skip;

        /* Compression operation */
        crdc->crd_alg = ipcompx->type;

        /* Crypto operation descriptor */
        crp->crp_ilen = m->m_pkthdr.len;        /* Total input length */
        crp->crp_flags = CRYPTO_F_IMBUF;
        crp->crp_buf = (caddr_t)m;
        crp->crp_sid = tdb->tdb_cryptoid;

        while ((error = crypto_invoke(crp)) == EAGAIN) {
                /* Reset the session ID */
                if (tdb->tdb_cryptoid != 0)
                        tdb->tdb_cryptoid = crp->crp_sid;
        }
        if (error) {
                DPRINTF("crypto error %d", error);
                ipsecstat_inc(ipsec_noxform);
                goto drop;
        }

        ilen = crp->crp_ilen;
        olen = crp->crp_olen;

        /* Release the crypto descriptors */
        crypto_freereq(crp);
        crp = NULL;

        rlen = ilen - skip;

        /* Check sizes. */
        if (rlen <= olen + IPCOMP_HLENGTH) {
                /* Compression was useless, we have lost time. */
                ipcompstat_inc(ipcomps_minlen); /* misnomer, but like to count */
                goto skiphdr;
        }

        /* Inject IPCOMP header */
        mo = m_makespace(m, skip, IPCOMP_HLENGTH, &roff);
        if (mo == NULL) {
                DPRINTF("ailed to inject IPCOMP header for IPCA %s/%08x",
                    ipsp_address(&tdb->tdb_dst, buf, sizeof(buf)),
                    ntohl(tdb->tdb_spi));
                ipcompstat_inc(ipcomps_wrap);
                error = ENOBUFS;
                goto drop;
        }

        /* Initialize the IPCOMP header */
        ipcomp = (struct ipcomp *)(mtod(mo, caddr_t) + roff);
        memset(ipcomp, 0, sizeof(struct ipcomp));
        cpi = (u_int16_t) ntohl(tdb->tdb_spi);
        ipcomp->ipcomp_cpi = htons(cpi);

        /* m_pullup before ? */
        switch (tdb->tdb_dst.sa.sa_family) {
        case AF_INET:
                ip = mtod(m, struct ip *);
                ipcomp->ipcomp_nh = ip->ip_p;
                ip->ip_p = IPPROTO_IPCOMP;
                break;
#ifdef INET6
        case AF_INET6:
                ip6 = mtod(m, struct ip6_hdr *);
                ipcomp->ipcomp_nh = ip6->ip6_nxt;
                ip6->ip6_nxt = IPPROTO_IPCOMP;
                break;
#endif
        default:
                DPRINTF("unsupported protocol family %d, IPCA %s/%08x",
                    tdb->tdb_dst.sa.sa_family,
                    ipsp_address(&tdb->tdb_dst, buf, sizeof(buf)),
                    ntohl(tdb->tdb_spi));
                ipcompstat_inc(ipcomps_nopf);
                error = EPFNOSUPPORT;
                goto drop;
        }

 skiphdr:
        error = ipsp_process_done(m, tdb);
        if (error)
                ipcompstat_inc(ipcomps_outfail);
        return error;

 drop:
        m_freem(m);
        crypto_freereq(crp);
        return error;
}