/* SPDX-License-Identifier: BSD-3-Clause */
/*
 * Copyright (c) 1982, 1986, 1988, 1993
 *	The Regents of the University of California.  All rights reserved.
 *
 * 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. Neither the name of the University nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``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 REGENTS OR CONTRIBUTORS 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_icmp.c	8.2 (Berkeley) 1/4/94
 * ip_icmp.c,v 1.7 1995/05/30 08:09:42 rgrimes Exp
 */

#include "slirp.h"
#include "ip_icmp.h"

#ifndef _WIN32
#include <sys/param.h>
#endif

#ifndef WITH_ICMP_ERROR_MSG
#define WITH_ICMP_ERROR_MSG 0
#endif

/* The message sent when emulating PING */
/* Be nice and tell them it's just a pseudo-ping packet */
static const char icmp_ping_msg[] =
    "This is a pseudo-PING packet used by Slirp to emulate ICMP ECHO-REQUEST "
    "packets.\n";

/* list of actions for icmp_send_error() on RX of an icmp message */
static const int icmp_flush[19] = {
    /*  ECHO REPLY (0)  */ 0,
    1,
    1,
    /* DEST UNREACH (3) */ 1,
    /* SOURCE QUENCH (4)*/ 1,
    /* REDIRECT (5) */ 1,
    1,
    1,
    /* ECHO (8) */ 0,
    /* ROUTERADVERT (9) */ 1,
    /* ROUTERSOLICIT (10) */ 1,
    /* TIME EXCEEDED (11) */ 1,
    /* PARAMETER PROBLEM (12) */ 1,
    /* TIMESTAMP (13) */ 0,
    /* TIMESTAMP REPLY (14) */ 0,
    /* INFO (15) */ 0,
    /* INFO REPLY (16) */ 0,
    /* ADDR MASK (17) */ 0,
    /* ADDR MASK REPLY (18) */ 0
};

void icmp_init(Slirp *slirp)
{
    slirp->icmp.so_next = slirp->icmp.so_prev = &slirp->icmp;
    slirp->icmp_last_so = &slirp->icmp;
}

void icmp_cleanup(Slirp *slirp)
{
    struct socket *so, *so_next;

    for (so = slirp->icmp.so_next; so != &slirp->icmp; so = so_next) {
        so_next = so->so_next;
        icmp_detach(so);
    }
}

/* Send ICMP packet to the Internet, and save it to so_m */
static int icmp_send(struct socket *so, struct mbuf *m, int hlen)
{
    Slirp *slirp = m->slirp;

    struct sockaddr_in addr;

    /*
     * The behavior of reading SOCK_DGRAM+IPPROTO_ICMP sockets is inconsistent
     * between host OSes.  On Linux, only the ICMP header and payload is
     * included.  On macOS/Darwin, the socket acts like a raw socket and
     * includes the IP header as well.  On other BSDs, SOCK_DGRAM+IPPROTO_ICMP
     * sockets aren't supported at all, so we treat them like raw sockets.  It
     * isn't possible to detect this difference at runtime, so we must use an
     * #ifdef to determine if we need to remove the IP header.
     */
#if defined(BSD) && !defined(__GNU__)
    so->so_type = IPPROTO_IP;
#else
    so->so_type = IPPROTO_ICMP;
#endif

    so->s = slirp_socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);
    if (so->s == -1) {
        if (errno == EAFNOSUPPORT
         || errno == EPROTONOSUPPORT
         || errno == EACCES) {
            /* Kernel doesn't support or allow ping sockets. */
            so->so_type = IPPROTO_IP;
            so->s = slirp_socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
        }
    }
    if (so->s == -1) {
        return -1;
    }
    so->slirp->cb->register_poll_fd(so->s, so->slirp->opaque);

    if (slirp_bind_outbound(so, AF_INET) != 0) {
        // bind failed - close socket
        closesocket(so->s);
        so->s = -1;
        return -1;
    }

    M_DUP_DEBUG(slirp, m, 0, 0);
    struct ip *ip = mtod(m, struct ip *);

    so->so_m = m;
    so->so_faddr = ip->ip_dst;
    so->so_laddr = ip->ip_src;
    so->so_iptos = ip->ip_tos;
    so->so_state = SS_ISFCONNECTED;
    so->so_expire = curtime + SO_EXPIRE;

    addr.sin_family = AF_INET;
    addr.sin_addr = so->so_faddr;

    slirp_insque(so, &so->slirp->icmp);

    if (sendto(so->s, m->m_data + hlen, m->m_len - hlen, 0,
               (struct sockaddr *)&addr, sizeof(addr)) == -1) {
        DEBUG_MISC("icmp_input icmp sendto tx errno = %d-%s", errno,
                   strerror(errno));
        icmp_send_error(m, ICMP_UNREACH, ICMP_UNREACH_NET, 0, strerror(errno));
        icmp_detach(so);
    }

    return 0;
}

void icmp_detach(struct socket *so)
{
    so->slirp->cb->unregister_poll_fd(so->s, so->slirp->opaque);
    closesocket(so->s);
    sofree(so);
}

/*
 * Process a received ICMP message.
 */
void icmp_input(struct mbuf *m, int hlen)
{
    Slirp *slirp = m->slirp;
    M_DUP_DEBUG(slirp, m, 0, 0);

    register struct icmp *icp;
    register struct ip *ip = mtod(m, struct ip *);
    int icmplen = ip->ip_len;

    DEBUG_CALL("icmp_input");
    DEBUG_ARG("m = %p", m);
    DEBUG_ARG("m_len = %d", m->m_len);

    /*
     * Locate icmp structure in mbuf, and check
     * that its not corrupted and of at least minimum length.
     */
    if (icmplen < ICMP_MINLEN) { /* min 8 bytes payload */
    freeit:
        m_free(m);
        goto end_error;
    }

    m->m_len -= hlen;
    m->m_data += hlen;
    icp = mtod(m, struct icmp *);
    if (cksum(m, icmplen)) {
        goto freeit;
    }
    m->m_len += hlen;
    m->m_data -= hlen;

    DEBUG_ARG("icmp_type = %d", icp->icmp_type);
    switch (icp->icmp_type) {
    case ICMP_ECHO:
        ip->ip_len += hlen; /* since ip_input subtracts this */
        if (ip->ip_dst.s_addr == slirp->vhost_addr.s_addr ||
            ip->ip_dst.s_addr == slirp->vnameserver_addr.s_addr) {
            icmp_reflect(m);
        } else if (slirp->restricted) {
            goto freeit;
        } else {
            struct socket *so;
            struct sockaddr_storage addr;
            int ttl;

            so = socreate(slirp, IPPROTO_ICMP);
            if (icmp_send(so, m, hlen) == 0) {
                /* We could send this as ICMP, good! */
                return;
            }

            /* We could not send this as ICMP, try to send it on UDP echo
             * service (7), wishfully hoping that it is open there. */

            if (udp_attach(so, AF_INET) == -1) {
                DEBUG_MISC("icmp_input udp_attach errno = %d-%s", errno,
                           strerror(errno));
                sofree(so);
                m_free(m);
                goto end_error;
            }
            so->so_m = m;
            so->so_ffamily = AF_INET;
            so->so_faddr = ip->ip_dst;
            so->so_fport = htons(7);
            so->so_lfamily = AF_INET;
            so->so_laddr = ip->ip_src;
            so->so_lport = htons(9);
            so->so_iptos = ip->ip_tos;
            so->so_state = SS_ISFCONNECTED;

            /* Send the packet */
            addr = so->fhost.ss;
            if (sotranslate_out(so, &addr) < 0) {
                icmp_send_error(m, ICMP_UNREACH, ICMP_UNREACH_NET, 0,
                                strerror(errno));
                udp_detach(so);
                return;
            }

            /*
             * Check for TTL
             */
            ttl = ip->ip_ttl-1;
            if (ttl <= 0) {
                DEBUG_MISC("udp ttl exceeded");
                icmp_send_error(m, ICMP_TIMXCEED, ICMP_TIMXCEED_INTRANS, 0,
                                NULL);
                udp_detach(so);
                break;
            }
            setsockopt(so->s, IPPROTO_IP, IP_TTL, &ttl, sizeof(ttl));

            if (sendto(so->s, icmp_ping_msg, strlen(icmp_ping_msg), 0,
                       (struct sockaddr *)&addr, sockaddr_size(&addr)) == -1) {
                DEBUG_MISC("icmp_input udp sendto tx errno = %d-%s", errno,
                           strerror(errno));
                icmp_send_error(m, ICMP_UNREACH, ICMP_UNREACH_NET, 0,
                                strerror(errno));
                udp_detach(so);
            }
        } /* if ip->ip_dst.s_addr == alias_addr.s_addr */
        break;
    case ICMP_UNREACH:
        /* XXX? report error? close socket? */
    case ICMP_TIMXCEED:
    case ICMP_PARAMPROB:
    case ICMP_SOURCEQUENCH:
    case ICMP_TSTAMP:
    case ICMP_MASKREQ:
    case ICMP_REDIRECT:
        m_free(m);
        break;

    default:
        m_free(m);
    } /* switch */

end_error:
    /* m is m_free()'d xor put in a socket xor or given to ip_send */
    return;
}


/*
 *	Send an ICMP message in response to a situation
 *
 *	RFC 1122: 3.2.2	MUST send at least the IP header and 8 bytes of header.
 *MAY send more (we do). MUST NOT change this header information. MUST NOT reply
 *to a multicast/broadcast IP address. MUST NOT reply to a multicast/broadcast
 *MAC address. MUST reply to only the first fragment.
 */
/*
 * Send ICMP_UNREACH back to the source regarding msrc.
 * mbuf *msrc is used as a template, but is NOT m_free()'d.
 * It is reported as the bad ip packet.  The header should
 * be fully correct and in host byte order.
 * ICMP fragmentation is illegal.  All machines must accept 576 bytes in one
 * packet.  The maximum payload is 576-20(ip hdr)-8(icmp hdr)=548
 */

#define ICMP_MAXDATALEN (IP_MSS - 28)
void icmp_forward_error(struct mbuf *msrc, uint8_t type, uint8_t code, int minsize,
                        const char *message, struct in_addr *src)
{
    unsigned hlen, shlen, s_ip_len;
    register struct ip *ip;
    register struct icmp *icp;
    register struct mbuf *m;

    DEBUG_CALL("icmp_send_error");
    DEBUG_ARG("msrc = %p", msrc);
    DEBUG_ARG("msrc_len = %d", msrc->m_len);

    if (type != ICMP_UNREACH && type != ICMP_TIMXCEED)
        goto end_error;

    /* check msrc */
    if (!msrc)
        goto end_error;
    ip = mtod(msrc, struct ip *);
#ifndef VBOX
    if (slirp_debug & DBG_MISC) {
#else
    if (DEBUG_IS_MISC_ENABLED()) {
#endif
        char addr_src[INET_ADDRSTRLEN];
        char addr_dst[INET_ADDRSTRLEN];

        inet_ntop(AF_INET, &ip->ip_src, addr_src, sizeof(addr_src));
        inet_ntop(AF_INET, &ip->ip_dst, addr_dst, sizeof(addr_dst));
        DEBUG_MISC(" %.16s to %.16s", addr_src, addr_dst);
    }
    if (ip->ip_off & IP_OFFMASK)
        goto end_error; /* Only reply to fragment 0 */

    /* Do not reply to source-only IPs */
    if ((ip->ip_src.s_addr & htonl(~(0xf << 28))) == 0) {
        goto end_error;
    }

    shlen = ip->ip_hl << 2;
    s_ip_len = ip->ip_len;
    if (ip->ip_p == IPPROTO_ICMP) {
        icp = (struct icmp *)((char *)ip + shlen);
        /*
         *	Assume any unknown ICMP type is an error. This isn't
         *	specified by the RFC, but think about it..
         */
        if (icp->icmp_type > 18 || icmp_flush[icp->icmp_type])
            goto end_error;
    }

    /* make a copy */
    m = m_get(msrc->slirp);
    if (!m) {
        goto end_error;
    }

    {
        int new_m_size;
        new_m_size =
            sizeof(struct ip) + ICMP_MINLEN + msrc->m_len + ICMP_MAXDATALEN;
        if (new_m_size > m->m_size)
            m_inc(m, new_m_size);
    }
    memcpy(m->m_data, msrc->m_data, msrc->m_len);
    m->m_len = msrc->m_len; /* copy msrc to m */

    /* make the header of the reply packet */
    ip = mtod(m, struct ip *);
    hlen = sizeof(struct ip); /* no options in reply */

    /* fill in icmp */
    m->m_data += hlen;
    m->m_len -= hlen;

    icp = mtod(m, struct icmp *);

    if (minsize)
        s_ip_len = shlen + ICMP_MINLEN; /* return header+8b only */
    else if (s_ip_len > ICMP_MAXDATALEN) /* maximum size */
        s_ip_len = ICMP_MAXDATALEN;

    m->m_len = ICMP_MINLEN + s_ip_len; /* 8 bytes ICMP header */

    /* min. size = 8+sizeof(struct ip)+8 */

    icp->icmp_type = type;
    icp->icmp_code = code;
    icp->icmp_id = 0;
    icp->icmp_seq = 0;

    memcpy(&icp->icmp_ip, msrc->m_data, s_ip_len); /* report the ip packet */
    HTONS(icp->icmp_ip.ip_len);
    HTONS(icp->icmp_ip.ip_id);
    HTONS(icp->icmp_ip.ip_off);

    if (message && WITH_ICMP_ERROR_MSG) { /* append message to ICMP packet */
        int message_len;
        char *cpnt;
        message_len = strlen(message);
        if (message_len > ICMP_MAXDATALEN)
            message_len = ICMP_MAXDATALEN;
        cpnt = (char *)m->m_data + m->m_len;
        memcpy(cpnt, message, message_len);
        m->m_len += message_len;
    }

    icp->icmp_cksum = 0;
    icp->icmp_cksum = cksum(m, m->m_len);

    m->m_data -= hlen;
    m->m_len += hlen;

    /* fill in ip */
    ip->ip_hl = hlen >> 2;
    ip->ip_len = m->m_len;

    ip->ip_tos = ((ip->ip_tos & 0x1E) | 0xC0); /* high priority for errors */

    ip->ip_ttl = MAXTTL;
    ip->ip_p = IPPROTO_ICMP;
    ip->ip_dst = ip->ip_src; /* ip addresses */
    ip->ip_src = *src;

    ip_output((struct socket *)NULL, m);

end_error:
    return;
}
#undef ICMP_MAXDATALEN

void icmp_send_error(struct mbuf *msrc, uint8_t type, uint8_t code, int minsize,
                     const char *message)
{
    icmp_forward_error(msrc, type, code, minsize, message, &msrc->slirp->vhost_addr);
}

/*
 * Reflect the ip packet back to the source
 */
void icmp_reflect(struct mbuf *m)
{
    register struct ip *ip = mtod(m, struct ip *);
    int hlen = ip->ip_hl << 2;
    int optlen = hlen - sizeof(struct ip);
    register struct icmp *icp;

    /*
     * Send an icmp packet back to the ip level,
     * after supplying a checksum.
     */
    m->m_data += hlen;
    m->m_len -= hlen;
    icp = mtod(m, struct icmp *);

    icp->icmp_type = ICMP_ECHOREPLY;
    icp->icmp_cksum = 0;
    icp->icmp_cksum = cksum(m, ip->ip_len - hlen);

    m->m_data -= hlen;
    m->m_len += hlen;

    /* fill in ip */
    if (optlen > 0) {
        /*
         * Strip out original options by copying rest of first
         * mbuf's data back, and adjust the IP length.
         */
        memmove((char *)(ip + 1), (char *)ip + hlen,
                (unsigned)(m->m_len - hlen));
        hlen -= optlen;
        ip->ip_hl = hlen >> 2;
        ip->ip_len -= optlen;
        m->m_len -= optlen;
    }

    ip->ip_ttl = MAXTTL;
    { /* swap */
        struct in_addr icmp_dst;
        icmp_dst = ip->ip_dst;
        ip->ip_dst = ip->ip_src;
        ip->ip_src = icmp_dst;
    }

    ip_output((struct socket *)NULL, m);
}

void icmp_receive(struct socket *so)
{
    struct mbuf *m = so->so_m;
    struct ip *ip = mtod(m, struct ip *);
    int hlen = ip->ip_hl << 2;
    uint8_t error_code;
    struct icmp *icp;
    int id, len;

    m->m_data += hlen;
    m->m_len -= hlen;
    icp = mtod(m, struct icmp *);

    id = icp->icmp_id;
    len = recv(so->s, icp, M_ROOM(m), 0);

    if (so->so_type == IPPROTO_IP) {
        if (len >= sizeof(struct ip)) {
            struct ip *inner_ip = mtod(m, struct ip *);
            int inner_hlen = inner_ip->ip_hl << 2;
            if (inner_hlen > len) {
                len = -1;
                errno = -EINVAL;
            } else {
                len -= inner_hlen;
                memmove(icp, (unsigned char *)icp + inner_hlen, len);
            }
        } else {
            len = -1;
            errno = -EINVAL;
        }
    }

    icp->icmp_id = id;

    m->m_data -= hlen;
    m->m_len += hlen;

    if (len == -1 || len == 0) {
        if (errno == ENETUNREACH) {
            error_code = ICMP_UNREACH_NET;
        } else {
            error_code = ICMP_UNREACH_HOST;
        }
        DEBUG_MISC(" udp icmp rx errno = %d-%s", errno, strerror(errno));
        icmp_send_error(so->so_m, ICMP_UNREACH, error_code, 0, strerror(errno));
    } else {
        icmp_reflect(so->so_m);
        so->so_m = NULL; /* Don't m_free() it again! */
    }
    icmp_detach(so);
}
