/*
 *  Copyright (c) 2016, The OpenThread Authors.
 *  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 copyright holder 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 COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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.
 */

/**
 * @file
 *   This file implements IPv6 networking.
 */

#include "ip6.hpp"

#include "instance/instance.hpp"

namespace ot {
namespace Ip6 {

RegisterLogModule("Ip6");

const uint8_t Ip6::kForwardIcmpTypes[] = {
    Icmp::Header::kTypeDstUnreach,       Icmp::Header::kTypePacketToBig, Icmp::Header::kTypeTimeExceeded,
    Icmp::Header::kTypeParameterProblem, Icmp::Header::kTypeEchoRequest, Icmp::Header::kTypeEchoReply,
};

Ip6::Ip6(Instance &aInstance)
    : InstanceLocator(aInstance)
    , mReceiveFilterEnabled(false)
#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
    , mTmfOriginFilterEnabled(true)
#endif
    , mSendQueueTask(aInstance)
    , mIcmp(aInstance)
    , mUdp(aInstance)
    , mMpl(aInstance)
#if OPENTHREAD_CONFIG_TCP_ENABLE
    , mTcp(aInstance)
#endif
{
#if OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE
    ResetBorderRoutingCounters();
#endif
}

Message *Ip6::NewMessageFromData(const uint8_t *aData, uint16_t aDataLength, const Message::Settings &aSettings)
{
    Message          *message  = nullptr;
    Message::Settings settings = aSettings;
    const Header     *header;

    VerifyOrExit((aData != nullptr) && (aDataLength >= sizeof(Header)));

    // Determine priority from IPv6 header
    header = reinterpret_cast<const Header *>(aData);
    VerifyOrExit(header->IsValid());
    VerifyOrExit(sizeof(Header) + header->GetPayloadLength() == aDataLength);
    settings.mPriority = DscpToPriority(header->GetDscp());

    message = Get<MessagePool>().Allocate(Message::kTypeIp6, /* aReserveHeader */ 0, settings);

    VerifyOrExit(message != nullptr);

    if (message->AppendBytes(aData, aDataLength) != kErrorNone)
    {
        message->Free();
        message = nullptr;
    }

exit:
    return message;
}

Message::Priority Ip6::DscpToPriority(uint8_t aDscp)
{
    Message::Priority priority;
    uint8_t           cs = aDscp & kDscpCsMask;

    switch (cs)
    {
    case kDscpCs1:
    case kDscpCs2:
        priority = Message::kPriorityLow;
        break;

    case kDscpCs0:
    case kDscpCs3:
        priority = Message::kPriorityNormal;
        break;

    case kDscpCs4:
    case kDscpCs5:
    case kDscpCs6:
    case kDscpCs7:
        priority = Message::kPriorityHigh;
        break;

    default:
        priority = Message::kPriorityNormal;
        break;
    }

    return priority;
}

uint8_t Ip6::PriorityToDscp(Message::Priority aPriority)
{
    uint8_t dscp = kDscpCs0;

    switch (aPriority)
    {
    case Message::kPriorityLow:
        dscp = kDscpCs1;
        break;

    case Message::kPriorityNormal:
    case Message::kPriorityNet:
        dscp = kDscpCs0;
        break;

    case Message::kPriorityHigh:
        dscp = kDscpCs4;
        break;
    }

    return dscp;
}

Error Ip6::AddMplOption(Message &aMessage, Header &aHeader)
{
    Error          error = kErrorNone;
    HopByHopHeader hbhHeader;
    MplOption      mplOption;
    PadOption      padOption;

    hbhHeader.SetNextHeader(aHeader.GetNextHeader());
    hbhHeader.SetLength(0);
    mMpl.InitOption(mplOption, aHeader.GetSource());

    // Check if MPL option may require padding
    if (padOption.InitToPadHeaderWithSize(sizeof(HopByHopHeader) + mplOption.GetSize()) == kErrorNone)
    {
        SuccessOrExit(error = aMessage.PrependBytes(&padOption, padOption.GetSize()));
    }

    SuccessOrExit(error = aMessage.PrependBytes(&mplOption, mplOption.GetSize()));
    SuccessOrExit(error = aMessage.Prepend(hbhHeader));
    aHeader.SetPayloadLength(aHeader.GetPayloadLength() + sizeof(hbhHeader) + sizeof(mplOption));
    aHeader.SetNextHeader(kProtoHopOpts);

exit:
    return error;
}

Error Ip6::PrepareMulticastToLargerThanRealmLocal(Message &aMessage, const Header &aHeader)
{
    Error  error = kErrorNone;
    Header tunnelHeader;

#if OPENTHREAD_FTD
    if (aHeader.GetDestination().IsMulticastLargerThanRealmLocal() &&
        Get<ChildTable>().HasSleepyChildWithAddress(aHeader.GetDestination()))
    {
        Message *messageCopy = aMessage.Clone<kSameReservedHeader>();

        if (messageCopy != nullptr)
        {
            EnqueueDatagram(*messageCopy);
        }
        else
        {
            LogWarn("Failed to clone mcast message for indirect tx to sleepy children");
        }
    }
#endif

    // Use IP-in-IP encapsulation (RFC2473) and ALL_MPL_FORWARDERS address.
    tunnelHeader.InitVersionTrafficClassFlow();
    tunnelHeader.SetHopLimit(kDefaultHopLimit);
    tunnelHeader.SetPayloadLength(aHeader.GetPayloadLength() + sizeof(tunnelHeader));
    tunnelHeader.SetSource(Get<Mle::Mle>().GetMeshLocalRloc());
    tunnelHeader.SetDestination(Address::GetRealmLocalAllMplForwarders());
    tunnelHeader.SetNextHeader(kProtoIp6);

    SuccessOrExit(error = AddMplOption(aMessage, tunnelHeader));
    SuccessOrExit(error = aMessage.Prepend(tunnelHeader));

exit:
    return error;
}

Error Ip6::InsertMplOption(Message &aMessage, Header &aHeader)
{
    Error error = kErrorNone;

    if (aHeader.GetDestination().IsMulticastLargerThanRealmLocal())
    {
        error = PrepareMulticastToLargerThanRealmLocal(aMessage, aHeader);
        ExitNow();
    }

    VerifyOrExit(aHeader.GetDestination().IsRealmLocalMulticast());

    aMessage.RemoveHeader(sizeof(aHeader));

    if (aHeader.GetNextHeader() == kProtoHopOpts)
    {
        HopByHopHeader hbh;
        uint16_t       hbhSize;
        MplOption      mplOption;
        PadOption      padOption;

        // Read existing hop-by-hop option header
        SuccessOrExit(error = aMessage.Read(0, hbh));
        hbhSize = hbh.GetSize();

        VerifyOrExit(hbhSize <= aHeader.GetPayloadLength(), error = kErrorParse);

        // Increment hop-by-hop option header length by one which
        // increases its total size by 8 bytes.
        hbh.SetLength(hbh.GetLength() + 1);
        aMessage.Write(0, hbh);

        // Make space for MPL Option + padding (8 bytes) at the end
        // of hop-by-hop header
        SuccessOrExit(error = aMessage.InsertHeader(hbhSize, ExtensionHeader::kLengthUnitSize));

        // Insert MPL Option
        mMpl.InitOption(mplOption, aHeader.GetSource());
        aMessage.WriteBytes(hbhSize, &mplOption, mplOption.GetSize());

        // Insert Pad Option (if needed)
        if (padOption.InitToPadHeaderWithSize(mplOption.GetSize()) == kErrorNone)
        {
            aMessage.WriteBytes(hbhSize + mplOption.GetSize(), &padOption, padOption.GetSize());
        }

        // Update IPv6 Payload Length
        aHeader.SetPayloadLength(aHeader.GetPayloadLength() + ExtensionHeader::kLengthUnitSize);
    }
    else
    {
        SuccessOrExit(error = AddMplOption(aMessage, aHeader));
    }

    SuccessOrExit(error = aMessage.Prepend(aHeader));

exit:
    return error;
}

Error Ip6::RemoveMplOption(Message &aMessage)
{
    enum Action : uint8_t
    {
        kNoMplOption,
        kShrinkHbh,
        kRemoveHbh,
        kReplaceMplWithPad,
    };

    Error          error  = kErrorNone;
    Action         action = kNoMplOption;
    Header         ip6Header;
    HopByHopHeader hbh;
    Option         option;
    OffsetRange    offsetRange;
    OffsetRange    mplOffsetRange;
    PadOption      padOption;

    offsetRange.InitFromMessageFullLength(aMessage);

    IgnoreError(aMessage.ReadAndAdvance(offsetRange, ip6Header));

    VerifyOrExit(ip6Header.GetNextHeader() == kProtoHopOpts);

    SuccessOrExit(error = ReadHopByHopHeader(aMessage, offsetRange, hbh));

    for (; !offsetRange.IsEmpty(); offsetRange.AdvanceOffset(option.GetSize()))
    {
        SuccessOrExit(error = option.ParseFrom(aMessage, offsetRange));

        if (option.IsPadding())
        {
            continue;
        }

        if (option.GetType() == MplOption::kType)
        {
            // If multiple MPL options exist, discard packet
            VerifyOrExit(action == kNoMplOption, error = kErrorParse);

            // `Option::ParseFrom()` already validated that the entire
            // option is present in the `offsetRange`.

            mplOffsetRange = offsetRange;
            mplOffsetRange.ShrinkLength(option.GetSize());

            VerifyOrExit(option.GetSize() <= sizeof(MplOption), error = kErrorParse);

            if (mplOffsetRange.GetOffset() == sizeof(ip6Header) + sizeof(hbh) && hbh.GetLength() == 0)
            {
                // First and only IPv6 Option, remove IPv6 HBH Option header
                action = kRemoveHbh;
            }
            else if (mplOffsetRange.GetOffset() + ExtensionHeader::kLengthUnitSize == offsetRange.GetEndOffset())
            {
                // Last IPv6 Option, shrink the last 8 bytes
                action = kShrinkHbh;
            }
        }
        else if (action != kNoMplOption)
        {
            // Encountered another option, now just replace
            // MPL Option with Pad Option
            action = kReplaceMplWithPad;
        }
    }

    switch (action)
    {
    case kNoMplOption:
        break;

    case kShrinkHbh:
    case kRemoveHbh:
        // Last IPv6 Option, shrink HBH Option header by
        // 8 bytes (`kLengthUnitSize`)
        aMessage.RemoveHeader(offsetRange.GetEndOffset() - ExtensionHeader::kLengthUnitSize,
                              ExtensionHeader::kLengthUnitSize);

        if (action == kRemoveHbh)
        {
            ip6Header.SetNextHeader(hbh.GetNextHeader());
        }
        else
        {
            // Update HBH header length, decrement by one
            // which decreases its total size by 8 bytes.

            hbh.SetLength(hbh.GetLength() - 1);
            aMessage.Write(sizeof(ip6Header), hbh);
        }

        ip6Header.SetPayloadLength(ip6Header.GetPayloadLength() - ExtensionHeader::kLengthUnitSize);
        aMessage.Write(0, ip6Header);
        break;

    case kReplaceMplWithPad:
        padOption.InitForPadSize(static_cast<uint8_t>(mplOffsetRange.GetLength()));
        aMessage.WriteBytes(mplOffsetRange.GetOffset(), &padOption, padOption.GetSize());
        break;
    }

exit:
    return error;
}

void Ip6::EnqueueDatagram(Message &aMessage)
{
    mSendQueue.Enqueue(aMessage);
    mSendQueueTask.Post();
}

Error Ip6::SendDatagram(Message &aMessage, MessageInfo &aMessageInfo, uint8_t aIpProto)
{
    Error    error = kErrorNone;
    Header   header;
    uint8_t  dscp;
    uint16_t payloadLength = aMessage.GetLength();

    if ((aIpProto == kProtoUdp) &&
        Get<Tmf::Agent>().IsTmfMessage(aMessageInfo.GetSockAddr(), aMessageInfo.GetPeerAddr(),
                                       aMessageInfo.GetPeerPort()))
    {
        dscp = Tmf::Agent::PriorityToDscp(aMessage.GetPriority());
    }
    else
    {
        dscp = PriorityToDscp(aMessage.GetPriority());
    }

    header.InitVersionTrafficClassFlow();
    header.SetDscp(dscp);
    header.SetEcn(aMessageInfo.GetEcn());
    header.SetPayloadLength(payloadLength);
    header.SetNextHeader(aIpProto);

    if (aMessageInfo.GetHopLimit() != 0 || aMessageInfo.ShouldAllowZeroHopLimit())
    {
        header.SetHopLimit(aMessageInfo.GetHopLimit());
    }
    else
    {
        header.SetHopLimit(kDefaultHopLimit);
    }

    if (aMessageInfo.GetSockAddr().IsUnspecified() || aMessageInfo.GetSockAddr().IsMulticast())
    {
        const Address *source = SelectSourceAddress(aMessageInfo.GetPeerAddr());

        VerifyOrExit(source != nullptr, error = kErrorInvalidSourceAddress);
        header.SetSource(*source);
    }
    else
    {
        header.SetSource(aMessageInfo.GetSockAddr());
    }

    header.SetDestination(aMessageInfo.GetPeerAddr());

    if (aMessageInfo.GetPeerAddr().IsRealmLocalMulticast())
    {
        SuccessOrExit(error = AddMplOption(aMessage, header));
    }

    SuccessOrExit(error = aMessage.Prepend(header));

    Checksum::UpdateMessageChecksum(aMessage, header.GetSource(), header.GetDestination(), aIpProto);

    if (aMessageInfo.GetPeerAddr().IsMulticastLargerThanRealmLocal())
    {
        SuccessOrExit(error = PrepareMulticastToLargerThanRealmLocal(aMessage, header));
    }

    aMessage.SetMulticastLoop(aMessageInfo.GetMulticastLoop());

    if (aMessage.GetLength() > kMaxDatagramLength)
    {
        error = FragmentDatagram(aMessage, aIpProto);
    }
    else
    {
        EnqueueDatagram(aMessage);
    }

exit:

    return error;
}

void Ip6::HandleSendQueue(void)
{
    Message *message;

    while ((message = mSendQueue.GetHead()) != nullptr)
    {
        mSendQueue.Dequeue(*message);
        IgnoreError(HandleDatagram(OwnedPtr<Message>(message)));
    }
}

Error Ip6::ReadHopByHopHeader(const Message &aMessage, OffsetRange &aOffsetRange, HopByHopHeader &aHbhHeader) const
{
    // Reads the HBH header from the message at the given offset range.
    // On success, updates `aOffsetRange` to indicate the location of
    // options within the HBH header.

    Error error;

    SuccessOrExit(error = aMessage.Read(aOffsetRange, aHbhHeader));
    VerifyOrExit(aOffsetRange.Contains(aHbhHeader.GetSize()), error = kErrorParse);
    aOffsetRange.ShrinkLength(aHbhHeader.GetSize());
    aOffsetRange.AdvanceOffset(sizeof(HopByHopHeader));

exit:
    return error;
}

Error Ip6::HandleOptions(Message &aMessage, const Header &aHeader, bool &aReceive, bool aIsHopByHop)
{
    Error          error        = kErrorNone;
    bool           hasMplOption = false;
    HopByHopHeader hbhHeader;
    Option         option;
    OffsetRange    offsetRange;
    MplOption      mplOption;

    offsetRange.InitFromMessageOffsetToEnd(aMessage);

    SuccessOrExit(error = ReadHopByHopHeader(aMessage, offsetRange, hbhHeader));

    // `ReadHopByHopHeader()` updates `offsetRange` to refer to the
    // location of the options within the HBH header. We first
    // validate all options, ensuring there is at most one MPL
    // option, before processing it.

    for (; !offsetRange.IsEmpty(); offsetRange.AdvanceOffset(option.GetSize()))
    {
        SuccessOrExit(error = option.ParseFrom(aMessage, offsetRange));

        if (option.IsPadding())
        {
            continue;
        }

        if (aIsHopByHop && (option.GetType() == MplOption::kType))
        {
            VerifyOrExit(!hasMplOption, error = kErrorDrop);
            hasMplOption = true;

            SuccessOrExit(error = mMpl.ReadAndValidateOption(aMessage, offsetRange, aHeader.GetSource(), mplOption));

            continue;
        }

        VerifyOrExit(option.GetAction() == Option::kActionSkip, error = kErrorDrop);
    }

    if (hasMplOption)
    {
        SuccessOrExit(error = mMpl.ProcessOption(aMessage, mplOption, aReceive));
    }

    aMessage.SetOffset(offsetRange.GetEndOffset());

exit:
    return error;
}

#if OPENTHREAD_CONFIG_IP6_FRAGMENTATION_ENABLE
Error Ip6::FragmentDatagram(Message &aMessage, uint8_t aIpProto)
{
    Error          error = kErrorNone;
    Header         header;
    FragmentHeader fragmentHeader;
    Message       *fragment        = nullptr;
    uint16_t       fragmentCnt     = 0;
    uint16_t       payloadFragment = 0;
    uint16_t       offset          = 0;

    uint16_t maxPayloadFragment =
        FragmentHeader::MakeDivisibleByEight(kMinimalMtu - aMessage.GetOffset() - sizeof(fragmentHeader));
    uint16_t payloadLeft = aMessage.DetermineLengthAfterOffset();

    SuccessOrExit(error = aMessage.Read(0, header));
    header.SetNextHeader(kProtoFragment);

    fragmentHeader.Init();
    fragmentHeader.SetIdentification(Random::NonCrypto::Generate<uint32_t>());
    fragmentHeader.SetNextHeader(aIpProto);
    fragmentHeader.SetMoreFlag();

    while (payloadLeft != 0)
    {
        if (payloadLeft < maxPayloadFragment)
        {
            fragmentHeader.ClearMoreFlag();

            payloadFragment = payloadLeft;
            payloadLeft     = 0;

            LogDebg("Last Fragment");
        }
        else
        {
            payloadLeft -= maxPayloadFragment;
            payloadFragment = maxPayloadFragment;
        }

        offset = fragmentCnt * FragmentHeader::BytesToFragmentOffset(maxPayloadFragment);
        fragmentHeader.SetOffset(offset);

        VerifyOrExit((fragment = NewMessage()) != nullptr, error = kErrorNoBufs);
        IgnoreError(fragment->SetPriority(aMessage.GetPriority()));
        SuccessOrExit(error = fragment->SetLength(aMessage.GetOffset() + sizeof(fragmentHeader) + payloadFragment));

        header.SetPayloadLength(payloadFragment + sizeof(fragmentHeader));
        fragment->Write(0, header);

        fragment->SetOffset(aMessage.GetOffset());
        fragment->Write(aMessage.GetOffset(), fragmentHeader);

        fragment->WriteBytesFromMessage(
            /* aWriteOffset */ aMessage.GetOffset() + sizeof(fragmentHeader), aMessage,
            /* aReadOffset */ aMessage.GetOffset() + FragmentHeader::FragmentOffsetToBytes(offset),
            /* aLength */ payloadFragment);

        EnqueueDatagram(*fragment);

        fragmentCnt++;
        fragment = nullptr;

        LogInfo("Fragment %d with %d bytes sent", fragmentCnt, payloadFragment);
    }

    aMessage.Free();

exit:

    if (error == kErrorNoBufs)
    {
        LogWarn("No buffer for Ip6 fragmentation");
    }

    FreeMessageOnError(fragment, error);
    return error;
}

Error Ip6::HandleFragment(Message &aMessage)
{
    Error          error = kErrorNone;
    Header         header, headerBuffer;
    FragmentHeader fragmentHeader;
    Message       *message         = nullptr;
    uint16_t       offset          = 0;
    uint16_t       payloadFragment = 0;
    bool           isFragmented    = true;

    SuccessOrExit(error = aMessage.Read(0, header));
    SuccessOrExit(error = aMessage.Read(aMessage.GetOffset(), fragmentHeader));

    if (fragmentHeader.GetOffset() == 0 && !fragmentHeader.IsMoreFlagSet())
    {
        isFragmented = false;
        aMessage.MoveOffset(sizeof(fragmentHeader));
        ExitNow();
    }

    for (Message &msg : mReassemblyList)
    {
        SuccessOrExit(error = msg.Read(0, headerBuffer));

        if (msg.GetDatagramTag() == fragmentHeader.GetIdentification() &&
            headerBuffer.GetSource() == header.GetSource() && headerBuffer.GetDestination() == header.GetDestination())
        {
            message = &msg;
            break;
        }
    }

    offset          = FragmentHeader::FragmentOffsetToBytes(fragmentHeader.GetOffset());
    payloadFragment = aMessage.GetLength() - aMessage.GetOffset() - sizeof(fragmentHeader);

    LogInfo("Fragment with id %lu received > %u bytes, offset %u", ToUlong(fragmentHeader.GetIdentification()),
            payloadFragment, offset);

    if (offset + payloadFragment + aMessage.GetOffset() > kMaxAssembledDatagramLength)
    {
        LogWarn("Packet too large for fragment buffer");
        ExitNow(error = kErrorNoBufs);
    }

    if (message == nullptr)
    {
        LogDebg("start reassembly");
        VerifyOrExit(offset == 0, error = kErrorDrop);
        VerifyOrExit((message = NewMessage()) != nullptr, error = kErrorNoBufs);
        mReassemblyList.Enqueue(*message);

        message->SetTimestampToNow();
        message->SetOffset(0);
        message->SetDatagramTag(fragmentHeader.GetIdentification());

        // copying the non-fragmentable header to the fragmentation buffer
        SuccessOrExit(error = message->AppendBytesFromMessage(aMessage, 0, aMessage.GetOffset()));

        Get<TimeTicker>().RegisterReceiver(TimeTicker::kIp6FragmentReassembler);
    }
    else
    {
        VerifyOrExit(offset == message->GetOffset(), error = kErrorDrop);
    }

    // increase message buffer if necessary
    if (message->GetLength() < offset + payloadFragment + aMessage.GetOffset())
    {
        SuccessOrExit(error = message->SetLength(offset + payloadFragment + aMessage.GetOffset()));
    }

    // copy the fragment payload into the message buffer
    message->WriteBytesFromMessage(
        /* aWriteOffset */ aMessage.GetOffset() + offset, aMessage,
        /* aReadOffset */ aMessage.GetOffset() + sizeof(fragmentHeader), /* aLength */ payloadFragment);

    message->SetOffset(offset + payloadFragment);

    // check if it is the last frame
    if (!fragmentHeader.IsMoreFlagSet())
    {
        // use the offset value for the whole ip message length
        message->SetOffset(aMessage.GetOffset() + offset + payloadFragment);

        // creates the header for the reassembled ipv6 package
        SuccessOrExit(error = aMessage.Read(0, header));
        header.SetPayloadLength(message->GetLength() - sizeof(header));
        header.SetNextHeader(fragmentHeader.GetNextHeader());
        message->Write(0, header);

        LogDebg("Reassembly complete.");

        mReassemblyList.Dequeue(*message);

        IgnoreError(HandleDatagram(OwnedPtr<Message>(message), /* aIsReassembled */ true));
    }

exit:
    if (error != kErrorDrop && error != kErrorNone && isFragmented)
    {
        if (message != nullptr)
        {
            mReassemblyList.DequeueAndFree(*message);
        }

        LogWarnOnError(error, "reassemble");
    }

    if (isFragmented)
    {
        // drop all fragments, the payload is stored in the fragment buffer
        error = kErrorDrop;
    }

    return error;
}

void Ip6::CleanupFragmentationBuffer(void) { mReassemblyList.DequeueAndFreeAll(); }

void Ip6::HandleTimeTick(void)
{
    UpdateReassemblyList();

    if (mReassemblyList.GetHead() == nullptr)
    {
        Get<TimeTicker>().UnregisterReceiver(TimeTicker::kIp6FragmentReassembler);
    }
}

void Ip6::UpdateReassemblyList(void)
{
    TimeMilli now = TimerMilli::GetNow();

    for (Message &message : mReassemblyList)
    {
        if (now - message.GetTimestamp() >= TimeMilli::SecToMsec(kReassemblyTimeout))
        {
            LogInfo("Reassembly timeout.");
            SendIcmpError(message, Icmp::Header::kTypeTimeExceeded, Icmp::Header::kCodeFragmReasTimeEx);

            mReassemblyList.DequeueAndFree(message);
        }
    }
}

void Ip6::SendIcmpError(Message &aMessage, Icmp::Header::Type aIcmpType, Icmp::Header::Code aIcmpCode)
{
    Error       error = kErrorNone;
    Header      header;
    MessageInfo messageInfo;

    SuccessOrExit(error = aMessage.Read(0, header));

    messageInfo.SetPeerAddr(header.GetSource());
    messageInfo.SetSockAddr(header.GetDestination());
    messageInfo.SetHopLimit(header.GetHopLimit());

    error = mIcmp.SendError(aIcmpType, aIcmpCode, messageInfo, aMessage);

exit:
    LogWarnOnError(error, "send ICMP");
    OT_UNUSED_VARIABLE(error);
}

#else
Error Ip6::FragmentDatagram(Message &aMessage, uint8_t aIpProto)
{
    OT_UNUSED_VARIABLE(aIpProto);

    EnqueueDatagram(aMessage);

    return kErrorNone;
}

Error Ip6::HandleFragment(Message &aMessage)
{
    Error          error;
    FragmentHeader fragmentHeader;

    SuccessOrExit(error = aMessage.ReadAtAndAdvanceOffset(fragmentHeader));

    VerifyOrExit(fragmentHeader.GetOffset() == 0 && !fragmentHeader.IsMoreFlagSet(), error = kErrorDrop);

exit:
    return error;
}
#endif // OPENTHREAD_CONFIG_IP6_FRAGMENTATION_ENABLE

Error Ip6::HandleExtensionHeaders(OwnedPtr<Message> &aMessagePtr,
                                  const Header      &aHeader,
                                  uint8_t           &aNextHeader,
                                  bool              &aReceive)
{
    Error           error = kErrorNone;
    ExtensionHeader extHeader;
    bool            first = true;

    while (aReceive || aNextHeader == kProtoHopOpts)
    {
        SuccessOrExit(error = aMessagePtr->Read(aMessagePtr->GetOffset(), extHeader));

        switch (aNextHeader)
        {
        case kProtoHopOpts:
            VerifyOrExit(first, error = kErrorDrop);
            SuccessOrExit(error = HandleOptions(*aMessagePtr, aHeader, aReceive, /* aIsHopByHop */ true));
            break;

        case kProtoDstOpts:
            SuccessOrExit(error = HandleOptions(*aMessagePtr, aHeader, aReceive, /* aIsHopByHop */ false));
            break;

        case kProtoFragment:
            IgnoreError(PassToHost(aMessagePtr, aHeader, aNextHeader, aReceive, kCopyMessageToUse));
            SuccessOrExit(error = HandleFragment(*aMessagePtr));
            break;

        case kProtoIp6:
            ExitNow();

        case kProtoRouting:
        case kProtoNone:
            ExitNow(error = kErrorDrop);

        default:
            ExitNow();
        }

        aNextHeader = extHeader.GetNextHeader();
        first       = false;
    }

exit:
    return error;
}

bool Ip6::HasIp6InIpTunnel(const Message &aMessage, uint8_t aNextHeader) const
{
    bool        hasTunnel = false;
    OffsetRange offsetRange;

    offsetRange.InitFromMessageOffsetToEnd(aMessage);

    while (!offsetRange.IsEmpty())
    {
        if (aNextHeader == kProtoIp6)
        {
            hasTunnel = true;
            break;
        }

        if (aNextHeader == kProtoHopOpts || aNextHeader == kProtoDstOpts || aNextHeader == kProtoRouting)
        {
            ExtensionHeader extHeader;
            uint16_t        size;

            SuccessOrExit(aMessage.Read(offsetRange, extHeader));
            size = extHeader.GetSize();
            VerifyOrExit(offsetRange.Contains(size));
            offsetRange.AdvanceOffset(size);
            aNextHeader = extHeader.GetNextHeader();
        }
        else if (aNextHeader == kProtoFragment)
        {
            FragmentHeader fragHeader;

            SuccessOrExit(aMessage.ReadAndAdvance(offsetRange, fragHeader));
            aNextHeader = fragHeader.GetNextHeader();
        }
        else
        {
            break;
        }
    }

exit:
    return hasTunnel;
}

Error Ip6::TakeOrCopyMessagePtr(OwnedPtr<Message> &aTargetPtr,
                                OwnedPtr<Message> &aMessagePtr,
                                MessageOwnership   aMessageOwnership)
{
    switch (aMessageOwnership)
    {
    case kTakeMessageCustody:
        aTargetPtr = aMessagePtr.PassOwnership();
        break;

    case kCopyMessageToUse:
        aTargetPtr.Reset(aMessagePtr->Clone<kNoReservedHeader>());
        break;
    }

    return (aTargetPtr != nullptr) ? kErrorNone : kErrorNoBufs;
}

Error Ip6::Receive(Header            &aIp6Header,
                   OwnedPtr<Message> &aMessagePtr,
                   uint8_t            aIpProto,
                   MessageOwnership   aMessageOwnership)
{
    Error             error = kErrorNone;
    OwnedPtr<Message> messagePtr;
    MessageInfo       messageInfo;

    messageInfo.Clear();
    messageInfo.SetPeerAddr(aIp6Header.GetSource());
    messageInfo.SetSockAddr(aIp6Header.GetDestination());
    messageInfo.SetHopLimit(aIp6Header.GetHopLimit());
    messageInfo.SetEcn(aIp6Header.GetEcn());

    switch (aIpProto)
    {
    case kProtoUdp:
    case kProtoIcmp6:
        break;
#if OPENTHREAD_CONFIG_TCP_ENABLE
    case kProtoTcp:
        break;
#endif
    default:
        ExitNow();
    }

    SuccessOrExit(error = TakeOrCopyMessagePtr(messagePtr, aMessagePtr, aMessageOwnership));

    switch (aIpProto)
    {
#if OPENTHREAD_CONFIG_TCP_ENABLE
    case kProtoTcp:
        error = mTcp.HandleMessage(aIp6Header, *messagePtr, messageInfo);
        break;
#endif
    case kProtoUdp:
        error = mUdp.HandleMessage(*messagePtr, messageInfo);
        break;

    case kProtoIcmp6:
        error = mIcmp.HandleMessage(*messagePtr, messageInfo);
        break;

    default:
        break;
    }

exit:
    LogWarnOnError(error, "handle payload");
    return error;
}

Error Ip6::PassToHost(OwnedPtr<Message> &aMessagePtr,
                      const Header      &aHeader,
                      uint8_t            aIpProto,
                      bool               aReceive,
                      MessageOwnership   aMessageOwnership)
{
    // This method passes the message to host by invoking the
    // registered IPv6 receive callback. When NAT64 is enabled, it
    // may also perform translation and invoke IPv4 receive
    // callback.

    Error             error = kErrorNone;
    OwnedPtr<Message> messagePtr;

    VerifyOrExit(aMessagePtr->IsLoopbackToHostAllowed());

    VerifyOrExit(mReceiveCallback.IsSet(), error = kErrorNoRoute);

    // Do not pass IPv6 packets that exceed kMinimalMtu.
    VerifyOrExit(aMessagePtr->GetLength() <= kMinimalMtu, error = kErrorDrop);

#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE && OPENTHREAD_CONFIG_BORDER_ROUTING_REACHABILITY_CHECK_ICMP6_ERROR_ENABLE
    if (!aReceive)
    {
        Get<BorderRouter::RoutingManager>().CheckReachabilityToSendIcmpError(*aMessagePtr, aHeader);
    }
#endif

    // If the sender used mesh-local address as source, do not pass to
    // host unless this message is intended for this device itself.
    if (Get<Mle::Mle>().IsMeshLocalAddress(aHeader.GetSource()))
    {
        VerifyOrExit(aReceive, error = kErrorDrop);
    }

    if (mReceiveFilterEnabled && aReceive && !aHeader.GetDestination().IsMulticastLargerThanRealmLocal())
    {
        switch (aIpProto)
        {
        case kProtoIcmp6:
            if (mIcmp.ShouldHandleEchoRequest(aHeader.GetDestination()))
            {
                Icmp::Header icmp;

                IgnoreError(aMessagePtr->Read(aMessagePtr->GetOffset(), icmp));
                VerifyOrExit(icmp.GetType() != Icmp::Header::kTypeEchoRequest, error = kErrorDrop);
            }

            break;

        case kProtoUdp:
        {
            Udp::Header udp;

            IgnoreError(aMessagePtr->Read(aMessagePtr->GetOffset(), udp));
            VerifyOrExit(!Get<Udp>().IsPortInUse(udp.GetDestinationPort()), error = kErrorNoRoute);
            break;
        }

#if OPENTHREAD_CONFIG_TCP_ENABLE
        // Do not pass TCP message to avoid dual processing from both
        // OpenThread and POSIX TCP stacks.
        case kProtoTcp:
            error = kErrorNoRoute;
            ExitNow();
#endif

        default:
            break;
        }
    }

    SuccessOrExit(error = TakeOrCopyMessagePtr(messagePtr, aMessagePtr, aMessageOwnership));

    IgnoreError(RemoveMplOption(*messagePtr));

#if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
    error = Get<Nat64::Translator>().TranslateIp6ToIp4(*messagePtr);

    if (error == kErrorAbort) // `kErrorAbort` indicates no translation was needed or performed.
    {
        error = kErrorNone;
    }
    else
    {
        SuccessOrExit(error);
        VerifyOrExit(mIp4ReceiveCallback.IsSet(), error = kErrorNoRoute);
        // Pass message to callback transferring its ownership.
        mIp4ReceiveCallback.Invoke(messagePtr.Release());
        ExitNow();
    }
#endif

#if OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE
    UpdateBorderRoutingCounters(aHeader, messagePtr->GetLength(), /* aIsInbound */ false);
#endif

#if OPENTHREAD_CONFIG_IP6_RESTRICT_FORWARDING_LARGER_SCOPE_MCAST_WITH_LOCAL_SRC
    // Some platforms (e.g. Android) currently doesn't restrict link-local/mesh-local source
    // addresses when forwarding multicast packets.
    // For a multicast packet sent from link-local/mesh-local address to scope larger
    // than realm-local, set the hop limit to 1 before sending to host, so this packet
    // will not be forwarded by host.
    if (aHeader.GetDestination().IsMulticastLargerThanRealmLocal() &&
        (aHeader.GetSource().IsLinkLocalUnicast() || (Get<Mle::Mle>().IsMeshLocalAddress(aHeader.GetSource()))))
    {
        messagePtr->Write<uint8_t>(Header::kHopLimitFieldOffset, 1);
    }
#endif

    // Pass message to callback transferring its ownership.
    mReceiveCallback.Invoke(messagePtr.Release());

exit:
    return error;
}

Error Ip6::SendRaw(OwnedPtr<Message> aMessagePtr)
{
    Error  error = kErrorNone;
    Header header;

    SuccessOrExit(error = header.ParseFrom(*aMessagePtr));
    VerifyOrExit(!header.GetSource().IsMulticast(), error = kErrorInvalidSourceAddress);

    aMessagePtr->SetOffset(sizeof(header));

    if (aMessagePtr->IsOriginHostUntrusted() && HasIp6InIpTunnel(*aMessagePtr, header.GetNextHeader()))
    {
        LogInfo("Dropping host-untrusted IP-in-IP packet");
        ExitNow(error = kErrorDrop);
    }

#if OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE
    // The filtering rules don't apply to packets from DUA.
    if (!Get<BackboneRouter::Leader>().IsDomainUnicast(header.GetSource()))
#endif
    {
        // When the packet is forwarded from host to Thread, if its source is on-mesh or its destination is
        // mesh-local, we'll drop the packet unless the packet originates from this device.
        if (Get<NetworkData::Leader>().IsOnMesh(header.GetSource()) ||
            Get<Mle::Mle>().IsMeshLocalAddress(header.GetDestination()))
        {
            VerifyOrExit(Get<ThreadNetif>().HasUnicastAddress(header.GetSource()), error = kErrorDrop);
        }
    }

    if (header.GetDestination().IsMulticast())
    {
        SuccessOrExit(error = InsertMplOption(*aMessagePtr, header));
    }

#if OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE
    UpdateBorderRoutingCounters(header, aMessagePtr->GetLength(), /* aIsInbound */ true);
#endif

    error = HandleDatagram(aMessagePtr.PassOwnership());

exit:
    return error;
}

void Ip6::DetermineAction(const Message &aMessage,
                          const Header  &aHeader,
                          bool          &aForwardThread,
                          bool          &aForwardHost,
                          bool          &aReceive) const
{
    // Determine `aForwardThread`, `aForwardHost` and `aReceive`
    // based on the destination address and message origin.

    uint16_t rloc16;

    aForwardThread = false;
    aForwardHost   = false;
    aReceive       = false;

    if (aHeader.GetDestination().IsMulticast())
    {
        // Destination is multicast

        if (aHeader.GetDestination().IsMulticastLargerThanRealmLocal())
        {
            // Multicast messages with scope larger than realm-local
            // use IP-in-IP encapsulation destined to the "All MPL
            // Forwarders" multicast address. Both the encapsulated
            // (outer) and embedded (inner) messages are processed.
            //
            // For the inner message, we set `aForwardThread` if the
            // device has a sleepy child that is subscribed to the
            // destination address. `MeshForwarder::SendMessage()` on
            // an FTD will then check for this and schedules the
            // indirect tx to such children. This is skipped on an MTD
            // (`aForwardThread` remains `false`) since an MTD cannot
            // have children.

#if OPENTHREAD_FTD
            aForwardThread = Get<ChildTable>().HasSleepyChildWithAddress(aHeader.GetDestination());
#endif
        }
        else
        {
            // For all other multicast messages, we forward to the
            // Thread network unless the message was received on the
            // Thread netif.

            aForwardThread = !aMessage.IsOriginThreadNetif();
        }

        // Always forward multicast packets to host network stack
        aForwardHost = true;

        // Determine `aReceive` for a multicast message destined for
        // an address to which this device is subscribed.
        //
        // 1) Accept if `MulticastLoop` is enabled.
        // 2) Otherwise, if it originates from the Thread Netif:
        //   - Always accept if the device is non-sleepy.
        //   - If sleepy, only if the source is NOT the SED itself.
        //
        // For non-sleepy devices, duplicate multicast detection is
        // handled in `Mpl::ProcessOption()`, which will update
        // `aReceive` if the message was seen before (based on the
        // MPL Seed).
        //
        // To optimize SED behavior, MPL processing is fully skipped
        // on SEDs. Therefore, the check above is added to handle the
        // specific case where an SED sends a multicast message to a
        // group it is subscribed to. The parent can send it back to
        // the SED (since the child is subscribed), we check and skip
        // processing it.

        if (Get<ThreadNetif>().IsMulticastSubscribed(aHeader.GetDestination()))
        {
            if (aMessage.GetMulticastLoop())
            {
                aReceive = true;
            }
            else if (aMessage.IsOriginThreadNetif())
            {
                if (Get<Mle::Mle>().IsRxOnWhenIdle())
                {
                    aReceive = true;
                }
                else
                {
                    aReceive = !Get<ThreadNetif>().HasUnicastAddress(aHeader.GetSource());
                }
            }
        }

        ExitNow();
    }

    // Destination is unicast

    if (Get<ThreadNetif>().HasUnicastAddress(aHeader.GetDestination()))
    {
        aReceive = true;
        ExitNow();
    }

    if (aHeader.GetDestination().IsLinkLocalUnicast())
    {
        // Forward a message with a link-local destination address
        // to thread, unless it is received on the Thread netif.

        aForwardThread = !aMessage.IsOriginThreadNetif();
        ExitNow();
    }

    if (IsOnLink(aHeader.GetDestination()))
    {
        aForwardThread = true;
        ExitNow();
    }

    if (Get<NetworkData::Leader>().RouteLookup(aHeader.GetSource(), aHeader.GetDestination(), rloc16) != kErrorNone)
    {
        // No route in mesh, forward to host (as a last resort).
        LogInfo("Failed to find valid route for: %s", aHeader.GetDestination().ToString().AsCString());
        aForwardHost = true;
        ExitNow();
    }

    // `RouteLookup()` found a destination within the mesh. If we are
    // the destination (device is acting as a Border Router), forward
    // it to the host. Otherwise, forward to Thread.

    if (Get<Mle::Mle>().HasRloc16(rloc16))
    {
        aForwardHost = true;
    }
    else
    {
        aForwardThread = true;
    }

exit:
    return;
}

Error Ip6::HandleDatagram(OwnedPtr<Message> aMessagePtr, bool aIsReassembled, uint8_t aRecursionDepth)
{
    Error  error;
    Header header;

    VerifyOrExit(aRecursionDepth <= kMaxRecursionDepth, error = kErrorDrop);
    bool    receive;
    bool    forwardThread;
    bool    forwardHost;
    uint8_t nextHeader;

    SuccessOrExit(error = header.ParseFrom(*aMessagePtr));

    if (!aMessagePtr->IsOriginHostTrusted())
    {
        VerifyOrExit(!header.GetSource().IsLoopback() && !header.GetDestination().IsLoopback(), error = kErrorDrop);
    }

    DetermineAction(*aMessagePtr, header, forwardThread, forwardHost, receive);

    aMessagePtr->SetOffset(sizeof(header));

    // Process IPv6 Extension Headers
    nextHeader = header.GetNextHeader();
    SuccessOrExit(error = HandleExtensionHeaders(aMessagePtr, header, nextHeader, receive));

    if (receive && (nextHeader == kProtoIp6))
    {
        // Process the embedded IPv6 message in an IPv6 tunnel message.
        // If we need to `forwardThread` we create a copy by cloning
        // the message, otherwise we take ownership of `aMessage`
        // itself and use it directly. The encapsulating header is
        // then removed before processing the embedded message.

        OwnedPtr<Message> messagePtr;
        bool              multicastLoop = aMessagePtr->GetMulticastLoop();

        SuccessOrExit(error = TakeOrCopyMessagePtr(messagePtr, aMessagePtr,
                                                   forwardThread ? kCopyMessageToUse : kTakeMessageCustody));
        messagePtr->SetMulticastLoop(multicastLoop);
        messagePtr->RemoveHeader(messagePtr->GetOffset());

        Get<MeshForwarder>().LogMessage(MeshForwarder::kMessageReceive, *messagePtr);

        SuccessOrExit(error = HandleDatagram(messagePtr.PassOwnership(), aIsReassembled, aRecursionDepth + 1));

        receive     = false;
        forwardHost = false;
    }

    if ((forwardHost || receive) && !aIsReassembled)
    {
        error = PassToHost(aMessagePtr, header, nextHeader, receive,
                           (receive || forwardThread) ? kCopyMessageToUse : kTakeMessageCustody);
    }

    if (receive)
    {
        error = Receive(header, aMessagePtr, nextHeader, forwardThread ? kCopyMessageToUse : kTakeMessageCustody);
    }

    if (forwardThread)
    {
        if (aMessagePtr->IsOriginThreadNetif())
        {
            VerifyOrExit(Get<Mle::Mle>().IsRouterOrLeader());
            header.SetHopLimit(header.GetHopLimit() - 1);
        }

        VerifyOrExit(header.GetHopLimit() > 0, error = kErrorDrop);

        aMessagePtr->Write<uint8_t>(Header::kHopLimitFieldOffset, header.GetHopLimit());

        if (nextHeader == kProtoIcmp6)
        {
            uint8_t icmpType;

            SuccessOrExit(error = aMessagePtr->Read(aMessagePtr->GetOffset(), icmpType));

            VerifyOrExit(DoesArrayContain(kForwardIcmpTypes, icmpType), error = kErrorDrop);
        }
#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
        if (mTmfOriginFilterEnabled)
#endif
        {
            if (aMessagePtr->IsOriginHostUntrusted() && (nextHeader == kProtoUdp))
            {
                Udp::Header udpHeader;

                SuccessOrExit(error = aMessagePtr->Read(aMessagePtr->GetOffset(), udpHeader));

                if (udpHeader.GetDestinationPort() == Tmf::kUdpPort)
                {
                    LogInfo("Dropping TMF message from untrusted origin");
                    ExitNow(error = kErrorDrop);
                }
            }
        }

#if OPENTHREAD_CONFIG_MULTI_RADIO
        // Since the message will be forwarded, we clear the radio
        // type on the message to allow the radio type for tx to be
        // selected later (based on the radios supported by the next
        // hop).
        aMessagePtr->ClearRadioType();
#endif

        Get<MeshForwarder>().SendMessage(aMessagePtr.PassOwnership());
    }

exit:
    return error;
}

Error Ip6::SelectSourceAddress(MessageInfo &aMessageInfo) const
{
    Error          error = kErrorNone;
    const Address *source;

    source = SelectSourceAddress(aMessageInfo.GetPeerAddr());
    VerifyOrExit(source != nullptr, error = kErrorNotFound);
    aMessageInfo.SetSockAddr(*source);

exit:
    return error;
}

const Address *Ip6::SelectSourceAddress(const Address &aDestination) const
{
    uint8_t                      destScope    = aDestination.GetScope();
    bool                         destIsRloc   = Get<Mle::Mle>().IsRoutingLocator(aDestination);
    const Netif::UnicastAddress *bestAddr     = nullptr;
    uint8_t                      bestMatchLen = 0;

    for (const Netif::UnicastAddress &addr : Get<ThreadNetif>().GetUnicastAddresses())
    {
        bool    newAddrIsPreferred = false;
        uint8_t matchLen;
        uint8_t overrideScope;

        if (Get<Mle::Mle>().IsAnycastLocator(addr.GetAddress()))
        {
            // Don't use anycast address as source address.
            continue;
        }

        matchLen = aDestination.PrefixMatch(addr.GetAddress());

        if (matchLen >= addr.mPrefixLength)
        {
            matchLen      = addr.mPrefixLength;
            overrideScope = addr.GetScope();
        }
        else
        {
            overrideScope = destScope;
        }

        if (addr.GetAddress() == aDestination)
        {
            // Rule 1: Prefer same address
            bestAddr = &addr;
            ExitNow();
        }

        if (bestAddr == nullptr)
        {
            newAddrIsPreferred = true;
        }
        else if (addr.GetScope() < bestAddr->GetScope())
        {
            // Rule 2: Prefer appropriate scope
            newAddrIsPreferred = (addr.GetScope() >= overrideScope);
        }
        else if (addr.GetScope() > bestAddr->GetScope())
        {
            newAddrIsPreferred = (bestAddr->GetScope() < overrideScope);
        }
        else if (addr.mPreferred != bestAddr->mPreferred)
        {
            // Rule 3: Avoid deprecated addresses
            newAddrIsPreferred = addr.mPreferred;
        }
        else if (matchLen > bestMatchLen)
        {
            // Rule 6: Prefer matching label
            // Rule 7: Prefer public address
            // Rule 8: Use longest prefix matching

            newAddrIsPreferred = true;
        }
        else if ((matchLen == bestMatchLen) && (destIsRloc == Get<Mle::Mle>().IsRoutingLocator(addr.GetAddress())))
        {
            // Additional rule: Prefer RLOC source for RLOC destination, EID source for anything else
            newAddrIsPreferred = true;
        }

        if (newAddrIsPreferred)
        {
            bestAddr     = &addr;
            bestMatchLen = matchLen;

            // Infer destination scope based on prefix match
            if (bestMatchLen >= bestAddr->mPrefixLength)
            {
                destScope = bestAddr->GetScope();
            }
        }
    }

exit:
    return (bestAddr != nullptr) ? &bestAddr->GetAddress() : nullptr;
}

bool Ip6::IsOnLink(const Address &aAddress) const
{
    bool isOnLink = false;

    if (Get<NetworkData::Leader>().IsOnMesh(aAddress))
    {
        ExitNow(isOnLink = true);
    }

    for (const Netif::UnicastAddress &unicastAddr : Get<ThreadNetif>().GetUnicastAddresses())
    {
        if (unicastAddr.GetAddress().PrefixMatch(aAddress) >= unicastAddr.mPrefixLength)
        {
            ExitNow(isOnLink = true);
        }
    }

exit:
    return isOnLink;
}

#if OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE

void Ip6::UpdateBorderRoutingCounters(const Header &aHeader, uint16_t aMessageLength, bool aIsInbound)
{
    static constexpr uint8_t kPrefixLength = 48;

    otPacketsAndBytes *counter         = nullptr;
    otPacketsAndBytes *internetCounter = nullptr;

    VerifyOrExit(!aHeader.GetSource().IsLinkLocalUnicast());
    VerifyOrExit(!aHeader.GetDestination().IsLinkLocalUnicast());
    VerifyOrExit(!Get<Mle::Mle>().IsMeshLocalAddress(aHeader.GetSource()));
    VerifyOrExit(!Get<Mle::Mle>().IsMeshLocalAddress(aHeader.GetDestination()));

    if (aIsInbound)
    {
        VerifyOrExit(!Get<Netif>().HasUnicastAddress(aHeader.GetSource()));

        if (!aHeader.GetSource().MatchesPrefix(aHeader.GetDestination().GetPrefix().m8, kPrefixLength))
        {
            internetCounter = &mBrCounters.mInboundInternet;
        }

        if (aHeader.GetDestination().IsMulticast())
        {
            VerifyOrExit(aHeader.GetDestination().IsMulticastLargerThanRealmLocal());
            counter = &mBrCounters.mInboundMulticast;
        }
        else
        {
            counter = &mBrCounters.mInboundUnicast;
        }
    }
    else
    {
        VerifyOrExit(!Get<Netif>().HasUnicastAddress(aHeader.GetDestination()));

        if (!aHeader.GetSource().MatchesPrefix(aHeader.GetDestination().GetPrefix().m8, kPrefixLength))
        {
            internetCounter = &mBrCounters.mOutboundInternet;
        }

        if (aHeader.GetDestination().IsMulticast())
        {
            VerifyOrExit(aHeader.GetDestination().IsMulticastLargerThanRealmLocal());
            counter = &mBrCounters.mOutboundMulticast;
        }
        else
        {
            counter = &mBrCounters.mOutboundUnicast;
        }
    }

exit:

    if (counter)
    {
        counter->mPackets++;
        counter->mBytes += aMessageLength;
    }
    if (internetCounter)
    {
        internetCounter->mPackets++;
        internetCounter->mBytes += aMessageLength;
    }
}

#endif // OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE

// LCOV_EXCL_START

const char *Ip6::IpProtoToString(uint8_t aIpProto)
{
    static constexpr Stringify::Entry kIpProtoTable[] = {
        {kProtoHopOpts, "HopOpts"}, {kProtoTcp, "TCP"},         {kProtoUdp, "UDP"},
        {kProtoIp6, "IP6"},         {kProtoRouting, "Routing"}, {kProtoFragment, "Frag"},
        {kProtoIcmp6, "ICMP6"},     {kProtoNone, "None"},       {kProtoDstOpts, "DstOpts"},
    };

    static_assert(Stringify::IsSorted(kIpProtoTable), "kIpProtoTable is not sorted");

    return Stringify::Lookup(aIpProto, kIpProtoTable, "Unknown");
}

const char *Ip6::EcnToString(Ecn aEcn)
{
#define EcnMapList(_)                \
    _(kEcnNotCapable, "no")          \
    _(kEcnCapable1, "e1") /* ECT1*/  \
    _(kEcnCapable0, "e0") /* ECT0 */ \
    _(kEcnMarked, "ce")   /* Congestion Encountered */

    DefineEnumStringArray(EcnMapList);

    return kStrings[aEcn];
}

// LCOV_EXCL_STOP

//---------------------------------------------------------------------------------------------------------------------
// Headers

Error Headers::ParseFrom(const Message &aMessage)
{
    Error error = kErrorParse;

    Clear();

    SuccessOrExit(mIp6Header.ParseFrom(aMessage));

    switch (mIp6Header.GetNextHeader())
    {
    case kProtoUdp:
        SuccessOrExit(aMessage.Read(sizeof(Header), mHeader.mUdp));
        break;
    case kProtoTcp:
        SuccessOrExit(aMessage.Read(sizeof(Header), mHeader.mTcp));
        break;
    case kProtoIcmp6:
        SuccessOrExit(aMessage.Read(sizeof(Header), mHeader.mIcmp));
        break;
    default:
        break;
    }

    error = kErrorNone;

exit:
    return error;
}

Error Headers::DecompressFrom(const Message &aMessage, uint16_t aOffset, const Mac::Addresses &aMacAddrs)
{
    static constexpr uint16_t kReadLength = sizeof(Lowpan::FragmentHeader::NextFrag) + sizeof(Headers);

    uint8_t   frameBuffer[kReadLength];
    uint16_t  frameLength;
    FrameData frameData;

    frameLength = aMessage.ReadBytes(aOffset, frameBuffer, sizeof(frameBuffer));
    frameData.Init(frameBuffer, frameLength);

    return DecompressFrom(frameData, aMacAddrs, aMessage.GetInstance());
}

Error Headers::DecompressFrom(const FrameData &aFrameData, const Mac::Addresses &aMacAddrs, Instance &aInstance)
{
    Error                  error     = kErrorNone;
    FrameData              frameData = aFrameData;
    Lowpan::FragmentHeader fragmentHeader;
    bool                   nextHeaderCompressed;

    if (fragmentHeader.ParseFrom(frameData) == kErrorNone)
    {
        // Only the first fragment header is followed by a LOWPAN_IPHC header
        VerifyOrExit(fragmentHeader.GetDatagramOffset() == 0, error = kErrorNotFound);
    }

    VerifyOrExit(Lowpan::Lowpan::IsLowpanHc(frameData), error = kErrorNotFound);

    SuccessOrExit(error = aInstance.Get<Lowpan::Lowpan>().DecompressBaseHeader(mIp6Header, nextHeaderCompressed,
                                                                               aMacAddrs, frameData));

    switch (mIp6Header.GetNextHeader())
    {
    case kProtoUdp:
        if (nextHeaderCompressed)
        {
            SuccessOrExit(error = aInstance.Get<Lowpan::Lowpan>().DecompressUdpHeader(mHeader.mUdp, frameData));
        }
        else
        {
            SuccessOrExit(error = frameData.Read(mHeader.mUdp));
        }
        break;

    case kProtoTcp:
        SuccessOrExit(error = frameData.Read(mHeader.mTcp));
        break;

    case kProtoIcmp6:
        SuccessOrExit(error = frameData.Read(mHeader.mIcmp));
        break;

    default:
        break;
    }

exit:
    return error;
}

uint16_t Headers::GetSourcePort(void) const
{
    uint16_t port = 0;

    switch (GetIpProto())
    {
    case kProtoUdp:
        port = mHeader.mUdp.GetSourcePort();
        break;

    case kProtoTcp:
        port = mHeader.mTcp.GetSourcePort();
        break;

    default:
        break;
    }

    return port;
}

void Headers::SetSourcePort(uint16_t aSrcPort)
{
    switch (GetIpProto())
    {
    case kProtoUdp:
        mHeader.mUdp.SetSourcePort(aSrcPort);
        break;

    case kProtoTcp:
        mHeader.mTcp.SetSourcePort(aSrcPort);
        break;

    default:
        break;
    }
}

uint16_t Headers::GetDestinationPort(void) const
{
    uint16_t port = 0;

    switch (GetIpProto())
    {
    case kProtoUdp:
        port = mHeader.mUdp.GetDestinationPort();
        break;

    case kProtoTcp:
        port = mHeader.mTcp.GetDestinationPort();
        break;

    default:
        break;
    }

    return port;
}

uint16_t Headers::GetChecksum(void) const
{
    uint16_t checksum = 0;

    switch (GetIpProto())
    {
    case kProtoUdp:
        checksum = mHeader.mUdp.GetChecksum();
        break;

    case kProtoTcp:
        checksum = mHeader.mTcp.GetChecksum();
        break;

    case kProtoIcmp6:
        checksum = mHeader.mIcmp.GetChecksum();
        break;

    default:
        break;
    }

    return checksum;
}

} // namespace Ip6
} // namespace ot
