// See the file "COPYING" in the main distribution directory for copyright.

#include "zeek/packet_analysis/protocol/icmp/ICMP.h"

#include <netinet/icmp6.h>
#include <algorithm>

#include "zeek/Conn.h"
#include "zeek/Desc.h"
#include "zeek/Reporter.h"
#include "zeek/RunState.h"
#include "zeek/Val.h"
#include "zeek/ZeekString.h"
#include "zeek/analyzer/Manager.h"
#include "zeek/packet_analysis/protocol/icmp/ICMPSessionAdapter.h"
#include "zeek/packet_analysis/protocol/icmp/events.bif.h"
#include "zeek/packet_analysis/protocol/ip/SessionAdapter.h"
#include "zeek/session/Manager.h"

using namespace zeek::packet_analysis::ICMP;
using namespace zeek::packet_analysis::IP;

// These constants exist on macOS but not some versions of Linux
#ifndef MLD_LISTENER_DONE
constexpr int MLD_LISTENER_DONE = 132;
#endif

#ifndef MLDV2_LISTENER_REPORT
constexpr int MLDV2_LISTENER_REPORT = 143;
#endif

ICMPAnalyzer::ICMPAnalyzer() : IPBasedAnalyzer("ICMP", TRANSPORT_ICMP, ICMP_PORT_MASK, false) {}

SessionAdapter* ICMPAnalyzer::MakeSessionAdapter(Connection* conn) {
    auto* root = new ICMPSessionAdapter(conn);
    root->SetParent(this);
    conn->SetInactivityTimeout(zeek::detail::icmp_inactivity_timeout);

    return root;
}

bool ICMPAnalyzer::InitConnKey(size_t len, const uint8_t* data, Packet* packet, IPBasedConnKey& key) {
    if ( ! CheckHeaderTrunc(ICMP_MINLEN, len, packet) )
        return false;

    const struct icmp* icmpp = reinterpret_cast<const icmp*>(data);

    uint32_t icmp_counter_type = 0;
    bool is_one_way = false;

    if ( packet->proto == IPPROTO_ICMP )
        icmp_counter_type = ICMP4_counterpart(icmpp->icmp_type, icmpp->icmp_code, is_one_way);
    else if ( packet->proto == IPPROTO_ICMPV6 )
        icmp_counter_type = ICMP6_counterpart(icmpp->icmp_type, icmpp->icmp_code, is_one_way);
    else
        reporter->InternalError("Reached ICMP packet analyzer with unknown packet protocol %x", packet->proto);

    key.InitTuple(packet->ip_hdr->SrcAddr(), htons(icmpp->icmp_type), packet->ip_hdr->DstAddr(),
                  htons(icmp_counter_type), packet->proto, is_one_way);

    return true;
}

void ICMPAnalyzer::DeliverPacket(Connection* c, double t, bool is_orig, int remaining, Packet* pkt) {
    auto* adapter = static_cast<ICMPSessionAdapter*>(c->GetSessionAdapter());

    const u_char* data = pkt->ip_hdr->Payload();
    int len = pkt->ip_hdr->PayloadLen();
    // If segment offloading or similar is enabled, the payload len will return 0.
    // Thus, let's ignore that case.
    if ( len == 0 )
        len = remaining;

    if ( packet_contents && len > 0 )
        adapter->PacketContents(data + 8, std::min(len, remaining) - 8);

    const struct icmp* icmpp = reinterpret_cast<const icmp*>(data);
    const std::shared_ptr<IP_Hdr>& ip = pkt->ip_hdr;

    if ( ! zeek::detail::ignore_checksums && ! GetIgnoreChecksumsNets()->Contains(ip->IPHeaderSrcAddr()) &&
         remaining >= len ) {
        int chksum = 0;

        switch ( ip->NextProto() ) {
            case IPPROTO_ICMP: chksum = icmp_checksum(icmpp, len); break;

            case IPPROTO_ICMPV6: chksum = icmp6_checksum(icmpp, ip.get(), len); break;

            default: reporter->Error("unexpected IP proto in ICMP analyzer: %d", ip->NextProto()); return;
        }

        if ( chksum != 0xffff ) {
            adapter->Weird("bad_ICMP_checksum");
            adapter->TapPacket(pkt, PacketAction::Skip, SkipReason::BadChecksum);
            return;
        }
    }

    c->SetLastTime(run_state::current_timestamp);
    adapter->InitEndpointMatcher(ip.get(), len, is_orig);

    // Move past common portion of ICMP header. BuildConnTuple() verified that
    // the header is fully present.
    data += 8;
    remaining -= 8;
    len -= 8;

    // The ICMP session adapter only uses len to signal endpoint activity, so
    // caplen vs len does not matter.
    adapter->UpdateLength(is_orig, len);

    if ( ip->NextProto() == IPPROTO_ICMP )
        NextICMP4(run_state::current_timestamp, icmpp, len, remaining, data, ip.get(), adapter);
    else if ( ip->NextProto() == IPPROTO_ICMPV6 )
        NextICMP6(run_state::current_timestamp, icmpp, len, remaining, data, ip.get(), adapter);
    else {
        reporter->Error("expected ICMP as IP packet's protocol, got %d", ip->NextProto());
        return;
    }

    // Store the session in the packet in case we get an encapsulation here. We need it for
    // handling those properly.
    pkt->session = c;

    // Tap the packet before processing/forwarding.
    adapter->TapPacket(pkt);

    ForwardPacket(std::min(len, remaining), data, pkt);

    if ( remaining >= len )
        adapter->ForwardPacket(len, data, is_orig, -1, ip.get(), remaining);

    adapter->MatchEndpoint(data, std::min(len, remaining), is_orig);
}

void ICMPAnalyzer::NextICMP4(double t, const struct icmp* icmpp, int len, int caplen, const u_char*& data,
                             const IP_Hdr* ip_hdr, ICMPSessionAdapter* adapter) {
    switch ( icmpp->icmp_type ) {
        case ICMP_ECHO:
        case ICMP_ECHOREPLY: Echo(t, icmpp, len, caplen, data, ip_hdr, adapter); break;

        case ICMP_UNREACH:
        case ICMP_TIMXCEED: Context4(t, icmpp, len, caplen, data, ip_hdr, adapter); break;

        default: ICMP_Sent(icmpp, len, caplen, 0, data, ip_hdr, adapter); break;
    }
}

void ICMPAnalyzer::NextICMP6(double t, const struct icmp* icmpp, int len, int caplen, const u_char*& data,
                             const IP_Hdr* ip_hdr, ICMPSessionAdapter* adapter) {
    switch ( icmpp->icmp_type ) {
        // Echo types.
        case ICMP6_ECHO_REQUEST:
        case ICMP6_ECHO_REPLY: Echo(t, icmpp, len, caplen, data, ip_hdr, adapter); break;

        // Error messages all have the same structure for their context,
        // and are handled by the same function.
        case ICMP6_PARAM_PROB:
        case ICMP6_TIME_EXCEEDED:
        case ICMP6_PACKET_TOO_BIG:
        case ICMP6_DST_UNREACH: Context6(t, icmpp, len, caplen, data, ip_hdr, adapter); break;

        // Router related messages.
        case ND_REDIRECT: Redirect(t, icmpp, len, caplen, data, ip_hdr, adapter); break;
        case ND_ROUTER_ADVERT: RouterAdvert(t, icmpp, len, caplen, data, ip_hdr, adapter); break;
        case ND_NEIGHBOR_ADVERT: NeighborAdvert(t, icmpp, len, caplen, data, ip_hdr, adapter); break;
        case ND_NEIGHBOR_SOLICIT: NeighborSolicit(t, icmpp, len, caplen, data, ip_hdr, adapter); break;
        case ND_ROUTER_SOLICIT: RouterSolicit(t, icmpp, len, caplen, data, ip_hdr, adapter); break;
        case ICMP6_ROUTER_RENUMBERING: ICMP_Sent(icmpp, len, caplen, 1, data, ip_hdr, adapter); break;
        case MLD_LISTENER_REPORT:
        case MLD_LISTENER_DONE: MLDReport(t, icmpp, len, caplen, data, ip_hdr, adapter); break;
        case MLDV2_LISTENER_REPORT: MLDReportV2(t, icmpp, len, caplen, data, ip_hdr, adapter); break;

#if 0
		// Currently not specifically implemented.
		case MLD_LISTENER_QUERY:
#endif
        default:
            // Error messages (i.e., ICMPv6 type < 128) all have
            // the same structure for their context, and are
            // handled by the same function.
            if ( icmpp->icmp_type < 128 )
                Context6(t, icmpp, len, caplen, data, ip_hdr, adapter);
            else
                ICMP_Sent(icmpp, len, caplen, 1, data, ip_hdr, adapter);
            break;
    }
}

void ICMPAnalyzer::ICMP_Sent(const struct icmp* icmpp, int len, int caplen, int icmpv6, const u_char* data,
                             const IP_Hdr* ip_hdr, ICMPSessionAdapter* adapter) {
    if ( icmp_sent )
        adapter->EnqueueConnEvent(icmp_sent, adapter->ConnVal(), BuildInfo(icmpp, len, icmpv6, ip_hdr));

    if ( icmp_sent_payload ) {
        String* payload = new String(data, std::min(len, caplen), false);

        adapter->EnqueueConnEvent(icmp_sent_payload, adapter->ConnVal(), BuildInfo(icmpp, len, icmpv6, ip_hdr),
                                  make_intrusive<StringVal>(payload));
    }
}

zeek::RecordValPtr ICMPAnalyzer::BuildInfo(const struct icmp* icmpp, int len, bool icmpv6, const IP_Hdr* ip_hdr) {
    static auto icmp_info = id::find_type<RecordType>("icmp_info");
    auto rval = make_intrusive<zeek::RecordVal>(icmp_info);
    rval->Assign(0, val_mgr->Bool(icmpv6));
    rval->Assign(1, val_mgr->Count(icmpp->icmp_type));
    rval->Assign(2, val_mgr->Count(icmpp->icmp_code));
    rval->Assign(3, val_mgr->Count(len));
    rval->Assign(4, val_mgr->Count(ip_hdr->TTL()));
    return rval;
}

TransportProto ICMPAnalyzer::GetContextProtocol(const IP_Hdr* ip_hdr, uint32_t* src_port, uint32_t* dst_port) {
    const u_char* transport_hdr;
    uint32_t ip_hdr_len = ip_hdr->HdrLen();
    bool ip4 = ip_hdr->IP4_Hdr();

    if ( ip4 )
        transport_hdr = (reinterpret_cast<const u_char*>(ip_hdr->IP4_Hdr()) + ip_hdr_len);
    else
        transport_hdr = (reinterpret_cast<const u_char*>(ip_hdr->IP6_Hdr()) + ip_hdr_len);

    TransportProto proto;

    switch ( ip_hdr->NextProto() ) {
        case 1: proto = TRANSPORT_ICMP; break;
        case 6: proto = TRANSPORT_TCP; break;
        case 17: proto = TRANSPORT_UDP; break;
        case 58: proto = TRANSPORT_ICMP; break;
        default: proto = TRANSPORT_UNKNOWN; break;
    }

    switch ( proto ) {
        case TRANSPORT_ICMP: {
            const struct icmp* icmpp = reinterpret_cast<const icmp*>(transport_hdr);
            bool is_one_way; // dummy
            *src_port = ntohs(icmpp->icmp_type);

            if ( ip4 )
                *dst_port = ntohs(ICMP4_counterpart(icmpp->icmp_type, icmpp->icmp_code, is_one_way));
            else
                *dst_port = ntohs(ICMP6_counterpart(icmpp->icmp_type, icmpp->icmp_code, is_one_way));

            break;
        }

        case TRANSPORT_TCP: {
            const struct tcphdr* tp = reinterpret_cast<const tcphdr*>(transport_hdr);
            *src_port = ntohs(tp->th_sport);
            *dst_port = ntohs(tp->th_dport);
            break;
        }

        case TRANSPORT_UDP: {
            const struct udphdr* up = reinterpret_cast<const udphdr*>(transport_hdr);
            *src_port = ntohs(up->uh_sport);
            *dst_port = ntohs(up->uh_dport);
            break;
        }

        default: *src_port = *dst_port = ntohs(0); break;
    }

    return proto;
}

zeek::RecordValPtr ICMPAnalyzer::ExtractICMP4Context(int len, const u_char*& data) {
    uint32_t ip_len;
    uint32_t frag_offset;
    bool bad_hdr_len = false;
    bool bad_checksum = false;
    TransportProto proto = TRANSPORT_UNKNOWN;
    int DF;
    int MF;
    IPAddr src_addr;
    IPAddr dst_addr;
    uint32_t src_port;
    uint32_t dst_port;

    if ( len < static_cast<int>(sizeof(struct ip)) ) {
        // We don't have an entire IP header.
        bad_hdr_len = true;
        bad_checksum = false;
        ip_len = frag_offset = 0;
        DF = MF = 0;
        src_port = dst_port = 0;
    }

    else {
        const IP_Hdr ip_hdr_data(reinterpret_cast<const ip*>(data), false);
        const IP_Hdr* ip_hdr = &ip_hdr_data;
        uint32_t ip_hdr_len = ip_hdr->HdrLen();
        bad_hdr_len = (ip_hdr_len > static_cast<uint32_t>(len));

        if ( ! bad_hdr_len )
            bad_checksum = ! run_state::current_pkt->l4_checksummed &&
                           (zeek::detail::in_cksum(reinterpret_cast<const uint8_t*>(ip_hdr->IP4_Hdr()),
                                                   static_cast<int>(ip_hdr_len)) != 0xffff);
        else
            bad_checksum = false;

        ip_len = ip_hdr->TotalLen();

        src_addr = ip_hdr->SrcAddr();
        dst_addr = ip_hdr->DstAddr();

        DF = ip_hdr->DF();
        MF = ip_hdr->MF();
        frag_offset = ip_hdr->FragOffset();

        if ( static_cast<uint32_t>(len) >= ip_hdr_len + 4 )
            proto = GetContextProtocol(ip_hdr, &src_port, &dst_port);
        else {
            // 4 above is the magic number meaning that both
            // port numbers are included in the ICMP.
            src_port = dst_port = 0;
            bad_hdr_len = true;
        }
    }

    static auto icmp_context = id::find_type<RecordType>("icmp_context");
    auto iprec = make_intrusive<zeek::RecordVal>(icmp_context);
    auto id_val = make_intrusive<zeek::RecordVal>(id::conn_id);

    id_val->Assign(0, make_intrusive<AddrVal>(src_addr));
    id_val->Assign(1, val_mgr->Port(src_port, proto));
    id_val->Assign(2, make_intrusive<AddrVal>(dst_addr));
    id_val->Assign(3, val_mgr->Port(dst_port, proto));
    id_val->Assign(4, IPPROTO_ICMP);

    iprec->Assign(0, std::move(id_val));
    iprec->Assign(1, val_mgr->Count(ip_len));
    iprec->Assign(2, val_mgr->Count(proto));
    iprec->Assign(3, val_mgr->Count(frag_offset));
    iprec->Assign(4, val_mgr->Bool(bad_hdr_len));
    iprec->Assign(5, val_mgr->Bool(bad_checksum));
    iprec->Assign(6, val_mgr->Bool(MF));
    iprec->Assign(7, val_mgr->Bool(DF));

    return iprec;
}

zeek::RecordValPtr ICMPAnalyzer::ExtractICMP6Context(int len, const u_char*& data) {
    int DF = 0;
    int MF = 0;
    int bad_hdr_len = 0;
    TransportProto proto = TRANSPORT_UNKNOWN;

    IPAddr src_addr;
    IPAddr dst_addr;
    uint32_t ip_len;
    uint32_t frag_offset = 0;
    uint32_t src_port;
    uint32_t dst_port;

    if ( len < static_cast<int>(sizeof(struct ip6_hdr)) ) {
        bad_hdr_len = 1;
        ip_len = 0;
        src_port = dst_port = 0;
    }
    else {
        const IP_Hdr ip_hdr_data(reinterpret_cast<const ip6_hdr*>(data), false, len);
        const IP_Hdr* ip_hdr = &ip_hdr_data;

        ip_len = ip_hdr->TotalLen();
        src_addr = ip_hdr->SrcAddr();
        dst_addr = ip_hdr->DstAddr();
        frag_offset = ip_hdr->FragOffset();
        MF = ip_hdr->MF();
        DF = ip_hdr->DF();

        if ( static_cast<uint32_t>(len) >= static_cast<uint32_t>(ip_hdr->HdrLen() + 4) )
            proto = GetContextProtocol(ip_hdr, &src_port, &dst_port);
        else {
            // 4 above is the magic number meaning that both
            // port numbers are included in the ICMP.
            src_port = dst_port = 0;
            bad_hdr_len = 1;
        }
    }

    static auto icmp_context = id::find_type<RecordType>("icmp_context");
    auto iprec = make_intrusive<zeek::RecordVal>(icmp_context);
    auto id_val = make_intrusive<zeek::RecordVal>(id::conn_id);

    id_val->Assign(0, make_intrusive<AddrVal>(src_addr));
    id_val->Assign(1, val_mgr->Port(src_port, proto));
    id_val->Assign(2, make_intrusive<AddrVal>(dst_addr));
    id_val->Assign(3, val_mgr->Port(dst_port, proto));
    id_val->Assign(4, IPPROTO_ICMPV6);

    iprec->Assign(0, std::move(id_val));
    iprec->Assign(1, val_mgr->Count(ip_len));
    iprec->Assign(2, val_mgr->Count(proto));
    iprec->Assign(3, val_mgr->Count(frag_offset));
    iprec->Assign(4, val_mgr->Bool(bad_hdr_len));
    // bad_checksum is always false since IPv6 layer doesn't have a checksum.
    iprec->Assign(5, val_mgr->False());
    iprec->Assign(6, val_mgr->Bool(MF));
    iprec->Assign(7, val_mgr->Bool(DF));

    return iprec;
}

void ICMPAnalyzer::Echo(double t, const struct icmp* icmpp, int len, int caplen, const u_char*& data,
                        const IP_Hdr* ip_hdr, ICMPSessionAdapter* adapter) {
    // For handling all Echo related ICMP messages
    EventHandlerPtr f = nullptr;

    if ( ip_hdr->NextProto() == IPPROTO_ICMPV6 )
        f = (icmpp->icmp_type == ICMP6_ECHO_REQUEST) ? icmp_echo_request : icmp_echo_reply;
    else
        f = (icmpp->icmp_type == ICMP_ECHO) ? icmp_echo_request : icmp_echo_reply;

    if ( ! f )
        return;

    int iid = ntohs(icmpp->icmp_hun.ih_idseq.icd_id);
    int iseq = ntohs(icmpp->icmp_hun.ih_idseq.icd_seq);

    String* payload = new String(data, caplen, false);

    adapter->EnqueueConnEvent(f, adapter->ConnVal(), BuildInfo(icmpp, len, ip_hdr->NextProto() != IPPROTO_ICMP, ip_hdr),
                              val_mgr->Count(iid), val_mgr->Count(iseq), make_intrusive<StringVal>(payload));
}

void ICMPAnalyzer::RouterAdvert(double t, const struct icmp* icmpp, int len, int caplen, const u_char*& data,
                                const IP_Hdr* ip_hdr, ICMPSessionAdapter* adapter) {
    EventHandlerPtr f = icmp_router_advertisement;

    if ( ! f )
        return;

    uint32_t reachable = 0;
    uint32_t retrans = 0;

    if ( caplen >= static_cast<int>(sizeof(reachable)) )
        memcpy(&reachable, data, sizeof(reachable));

    if ( caplen >= static_cast<int>(sizeof(reachable)) + static_cast<int>(sizeof(retrans)) )
        memcpy(&retrans, data + sizeof(reachable), sizeof(retrans));

    int opt_offset = sizeof(reachable) + sizeof(retrans);

    if ( caplen < opt_offset ) {
        Weird("icmp6_ra_bad_opt_offset", nullptr, util::fmt("%u (length %u)", opt_offset, caplen));
        return;
    }

    adapter->EnqueueConnEvent(f, adapter->ConnVal(), BuildInfo(icmpp, len, true, ip_hdr),
                              val_mgr->Count(icmpp->icmp_num_addrs),         // Cur Hop Limit
                              val_mgr->Bool(icmpp->icmp_wpa & 0x80),         // Managed
                              val_mgr->Bool(icmpp->icmp_wpa & 0x40),         // Other
                              val_mgr->Bool(icmpp->icmp_wpa & 0x20),         // Home Agent
                              val_mgr->Count((icmpp->icmp_wpa & 0x18) >> 3), // Pref
                              val_mgr->Bool(icmpp->icmp_wpa & 0x04),         // Proxy
                              val_mgr->Count(icmpp->icmp_wpa & 0x02),        // Reserved
                              make_intrusive<IntervalVal>(static_cast<double>(ntohs(icmpp->icmp_lifetime)), Seconds),
                              make_intrusive<IntervalVal>(static_cast<double>(ntohl(reachable)), Milliseconds),
                              make_intrusive<IntervalVal>(static_cast<double>(ntohl(retrans)), Milliseconds),
                              BuildNDOptionsVal(caplen - opt_offset, data + opt_offset, adapter));
}

void ICMPAnalyzer::NeighborAdvert(double t, const struct icmp* icmpp, int len, int caplen, const u_char*& data,
                                  const IP_Hdr* ip_hdr, ICMPSessionAdapter* adapter) {
    EventHandlerPtr f = icmp_neighbor_advertisement;

    if ( ! f )
        return;

    IPAddr tgtaddr;

    if ( caplen >= static_cast<int>(sizeof(in6_addr)) )
        tgtaddr = IPAddr(*reinterpret_cast<const in6_addr*>(data));

    int opt_offset = sizeof(in6_addr);

    if ( caplen < opt_offset ) {
        Weird("icmp6_na_bad_opt_offset", nullptr, util::fmt("%u (length %u)", opt_offset, caplen));
        return;
    }

    adapter->EnqueueConnEvent(f, adapter->ConnVal(), BuildInfo(icmpp, len, true, ip_hdr),
                              val_mgr->Bool(icmpp->icmp_num_addrs & 0x80), // Router
                              val_mgr->Bool(icmpp->icmp_num_addrs & 0x40), // Solicited
                              val_mgr->Bool(icmpp->icmp_num_addrs & 0x20), // Override
                              make_intrusive<AddrVal>(tgtaddr),
                              BuildNDOptionsVal(caplen - opt_offset, data + opt_offset, adapter));
}

void ICMPAnalyzer::NeighborSolicit(double t, const struct icmp* icmpp, int len, int caplen, const u_char*& data,
                                   const IP_Hdr* ip_hdr, ICMPSessionAdapter* adapter) {
    EventHandlerPtr f = icmp_neighbor_solicitation;

    if ( ! f )
        return;

    IPAddr tgtaddr;

    if ( caplen >= static_cast<int>(sizeof(in6_addr)) )
        tgtaddr = IPAddr(*reinterpret_cast<const in6_addr*>(data));

    int opt_offset = sizeof(in6_addr);

    if ( caplen < opt_offset ) {
        Weird("icmp6_ns_bad_opt_offset", nullptr, util::fmt("%u (length %u)", opt_offset, caplen));
        return;
    }

    adapter->EnqueueConnEvent(f, adapter->ConnVal(), BuildInfo(icmpp, len, true, ip_hdr),
                              make_intrusive<AddrVal>(tgtaddr),
                              BuildNDOptionsVal(caplen - opt_offset, data + opt_offset, adapter));
}

void ICMPAnalyzer::Redirect(double t, const struct icmp* icmpp, int len, int caplen, const u_char*& data,
                            const IP_Hdr* ip_hdr, ICMPSessionAdapter* adapter) {
    EventHandlerPtr f = icmp_redirect;

    if ( ! f )
        return;

    IPAddr tgtaddr;
    IPAddr dstaddr;

    if ( caplen >= static_cast<int>(sizeof(in6_addr)) )
        tgtaddr = IPAddr(*reinterpret_cast<const in6_addr*>(data));

    if ( caplen >= 2 * static_cast<int>(sizeof(in6_addr)) )
        dstaddr = IPAddr(*reinterpret_cast<const in6_addr*>(data + sizeof(in6_addr)));

    int opt_offset = 2 * sizeof(in6_addr);

    if ( caplen < opt_offset ) {
        Weird("icmp6_redirect_bad_opt_offset", nullptr, util::fmt("%u (length %u)", opt_offset, caplen));
        return;
    }

    adapter->EnqueueConnEvent(f, adapter->ConnVal(), BuildInfo(icmpp, len, true, ip_hdr),
                              make_intrusive<AddrVal>(tgtaddr), make_intrusive<AddrVal>(dstaddr),
                              BuildNDOptionsVal(caplen - opt_offset, data + opt_offset, adapter));
}

void ICMPAnalyzer::RouterSolicit(double t, const struct icmp* icmpp, int len, int caplen, const u_char*& data,
                                 const IP_Hdr* ip_hdr, ICMPSessionAdapter* adapter) {
    EventHandlerPtr f = icmp_router_solicitation;

    if ( ! f )
        return;

    adapter->EnqueueConnEvent(f, adapter->ConnVal(), BuildInfo(icmpp, len, true, ip_hdr),
                              BuildNDOptionsVal(caplen, data, adapter));
}

void ICMPAnalyzer::MLDReport(double t, const struct icmp* icmpp, int len, int caplen, const u_char*& data,
                             const IP_Hdr* ip_hdr, ICMPSessionAdapter* adapter) {
    if ( caplen < 16 ) {
        adapter->Weird("truncated_ICMPv6_MLDv1");
        return;
    }

    EventHandlerPtr f = nullptr;

    switch ( icmpp->icmp_type ) {
        case MLD_LISTENER_REPORT: f = icmpv6_mld_report_v1; break;
        case MLD_LISTENER_DONE: f = icmpv6_mld_done_v1; break;
        default: break;
    }

    if ( ! f )
        return;

    IPAddr mc_addr = IPAddr(*reinterpret_cast<const in6_addr*>(data));
    adapter->EnqueueConnEvent(f, adapter->ConnVal(), make_intrusive<AddrVal>(mc_addr));
}

void ICMPAnalyzer::MLDReportV2(double t, const struct icmp* icmpp, int len, int caplen, const u_char*& data,
                               const IP_Hdr* ip_hdr, ICMPSessionAdapter* adapter) {
    static auto icmp6_mld_group_type_type = id::find_type<EnumType>("icmp6_mldv2_group_type");
    static auto icmp6_mld_mar_type = id::find_type<RecordType>("icmp6_mldv2_mar");
    static auto icmp6_mld_mar_vec_type = id::find_type<VectorType>("icmp6_mldv2_mar_vec");
    static auto addr_vec_type = id::find_type<VectorType>("addr_vec");

    // The ICMP header was already parsed and the data pointer starts at the beginning of
    // the first record. We have to figure this counter out from the icmp header instead.
    const struct icmp6_hdr* i6_hdr = reinterpret_cast<const struct icmp6_hdr*>(icmpp);
    uint16_t num_multicast_records = ntohs(i6_hdr->icmp6_data16[1]);

    // The minimum size of a Multicast Address Record is 20 if it doesn't include any
    // sources. Check whether there's enough data to parse at least that size for each of
    // the records above.
    if ( caplen < static_cast<int>(num_multicast_records) * 20 ) {
        adapter->Weird("truncated_ICMPv6_MLDv2");
        return;
    }

    size_t remaining = caplen;
    const uint8_t* record_ptr = data;

    auto vec = make_intrusive<VectorVal>(icmp6_mld_mar_vec_type);
    vec->Reserve(std::min<size_t>(32, num_multicast_records));

    for ( uint16_t i = 0; i < num_multicast_records && remaining > 0; i++ ) {
        if ( remaining < 4 ) {
            adapter->Weird("truncated_ICMPv6_MLDv2_record");
            break;
        }

        uint8_t record_type = *(reinterpret_cast<const uint8_t*>(record_ptr));
        uint32_t aux_len = *(reinterpret_cast<const uint8_t*>(record_ptr + 1)) * 4;
        uint16_t num_sources = ntohs(*(reinterpret_cast<const uint16_t*>(record_ptr + 2)));

        record_ptr += 4;
        remaining -= 4;

        // The remaining data should be at least the size of the multicast address plus
        // the number of multicast sources plus the length of the aux data.
        if ( remaining < 16 + (num_sources * 16) + aux_len ) {
            adapter->Weird("truncated_ICMPv6_MLDv2_record");
            break;
        }

        IPAddr mc_addr = IPAddr(*reinterpret_cast<const in6_addr*>(record_ptr));
        record_ptr += 16;
        remaining -= 16;

        auto source_vec = make_intrusive<VectorVal>(addr_vec_type);
        source_vec->Reserve(std::min<size_t>(32, num_sources));

        for ( uint16_t src = 0; src < num_sources; src++ ) {
            source_vec->Append(make_intrusive<AddrVal>(IPAddr(*reinterpret_cast<const in6_addr*>(record_ptr))));
            record_ptr += 16;
            remaining -= 16;
        }

        auto rv = make_intrusive<RecordVal>(icmp6_mld_mar_type);
        rv->Assign(0, icmp6_mld_group_type_type->GetEnumVal(record_type));
        rv->Assign(1, aux_len);
        rv->Assign(2, num_sources);
        rv->Assign(3, make_intrusive<AddrVal>(mc_addr));
        rv->Assign(4, source_vec);

        if ( aux_len > 0 ) {
            rv->Assign(5, make_intrusive<StringVal>(aux_len, reinterpret_cast<const char*>(record_ptr)));
            record_ptr += aux_len;
            remaining -= aux_len;
        }

        vec->Append(std::move(rv));
    }

    if ( icmpv6_mld_report_v2 )
        adapter->EnqueueConnEvent(icmpv6_mld_report_v2, adapter->ConnVal(), vec);
}

void ICMPAnalyzer::Context4(double t, const struct icmp* icmpp, int len, int caplen, const u_char*& data,
                            const IP_Hdr* ip_hdr, ICMPSessionAdapter* adapter) {
    EventHandlerPtr f = nullptr;

    switch ( icmpp->icmp_type ) {
        case ICMP_UNREACH: f = icmp_unreachable; break;

        case ICMP_TIMXCEED: f = icmp_time_exceeded; break;
    }

    if ( f )
        adapter->EnqueueConnEvent(f, adapter->ConnVal(), BuildInfo(icmpp, len, false, ip_hdr),
                                  val_mgr->Count(icmpp->icmp_code), ExtractICMP4Context(caplen, data));
}

void ICMPAnalyzer::Context6(double t, const struct icmp* icmpp, int len, int caplen, const u_char*& data,
                            const IP_Hdr* ip_hdr, ICMPSessionAdapter* adapter) {
    EventHandlerPtr f = nullptr;

    switch ( icmpp->icmp_type ) {
        case ICMP6_DST_UNREACH: f = icmp_unreachable; break;

        case ICMP6_PARAM_PROB: f = icmp_parameter_problem; break;

        case ICMP6_TIME_EXCEEDED: f = icmp_time_exceeded; break;

        case ICMP6_PACKET_TOO_BIG: f = icmp_packet_too_big; break;

        default: f = icmp_error_message; break;
    }

    if ( f )
        adapter->EnqueueConnEvent(f, adapter->ConnVal(), BuildInfo(icmpp, len, true, ip_hdr),
                                  val_mgr->Count(icmpp->icmp_code), ExtractICMP6Context(caplen, data));
}

zeek::VectorValPtr ICMPAnalyzer::BuildNDOptionsVal(int caplen, const u_char* data, ICMPSessionAdapter* adapter) {
    assert(caplen >= 0);

    static auto icmp6_nd_option_type = id::find_type<RecordType>("icmp6_nd_option");
    static auto icmp6_nd_prefix_info_type = id::find_type<RecordType>("icmp6_nd_prefix_info");

    auto vv = make_intrusive<zeek::VectorVal>(id::find_type<VectorType>("icmp6_nd_options"));

    while ( caplen > 0 ) {
        // Must have at least type & length to continue parsing options.
        if ( caplen < 2 ) {
            adapter->Weird("truncated_ICMPv6_ND_options");
            break;
        }

        uint8_t type = *(reinterpret_cast<const uint8_t*>(data));
        uint16_t length = *(reinterpret_cast<const uint8_t*>((data + 1)));

        if ( length == 0 ) {
            adapter->Weird("zero_length_ICMPv6_ND_option");
            break;
        }

        auto rv = make_intrusive<zeek::RecordVal>(icmp6_nd_option_type);
        rv->Assign(0, val_mgr->Count(type));
        rv->Assign(1, val_mgr->Count(length));

        // Adjust length to be in units of bytes, exclude type/length fields.
        length = length * 8 - 2;

        data += 2;
        caplen -= 2;

        bool set_payload_field = false;

        // Only parse out known options that are there in full.
        switch ( type ) {
            case 1:
            case 2:
                // Source/Target Link-layer Address option
                {
                    if ( caplen >= length ) {
                        String* link_addr = new String(data, length, false);
                        rv->Assign(2, make_intrusive<StringVal>(link_addr));
                    }
                    else
                        set_payload_field = true;

                    break;
                }

            case 3:
                // Prefix Information option
                {
                    if ( caplen >= 30 ) {
                        auto info = make_intrusive<zeek::RecordVal>(icmp6_nd_prefix_info_type);
                        uint8_t prefix_len = *reinterpret_cast<const uint8_t*>(data);
                        bool L_flag = (*reinterpret_cast<const uint8_t*>(data + 1) & 0x80) != 0;
                        bool A_flag = (*reinterpret_cast<const uint8_t*>(data + 1) & 0x40) != 0;
                        uint32_t valid_life = *reinterpret_cast<const uint32_t*>(data + 2);
                        uint32_t prefer_life = *reinterpret_cast<const uint32_t*>(data + 6);
                        in6_addr prefix = *reinterpret_cast<const in6_addr*>(data + 14);
                        info->Assign(0, val_mgr->Count(prefix_len));
                        info->Assign(1, val_mgr->Bool(L_flag));
                        info->Assign(2, val_mgr->Bool(A_flag));
                        info->Assign(3, make_intrusive<IntervalVal>(static_cast<double>(ntohl(valid_life)), Seconds));
                        info->Assign(4, make_intrusive<IntervalVal>(static_cast<double>(ntohl(prefer_life)), Seconds));
                        info->Assign(5, make_intrusive<AddrVal>(IPAddr(prefix)));
                        rv->Assign(3, std::move(info));
                    }

                    else
                        set_payload_field = true;
                    break;
                }

            case 4:
                // Redirected Header option
                {
                    if ( caplen >= length ) {
                        const u_char* hdr = data + 6;
                        rv->Assign(4, ExtractICMP6Context(length - 6, hdr));
                    }

                    else
                        set_payload_field = true;

                    break;
                }

            case 5:
                // MTU option
                {
                    if ( caplen >= 6 )
                        rv->Assign(5, val_mgr->Count(ntohl(*reinterpret_cast<const uint32_t*>(data + 2))));
                    else
                        set_payload_field = true;

                    break;
                }

            default: {
                set_payload_field = true;
                break;
            }
        }

        if ( set_payload_field ) {
            String* payload = new String(data, std::min<int>(length, caplen), false);
            rv->Assign(6, make_intrusive<StringVal>(payload));
        }

        data += length;
        caplen -= length;

        vv->Assign(vv->Size(), std::move(rv));
    }

    return vv;
}

namespace zeek::packet_analysis::ICMP {

int ICMP4_counterpart(int icmp_type, int icmp_code, bool& is_one_way) {
    is_one_way = false;

    // Return the counterpart type if one exists.  This allows us
    // to track corresponding ICMP requests/replies.
    // Note that for the two-way ICMP messages, icmp_code is
    // always 0 (RFC 792).
    switch ( icmp_type ) {
        case ICMP_ECHO: return ICMP_ECHOREPLY;
        case ICMP_ECHOREPLY: return ICMP_ECHO;

        case ICMP_TSTAMP: return ICMP_TSTAMPREPLY;
        case ICMP_TSTAMPREPLY: return ICMP_TSTAMP;

        case ICMP_IREQ: return ICMP_IREQREPLY;
        case ICMP_IREQREPLY: return ICMP_IREQ;

        case ICMP_ROUTERSOLICIT: return ICMP_ROUTERADVERT;
        case ICMP_ROUTERADVERT: return ICMP_ROUTERSOLICIT;

        case ICMP_MASKREQ: return ICMP_MASKREPLY;
        case ICMP_MASKREPLY: return ICMP_MASKREQ;

        default: is_one_way = true; return icmp_code;
    }
}

int ICMP6_counterpart(int icmp_type, int icmp_code, bool& is_one_way) {
    is_one_way = false;

    switch ( icmp_type ) {
        case ICMP6_ECHO_REQUEST: return ICMP6_ECHO_REPLY;
        case ICMP6_ECHO_REPLY: return ICMP6_ECHO_REQUEST;

        case ND_ROUTER_SOLICIT: return ND_ROUTER_ADVERT;
        case ND_ROUTER_ADVERT: return ND_ROUTER_SOLICIT;

        case ND_NEIGHBOR_SOLICIT: return ND_NEIGHBOR_ADVERT;
        case ND_NEIGHBOR_ADVERT: return ND_NEIGHBOR_SOLICIT;

        case MLD_LISTENER_QUERY: return MLD_LISTENER_REPORT;
        case MLD_LISTENER_REPORT: return MLD_LISTENER_QUERY;

        // ICMP node information query and response respectively (not defined in
        // icmp6.h)
        case 139: return 140;
        case 140: return 139;

        // Home Agent Address Discovery Request Message and reply
        case 144: return 145;
        case 145:
            return 144;

            // TODO: Add further counterparts.

        default: is_one_way = true; return icmp_code;
    }
}

} // namespace zeek::packet_analysis::ICMP
