//
// Copyright (c) 2006 Georgia Tech Research Corporation
//
// SPDX-License-Identifier: GPL-2.0-only
//
// Author: George F. Riley<riley@ece.gatech.edu>
//

#include "ipv4-l3-protocol.h"

#include "arp-cache.h"
#include "arp-l3-protocol.h"
#include "icmpv4-l4-protocol.h"
#include "ipv4-header.h"
#include "ipv4-interface.h"
#include "ipv4-raw-socket-impl.h"
#include "ipv4-route.h"
#include "loopback-net-device.h"

#include "ns3/boolean.h"
#include "ns3/callback.h"
#include "ns3/ipv4-address.h"
#include "ns3/log.h"
#include "ns3/net-device.h"
#include "ns3/node.h"
#include "ns3/object-vector.h"
#include "ns3/packet.h"
#include "ns3/socket.h"
#include "ns3/string.h"
#include "ns3/trace-source-accessor.h"
#include "ns3/traffic-control-layer.h"
#include "ns3/uinteger.h"

namespace ns3
{

NS_LOG_COMPONENT_DEFINE("Ipv4L3Protocol");

NS_OBJECT_ENSURE_REGISTERED(Ipv4L3Protocol);

TypeId
Ipv4L3Protocol::GetTypeId()
{
    static TypeId tid =
        TypeId("ns3::Ipv4L3Protocol")
            .SetParent<Ipv4>()
            .SetGroupName("Internet")
            .AddConstructor<Ipv4L3Protocol>()
            .AddAttribute("DefaultTtl",
                          "The TTL value set by default on "
                          "all outgoing packets generated on this node.",
                          UintegerValue(64),
                          MakeUintegerAccessor(&Ipv4L3Protocol::m_defaultTtl),
                          MakeUintegerChecker<uint8_t>())
            .AddAttribute("FragmentExpirationTimeout",
                          "When this timeout expires, the fragments "
                          "will be cleared from the buffer.",
                          TimeValue(Seconds(30)),
                          MakeTimeAccessor(&Ipv4L3Protocol::m_fragmentExpirationTimeout),
                          MakeTimeChecker())
            .AddAttribute("EnableDuplicatePacketDetection",
                          "Enable multicast duplicate packet detection based on RFC 6621",
                          BooleanValue(false),
                          MakeBooleanAccessor(&Ipv4L3Protocol::m_enableDpd),
                          MakeBooleanChecker())
            .AddAttribute("DuplicateExpire",
                          "Expiration delay for duplicate cache entries",
                          TimeValue(MilliSeconds(1)),
                          MakeTimeAccessor(&Ipv4L3Protocol::m_expire),
                          MakeTimeChecker())
            .AddAttribute("PurgeExpiredPeriod",
                          "Time between purges of expired duplicate packet entries, "
                          "0 means never purge",
                          TimeValue(Seconds(1)),
                          MakeTimeAccessor(&Ipv4L3Protocol::m_purge),
                          MakeTimeChecker(Seconds(0)))
            .AddTraceSource("Tx",
                            "Send ipv4 packet to outgoing interface.",
                            MakeTraceSourceAccessor(&Ipv4L3Protocol::m_txTrace),
                            "ns3::Ipv4L3Protocol::TxRxTracedCallback")
            .AddTraceSource("Rx",
                            "Receive ipv4 packet from incoming interface.",
                            MakeTraceSourceAccessor(&Ipv4L3Protocol::m_rxTrace),
                            "ns3::Ipv4L3Protocol::TxRxTracedCallback")
            .AddTraceSource("Drop",
                            "Drop ipv4 packet",
                            MakeTraceSourceAccessor(&Ipv4L3Protocol::m_dropTrace),
                            "ns3::Ipv4L3Protocol::DropTracedCallback")
            .AddAttribute("InterfaceList",
                          "The set of Ipv4 interfaces associated to this Ipv4 stack.",
                          ObjectVectorValue(),
                          MakeObjectVectorAccessor(&Ipv4L3Protocol::m_interfaces),
                          MakeObjectVectorChecker<Ipv4Interface>())

            .AddTraceSource("SendOutgoing",
                            "A newly-generated packet by this node is "
                            "about to be queued for transmission",
                            MakeTraceSourceAccessor(&Ipv4L3Protocol::m_sendOutgoingTrace),
                            "ns3::Ipv4L3Protocol::SentTracedCallback")
            .AddTraceSource("UnicastForward",
                            "A unicast IPv4 packet was received by this node "
                            "and is being forwarded to another node",
                            MakeTraceSourceAccessor(&Ipv4L3Protocol::m_unicastForwardTrace),
                            "ns3::Ipv4L3Protocol::SentTracedCallback")
            .AddTraceSource("MulticastForward",
                            "A multicast IPv4 packet was received by this node "
                            "and is being forwarded to another node",
                            MakeTraceSourceAccessor(&Ipv4L3Protocol::m_multicastForwardTrace),
                            "ns3::Ipv4L3Protocol::SentTracedCallback")
            .AddTraceSource("LocalDeliver",
                            "An IPv4 packet was received by/for this node, "
                            "and it is being forward up the stack",
                            MakeTraceSourceAccessor(&Ipv4L3Protocol::m_localDeliverTrace),
                            "ns3::Ipv4L3Protocol::SentTracedCallback")

        ;
    return tid;
}

Ipv4L3Protocol::Ipv4L3Protocol()
{
    NS_LOG_FUNCTION(this);
    m_ucb = MakeCallback(&Ipv4L3Protocol::IpForward, this);
    m_mcb = MakeCallback(&Ipv4L3Protocol::IpMulticastForward, this);
    m_lcb = MakeCallback(&Ipv4L3Protocol::LocalDeliver, this);
    m_ecb = MakeCallback(&Ipv4L3Protocol::RouteInputError, this);
}

Ipv4L3Protocol::~Ipv4L3Protocol()
{
    NS_LOG_FUNCTION(this);
}

void
Ipv4L3Protocol::Insert(Ptr<IpL4Protocol> protocol)
{
    NS_LOG_FUNCTION(this << protocol);
    L4ListKey_t key = std::make_pair(protocol->GetProtocolNumber(), -1);
    if (m_protocols.find(key) != m_protocols.end())
    {
        NS_LOG_WARN("Overwriting default protocol " << int(protocol->GetProtocolNumber()));
    }
    m_protocols[key] = protocol;
}

void
Ipv4L3Protocol::Insert(Ptr<IpL4Protocol> protocol, uint32_t interfaceIndex)
{
    NS_LOG_FUNCTION(this << protocol << interfaceIndex);

    L4ListKey_t key = std::make_pair(protocol->GetProtocolNumber(), interfaceIndex);
    if (m_protocols.find(key) != m_protocols.end())
    {
        NS_LOG_WARN("Overwriting protocol " << int(protocol->GetProtocolNumber())
                                            << " on interface " << int(interfaceIndex));
    }
    m_protocols[key] = protocol;
}

void
Ipv4L3Protocol::Remove(Ptr<IpL4Protocol> protocol)
{
    NS_LOG_FUNCTION(this << protocol);

    L4ListKey_t key = std::make_pair(protocol->GetProtocolNumber(), -1);
    auto iter = m_protocols.find(key);
    if (iter == m_protocols.end())
    {
        NS_LOG_WARN("Trying to remove an non-existent default protocol "
                    << int(protocol->GetProtocolNumber()));
    }
    else
    {
        m_protocols.erase(key);
    }
}

void
Ipv4L3Protocol::Remove(Ptr<IpL4Protocol> protocol, uint32_t interfaceIndex)
{
    NS_LOG_FUNCTION(this << protocol << interfaceIndex);

    L4ListKey_t key = std::make_pair(protocol->GetProtocolNumber(), interfaceIndex);
    auto iter = m_protocols.find(key);
    if (iter == m_protocols.end())
    {
        NS_LOG_WARN("Trying to remove an non-existent protocol "
                    << int(protocol->GetProtocolNumber()) << " on interface "
                    << int(interfaceIndex));
    }
    else
    {
        m_protocols.erase(key);
    }
}

Ptr<IpL4Protocol>
Ipv4L3Protocol::GetProtocol(int protocolNumber) const
{
    return GetProtocol(protocolNumber, -1);
}

Ptr<IpL4Protocol>
Ipv4L3Protocol::GetProtocol(int protocolNumber, int32_t interfaceIndex) const
{
    if (interfaceIndex >= 0)
    {
        // try the interface-specific protocol.
        auto key = std::make_pair(protocolNumber, interfaceIndex);
        auto i = m_protocols.find(key);
        if (i != m_protocols.end())
        {
            return i->second;
        }
    }
    // try the generic protocol.
    auto key = std::make_pair(protocolNumber, -1);
    auto i = m_protocols.find(key);
    if (i != m_protocols.end())
    {
        return i->second;
    }

    return nullptr;
}

void
Ipv4L3Protocol::SetNode(Ptr<Node> node)
{
    NS_LOG_FUNCTION(this << node);
    m_node = node;
    // Add a LoopbackNetDevice if needed, and an Ipv4Interface on top of it
    SetupLoopback();
}

Ptr<Socket>
Ipv4L3Protocol::CreateRawSocket()
{
    NS_LOG_FUNCTION(this);
    Ptr<Ipv4RawSocketImpl> socket = CreateObject<Ipv4RawSocketImpl>();
    socket->SetNode(m_node);
    m_sockets.push_back(socket);
    return socket;
}

void
Ipv4L3Protocol::DeleteRawSocket(Ptr<Socket> socket)
{
    NS_LOG_FUNCTION(this << socket);
    for (auto i = m_sockets.begin(); i != m_sockets.end(); ++i)
    {
        if ((*i) == socket)
        {
            m_sockets.erase(i);
            return;
        }
    }
}

/*
 * This method is called by AggregateObject and completes the aggregation
 * by setting the node in the ipv4 stack
 */
void
Ipv4L3Protocol::NotifyNewAggregate()
{
    NS_LOG_FUNCTION(this);
    if (!m_node)
    {
        Ptr<Node> node = this->GetObject<Node>();
        // verify that it's a valid node and that
        // the node has not been set before
        if (node)
        {
            this->SetNode(node);
        }
    }
    Ipv4::NotifyNewAggregate();
}

void
Ipv4L3Protocol::SetRoutingProtocol(Ptr<Ipv4RoutingProtocol> routingProtocol)
{
    NS_LOG_FUNCTION(this << routingProtocol);
    m_routingProtocol = routingProtocol;
    m_routingProtocol->SetIpv4(this);
}

Ptr<Ipv4RoutingProtocol>
Ipv4L3Protocol::GetRoutingProtocol() const
{
    return m_routingProtocol;
}

void
Ipv4L3Protocol::DoDispose()
{
    NS_LOG_FUNCTION(this);
    for (auto i = m_protocols.begin(); i != m_protocols.end(); ++i)
    {
        i->second = nullptr;
    }
    m_protocols.clear();

    for (auto i = m_interfaces.begin(); i != m_interfaces.end(); ++i)
    {
        *i = nullptr;
    }
    m_interfaces.clear();
    m_reverseInterfacesContainer.clear();

    m_sockets.clear();
    m_node = nullptr;
    m_routingProtocol = nullptr;

    for (auto it = m_fragments.begin(); it != m_fragments.end(); it++)
    {
        it->second = nullptr;
    }

    m_fragments.clear();
    m_timeoutEventList.clear();
    if (m_timeoutEvent.IsPending())
    {
        m_timeoutEvent.Cancel();
    }

    if (m_cleanDpd.IsPending())
    {
        m_cleanDpd.Cancel();
    }
    m_dups.clear();

    Object::DoDispose();
}

void
Ipv4L3Protocol::SetupLoopback()
{
    NS_LOG_FUNCTION(this);

    Ptr<Ipv4Interface> interface = CreateObject<Ipv4Interface>();
    Ptr<LoopbackNetDevice> device = nullptr;
    // First check whether an existing LoopbackNetDevice exists on the node
    for (uint32_t i = 0; i < m_node->GetNDevices(); i++)
    {
        if ((device = DynamicCast<LoopbackNetDevice>(m_node->GetDevice(i))))
        {
            break;
        }
    }
    if (!device)
    {
        device = CreateObject<LoopbackNetDevice>();
        m_node->AddDevice(device);
    }
    interface->SetDevice(device);
    interface->SetNode(m_node);
    Ipv4InterfaceAddress ifaceAddr =
        Ipv4InterfaceAddress(Ipv4Address::GetLoopback(), Ipv4Mask::GetLoopback());
    interface->AddAddress(ifaceAddr);
    uint32_t index = AddIpv4Interface(interface);
    Ptr<Node> node = GetObject<Node>();
    node->RegisterProtocolHandler(MakeCallback(&Ipv4L3Protocol::Receive, this),
                                  Ipv4L3Protocol::PROT_NUMBER,
                                  device);
    interface->SetUp();
    if (m_routingProtocol)
    {
        m_routingProtocol->NotifyInterfaceUp(index);
    }
}

void
Ipv4L3Protocol::SetDefaultTtl(uint8_t ttl)
{
    NS_LOG_FUNCTION(this << static_cast<uint32_t>(ttl));
    m_defaultTtl = ttl;
}

uint32_t
Ipv4L3Protocol::AddInterface(Ptr<NetDevice> device)
{
    NS_LOG_FUNCTION(this << device);
    NS_ASSERT(m_node);

    Ptr<TrafficControlLayer> tc = m_node->GetObject<TrafficControlLayer>();

    NS_ASSERT(tc);

    m_node->RegisterProtocolHandler(MakeCallback(&TrafficControlLayer::Receive, tc),
                                    Ipv4L3Protocol::PROT_NUMBER,
                                    device);
    m_node->RegisterProtocolHandler(MakeCallback(&TrafficControlLayer::Receive, tc),
                                    ArpL3Protocol::PROT_NUMBER,
                                    device);

    tc->RegisterProtocolHandler(MakeCallback(&Ipv4L3Protocol::Receive, this),
                                Ipv4L3Protocol::PROT_NUMBER,
                                device);
    tc->RegisterProtocolHandler(
        MakeCallback(&ArpL3Protocol::Receive, PeekPointer(GetObject<ArpL3Protocol>())),
        ArpL3Protocol::PROT_NUMBER,
        device);

    Ptr<Ipv4Interface> interface = CreateObject<Ipv4Interface>();
    interface->SetNode(m_node);
    interface->SetDevice(device);
    interface->SetTrafficControl(tc);
    interface->SetForwarding(m_ipForward);
    return AddIpv4Interface(interface);
}

uint32_t
Ipv4L3Protocol::AddIpv4Interface(Ptr<Ipv4Interface> interface)
{
    NS_LOG_FUNCTION(this << interface);
    uint32_t index = m_interfaces.size();
    m_interfaces.push_back(interface);
    m_reverseInterfacesContainer[interface->GetDevice()] = index;
    return index;
}

Ptr<Ipv4Interface>
Ipv4L3Protocol::GetInterface(uint32_t index) const
{
    if (index < m_interfaces.size())
    {
        return m_interfaces[index];
    }
    return nullptr;
}

uint32_t
Ipv4L3Protocol::GetNInterfaces() const
{
    return m_interfaces.size();
}

int32_t
Ipv4L3Protocol::GetInterfaceForAddress(Ipv4Address address) const
{
    int32_t interface = 0;
    for (auto i = m_interfaces.begin(); i != m_interfaces.end(); i++, interface++)
    {
        for (uint32_t j = 0; j < (*i)->GetNAddresses(); j++)
        {
            if ((*i)->GetAddress(j).GetLocal() == address)
            {
                return interface;
            }
        }
    }

    return -1;
}

int32_t
Ipv4L3Protocol::GetInterfaceForPrefix(Ipv4Address address, Ipv4Mask mask) const
{
    int32_t interface = 0;
    for (auto i = m_interfaces.begin(); i != m_interfaces.end(); i++, interface++)
    {
        for (uint32_t j = 0; j < (*i)->GetNAddresses(); j++)
        {
            if ((*i)->GetAddress(j).GetLocal().CombineMask(mask) == address.CombineMask(mask))
            {
                return interface;
            }
        }
    }

    return -1;
}

int32_t
Ipv4L3Protocol::GetInterfaceForDevice(Ptr<const NetDevice> device) const
{
    auto iter = m_reverseInterfacesContainer.find(device);
    if (iter != m_reverseInterfacesContainer.end())
    {
        return (*iter).second;
    }

    return -1;
}

bool
Ipv4L3Protocol::IsDestinationAddress(Ipv4Address address, uint32_t iif) const
{
    // First check the incoming interface for a unicast address match
    for (uint32_t i = 0; i < GetNAddresses(iif); i++)
    {
        Ipv4InterfaceAddress iaddr = GetAddress(iif, i);
        if (address == iaddr.GetLocal())
        {
            NS_LOG_LOGIC("For me (destination " << address << " match)");
            return true;
        }
        if (address == iaddr.GetBroadcast())
        {
            NS_LOG_LOGIC("For me (interface broadcast address)");
            return true;
        }
    }

    if (address.IsMulticast())
    {
#ifdef NOTYET
        if (MulticastCheckGroup(iif, address))
#endif
        {
            NS_LOG_LOGIC("For me (Ipv4Addr multicast address)");
            return true;
        }
    }

    if (address.IsBroadcast())
    {
        NS_LOG_LOGIC("For me (Ipv4Addr broadcast address)");
        return true;
    }

    if (!GetStrongEndSystemModel()) // Check other interfaces
    {
        for (uint32_t j = 0; j < GetNInterfaces(); j++)
        {
            if (j == uint32_t(iif))
            {
                continue;
            }
            for (uint32_t i = 0; i < GetNAddresses(j); i++)
            {
                Ipv4InterfaceAddress iaddr = GetAddress(j, i);
                if (address == iaddr.GetLocal())
                {
                    NS_LOG_LOGIC("For me (destination " << address
                                                        << " match) on another interface");
                    return true;
                }
                //  This is a small corner case:  match another interface's broadcast address
                if (address == iaddr.GetBroadcast())
                {
                    NS_LOG_LOGIC("For me (interface broadcast address on another interface)");
                    return true;
                }
            }
        }
    }
    return false;
}

void
Ipv4L3Protocol::Receive(Ptr<NetDevice> device,
                        Ptr<const Packet> p,
                        uint16_t protocol,
                        const Address& from,
                        const Address& to,
                        NetDevice::PacketType packetType)
{
    NS_LOG_FUNCTION(this << device << p << protocol << from << to << packetType);

    NS_LOG_LOGIC("Packet from " << from << " received on node " << m_node->GetId());

    int32_t interface = GetInterfaceForDevice(device);
    NS_ASSERT_MSG(interface != -1, "Received a packet from an interface that is not known to IPv4");

    Ptr<Packet> packet = p->Copy();

    Ptr<Ipv4Interface> ipv4Interface = m_interfaces[interface];

    if (ipv4Interface->IsUp())
    {
        m_rxTrace(packet, this, interface);
    }
    else
    {
        NS_LOG_LOGIC("Dropping received packet -- interface is down");
        Ipv4Header ipHeader;
        packet->RemoveHeader(ipHeader);
        m_dropTrace(ipHeader, packet, DROP_INTERFACE_DOWN, this, interface);
        return;
    }

    Ipv4Header ipHeader;
    if (Node::ChecksumEnabled())
    {
        ipHeader.EnableChecksum();
    }
    packet->RemoveHeader(ipHeader);

    // Trim any residual frame padding from underlying devices
    if (ipHeader.GetPayloadSize() < packet->GetSize())
    {
        packet->RemoveAtEnd(packet->GetSize() - ipHeader.GetPayloadSize());
    }

    if (!ipHeader.IsChecksumOk())
    {
        NS_LOG_LOGIC("Dropping received packet -- checksum not ok");
        m_dropTrace(ipHeader, packet, DROP_BAD_CHECKSUM, this, interface);
        return;
    }

    // the packet is valid, we update the ARP cache entry (if present)
    Ptr<ArpCache> arpCache = ipv4Interface->GetArpCache();
    if (arpCache)
    {
        // case one, it's a a direct routing.
        ArpCache::Entry* entry = arpCache->Lookup(ipHeader.GetSource());
        if (entry)
        {
            if (entry->IsAlive())
            {
                entry->UpdateSeen();
            }
        }
        else
        {
            // It's not in the direct routing, so it's the router, and it could have multiple IP
            // addresses. In doubt, update all of them. Note: it's a confirmed behavior for Linux
            // routers.
            std::list<ArpCache::Entry*> entryList = arpCache->LookupInverse(from);
            for (auto iter = entryList.begin(); iter != entryList.end(); iter++)
            {
                if ((*iter)->IsAlive())
                {
                    (*iter)->UpdateSeen();
                }
            }
        }
    }

    if (m_enableDpd && ipHeader.GetDestination().IsMulticast() && UpdateDuplicate(packet, ipHeader))
    {
        NS_LOG_LOGIC("Dropping received packet -- duplicate.");
        m_dropTrace(ipHeader, packet, DROP_DUPLICATE, this, interface);
        return;
    }

    NS_ASSERT_MSG(m_routingProtocol, "Need a routing protocol object to process packets");
    if (!m_routingProtocol->RouteInput(packet, ipHeader, device, m_ucb, m_mcb, m_lcb, m_ecb))
    {
        NS_LOG_WARN("No route found for forwarding packet.  Drop.");
        m_dropTrace(ipHeader, packet, DROP_NO_ROUTE, this, interface);
    }
}

Ptr<Icmpv4L4Protocol>
Ipv4L3Protocol::GetIcmp() const
{
    Ptr<IpL4Protocol> prot = GetProtocol(Icmpv4L4Protocol::GetStaticProtocolNumber());
    if (prot)
    {
        return prot->GetObject<Icmpv4L4Protocol>();
    }
    else
    {
        return nullptr;
    }
}

bool
Ipv4L3Protocol::IsUnicast(Ipv4Address ad) const
{
    if (ad.IsBroadcast() || ad.IsMulticast())
    {
        return false;
    }
    else
    {
        // check for subnet-broadcast
        for (uint32_t ifaceIndex = 0; ifaceIndex < GetNInterfaces(); ifaceIndex++)
        {
            for (uint32_t j = 0; j < GetNAddresses(ifaceIndex); j++)
            {
                Ipv4InterfaceAddress ifAddr = GetAddress(ifaceIndex, j);
                NS_LOG_LOGIC("Testing address " << ad << " with subnet-directed broadcast "
                                                << ifAddr.GetBroadcast());
                if (ad == ifAddr.GetBroadcast())
                {
                    return false;
                }
            }
        }
    }

    return true;
}

bool
Ipv4L3Protocol::IsUnicast(Ipv4Address ad, Ipv4Mask interfaceMask) const
{
    return !ad.IsMulticast() && !ad.IsSubnetDirectedBroadcast(interfaceMask);
}

void
Ipv4L3Protocol::SendWithHeader(Ptr<Packet> packet, Ipv4Header ipHeader, Ptr<Ipv4Route> route)
{
    NS_LOG_FUNCTION(this << packet << ipHeader << route);
    if (Node::ChecksumEnabled())
    {
        ipHeader.EnableChecksum();
    }
    SendRealOut(route, packet, ipHeader);
}

void
Ipv4L3Protocol::CallTxTrace(const Ipv4Header& ipHeader,
                            Ptr<Packet> packet,
                            Ptr<Ipv4> ipv4,
                            uint32_t interface)
{
    if (!m_txTrace.IsEmpty())
    {
        Ptr<Packet> packetCopy = packet->Copy();
        packetCopy->AddHeader(ipHeader);
        m_txTrace(packetCopy, ipv4, interface);
    }
}

void
Ipv4L3Protocol::Send(Ptr<Packet> packet,
                     Ipv4Address source,
                     Ipv4Address destination,
                     uint8_t protocol,
                     Ptr<Ipv4Route> route)
{
    NS_LOG_FUNCTION(this << packet << source << destination << uint32_t(protocol) << route);

    bool mayFragment = true;

    // we need a copy of the packet with its tags in case we need to invoke recursion.
    Ptr<Packet> pktCopyWithTags = packet->Copy();

    uint8_t ttl = m_defaultTtl;
    SocketIpTtlTag ipTtlTag;
    bool ipTtlTagFound = packet->RemovePacketTag(ipTtlTag);
    if (ipTtlTagFound)
    {
        ttl = ipTtlTag.GetTtl();
    }

    uint8_t tos = 0;
    SocketIpTosTag ipTosTag;
    bool ipTosTagFound = packet->RemovePacketTag(ipTosTag);
    if (ipTosTagFound)
    {
        tos = ipTosTag.GetTos();
    }

    // can construct the header here
    Ipv4Header ipHeader =
        BuildHeader(source, destination, protocol, packet->GetSize(), ttl, tos, mayFragment);

    // Handle a few cases:
    // 1) packet is passed in with a route entry
    // 2) packet is passed without a route and packet is destined to limited broadcast address
    // 3) packet is passed without a route and packet is destined to a subnet-directed broadcast
    // address 4) packet is passed without a route, packet is not broadcast (e.g., a raw socket
    // call, or ICMP)

    // 1) packet is passed in with route entry
    if (route)
    {
        NS_LOG_LOGIC("Ipv4L3Protocol::Send case 1b:  passed in with route and valid gateway");
        int32_t interface = GetInterfaceForDevice(route->GetOutputDevice());
        m_sendOutgoingTrace(ipHeader, packet, interface);
        if (m_enableDpd && ipHeader.GetDestination().IsMulticast())
        {
            UpdateDuplicate(packet, ipHeader);
        }
        SendRealOut(route, packet->Copy(), ipHeader);
        return;
    }

    // 2) packet is destined to limited broadcast address or link-local multicast address
    if (destination.IsBroadcast() || destination.IsLocalMulticast())
    {
        NS_LOG_LOGIC("Ipv4L3Protocol::Send case 2:  limited broadcast - no route");
        uint32_t ifaceIndex = 0;
        for (auto ifaceIter = m_interfaces.begin(); ifaceIter != m_interfaces.end();
             ifaceIter++, ifaceIndex++)
        {
            Ptr<Ipv4Interface> outInterface = *ifaceIter;
            // ANY source matches any interface
            bool sendIt = source.IsAny();
            // check if some specific address on outInterface matches
            for (uint32_t index = 0; !sendIt && index < outInterface->GetNAddresses(); index++)
            {
                if (outInterface->GetAddress(index).GetLocal() == source)
                {
                    sendIt = true;
                }
            }

            if (sendIt)
            {
                // create a proxy route for this interface
                Ptr<Ipv4Route> route = Create<Ipv4Route>();
                route->SetDestination(destination);
                route->SetGateway(Ipv4Address::GetAny());
                route->SetSource(source);
                route->SetOutputDevice(outInterface->GetDevice());
                DecreaseIdentification(source, destination, protocol);
                Send(pktCopyWithTags, source, destination, protocol, route);
            }
        }
        return;
    }

    // 3) check: packet is destined to a subnet-directed broadcast address
    for (auto ifaceIter = m_interfaces.begin(); ifaceIter != m_interfaces.end(); ifaceIter++)
    {
        Ptr<Ipv4Interface> outInterface = *ifaceIter;
        uint32_t ifaceIndex = GetInterfaceForDevice(outInterface->GetDevice());
        for (uint32_t j = 0; j < GetNAddresses(ifaceIndex); j++)
        {
            Ipv4InterfaceAddress ifAddr = GetAddress(ifaceIndex, j);
            NS_LOG_LOGIC("Testing address " << ifAddr.GetLocal() << " with mask "
                                            << ifAddr.GetMask());
            if (destination.IsSubnetDirectedBroadcast(ifAddr.GetMask()) &&
                destination.CombineMask(ifAddr.GetMask()) ==
                    ifAddr.GetLocal().CombineMask(ifAddr.GetMask()))
            {
                NS_LOG_LOGIC("Ipv4L3Protocol::Send case 3:  subnet directed bcast to "
                             << ifAddr.GetLocal() << " - no route");
                // create a proxy route for this interface
                Ptr<Ipv4Route> route = Create<Ipv4Route>();
                route->SetDestination(destination);
                route->SetGateway(Ipv4Address::GetAny());
                route->SetSource(source);
                route->SetOutputDevice(outInterface->GetDevice());
                DecreaseIdentification(source, destination, protocol);
                Send(pktCopyWithTags, source, destination, protocol, route);
                return;
            }
        }
    }

    // 4) packet is not broadcast, and route is NULL (e.g., a raw socket call)
    NS_LOG_LOGIC("Ipv4L3Protocol::Send case 4:  not broadcast and passed in with no route "
                 << destination);
    Socket::SocketErrno errno_;
    Ptr<NetDevice> oif(nullptr); // unused for now
    Ptr<Ipv4Route> newRoute;
    if (m_routingProtocol)
    {
        newRoute = m_routingProtocol->RouteOutput(pktCopyWithTags, ipHeader, oif, errno_);
    }
    else
    {
        NS_LOG_ERROR("Ipv4L3Protocol::Send: m_routingProtocol == 0");
    }
    if (newRoute)
    {
        DecreaseIdentification(source, destination, protocol);
        Send(pktCopyWithTags, source, destination, protocol, newRoute);
    }
    else
    {
        NS_LOG_WARN("No route to host.  Drop.");
        m_dropTrace(ipHeader, packet, DROP_NO_ROUTE, this, 0);
        DecreaseIdentification(source, destination, protocol);
    }
}

void
Ipv4L3Protocol::DecreaseIdentification(Ipv4Address source,
                                       Ipv4Address destination,
                                       uint8_t protocol)
{
    uint64_t src = source.Get();
    uint64_t dst = destination.Get();
    uint64_t srcDst = dst | (src << 32);
    std::pair<uint64_t, uint8_t> key = std::make_pair(srcDst, protocol);
    m_identification[key]--;
}

Ipv4Header
Ipv4L3Protocol::BuildHeader(Ipv4Address source,
                            Ipv4Address destination,
                            uint8_t protocol,
                            uint16_t payloadSize,
                            uint8_t ttl,
                            uint8_t tos,
                            bool mayFragment)
{
    NS_LOG_FUNCTION(this << source << destination << (uint16_t)protocol << payloadSize
                         << (uint16_t)ttl << (uint16_t)tos << mayFragment);
    Ipv4Header ipHeader;
    ipHeader.SetSource(source);
    ipHeader.SetDestination(destination);
    ipHeader.SetProtocol(protocol);
    ipHeader.SetPayloadSize(payloadSize);
    ipHeader.SetTtl(ttl);
    ipHeader.SetTos(tos);

    uint64_t src = source.Get();
    uint64_t dst = destination.Get();
    uint64_t srcDst = dst | (src << 32);
    std::pair<uint64_t, uint8_t> key = std::make_pair(srcDst, protocol);

    if (mayFragment)
    {
        ipHeader.SetMayFragment();
        ipHeader.SetIdentification(m_identification[key]);
        m_identification[key]++;
    }
    else
    {
        ipHeader.SetDontFragment();
        // RFC 6864 does not state anything about atomic datagrams
        // identification requirement:
        // >> Originating sources MAY set the IPv4 ID field of atomic datagrams
        //    to any value.
        ipHeader.SetIdentification(m_identification[key]);
        m_identification[key]++;
    }
    if (Node::ChecksumEnabled())
    {
        ipHeader.EnableChecksum();
    }
    return ipHeader;
}

void
Ipv4L3Protocol::SendRealOut(Ptr<Ipv4Route> route, Ptr<Packet> packet, const Ipv4Header& ipHeader)
{
    NS_LOG_FUNCTION(this << route << packet << &ipHeader);
    if (!route)
    {
        NS_LOG_WARN("No route to host.  Drop.");
        m_dropTrace(ipHeader, packet, DROP_NO_ROUTE, this, 0);
        return;
    }
    Ptr<NetDevice> outDev = route->GetOutputDevice();
    int32_t interface = GetInterfaceForDevice(outDev);
    NS_ASSERT(interface >= 0);
    Ptr<Ipv4Interface> outInterface = GetInterface(interface);
    NS_LOG_LOGIC("Send via NetDevice ifIndex " << outDev->GetIfIndex() << " ipv4InterfaceIndex "
                                               << interface);

    Ipv4Address target;
    std::string targetLabel;
    if (route->GetGateway().IsAny())
    {
        target = ipHeader.GetDestination();
        targetLabel = "destination";
    }
    else
    {
        target = route->GetGateway();
        targetLabel = "gateway";
    }

    if (outInterface->IsUp())
    {
        NS_LOG_LOGIC("Send to " << targetLabel << " " << target);
        if (packet->GetSize() + ipHeader.GetSerializedSize() > outInterface->GetDevice()->GetMtu())
        {
            std::list<Ipv4PayloadHeaderPair> listFragments;
            DoFragmentation(packet, ipHeader, outInterface->GetDevice()->GetMtu(), listFragments);
            for (auto it = listFragments.begin(); it != listFragments.end(); it++)
            {
                NS_LOG_LOGIC("Sending fragment " << *(it->first));
                CallTxTrace(it->second, it->first, this, interface);
                outInterface->Send(it->first, it->second, target);
            }
        }
        else
        {
            CallTxTrace(ipHeader, packet, this, interface);
            outInterface->Send(packet, ipHeader, target);
        }
    }
}

// This function analogous to Linux ip_mr_forward()
void
Ipv4L3Protocol::IpMulticastForward(Ptr<Ipv4MulticastRoute> mrtentry,
                                   Ptr<const Packet> p,
                                   const Ipv4Header& header)
{
    NS_LOG_FUNCTION(this << mrtentry << p << header);
    NS_LOG_LOGIC("Multicast forwarding logic for node: " << m_node->GetId());

    std::map<uint32_t, uint32_t> ttlMap = mrtentry->GetOutputTtlMap();

    for (auto mapIter = ttlMap.begin(); mapIter != ttlMap.end(); mapIter++)
    {
        uint32_t interface = mapIter->first;
        // uint32_t outputTtl = mapIter->second;  // Unused for now

        Ptr<Packet> packet = p->Copy();
        Ipv4Header ipHeader = header;
        if (ipHeader.GetTtl() <= 1)
        {
            NS_LOG_WARN("TTL exceeded.  Drop.");
            m_dropTrace(header, packet, DROP_TTL_EXPIRED, this, interface);
            return;
        }
        ipHeader.SetTtl(header.GetTtl() - 1);
        NS_LOG_LOGIC("Forward multicast via interface " << interface);
        Ptr<Ipv4Route> rtentry = Create<Ipv4Route>();
        rtentry->SetSource(ipHeader.GetSource());
        rtentry->SetDestination(ipHeader.GetDestination());
        rtentry->SetGateway(Ipv4Address::GetAny());
        rtentry->SetOutputDevice(GetNetDevice(interface));

        m_multicastForwardTrace(ipHeader, packet, interface);
        SendRealOut(rtentry, packet, ipHeader);
    }
}

// This function analogous to Linux ip_forward()
void
Ipv4L3Protocol::IpForward(Ptr<Ipv4Route> rtentry, Ptr<const Packet> p, const Ipv4Header& header)
{
    NS_LOG_FUNCTION(this << rtentry << p << header);
    NS_LOG_LOGIC("Forwarding logic for node: " << m_node->GetId());
    // Forwarding
    Ipv4Header ipHeader = header;
    Ptr<Packet> packet = p->Copy();
    int32_t interface = GetInterfaceForDevice(rtentry->GetOutputDevice());
    if (ipHeader.GetTtl() <= 1)
    {
        // Do not reply to multicast/broadcast IP address
        if (!ipHeader.GetDestination().IsBroadcast() && !ipHeader.GetDestination().IsMulticast())
        {
            Ptr<Icmpv4L4Protocol> icmp = GetIcmp();
            icmp->SendTimeExceededTtl(ipHeader, packet, false);
        }
        NS_LOG_WARN("TTL exceeded.  Drop.");
        m_dropTrace(header, packet, DROP_TTL_EXPIRED, this, interface);
        return;
    }
    ipHeader.SetTtl(ipHeader.GetTtl() - 1);
    // in case the packet still has a priority tag attached, remove it
    SocketPriorityTag priorityTag;
    packet->RemovePacketTag(priorityTag);
    uint8_t priority = Socket::IpTos2Priority(ipHeader.GetTos());
    // add a priority tag if the priority is not null
    if (priority)
    {
        priorityTag.SetPriority(priority);
        packet->AddPacketTag(priorityTag);
    }

    m_unicastForwardTrace(ipHeader, packet, interface);
    SendRealOut(rtentry, packet, ipHeader);
}

void
Ipv4L3Protocol::LocalDeliver(Ptr<const Packet> packet, const Ipv4Header& ip, uint32_t iif)
{
    NS_LOG_FUNCTION(this << packet << &ip << iif);
    Ptr<Packet> p = packet->Copy(); // need to pass a non-const packet up
    Ipv4Header ipHeader = ip;

    if (!ipHeader.IsLastFragment() || ipHeader.GetFragmentOffset() != 0)
    {
        NS_LOG_LOGIC("Received a fragment, processing " << *p);
        bool isPacketComplete;
        isPacketComplete = ProcessFragment(p, ipHeader, iif);
        if (!isPacketComplete)
        {
            return;
        }
        NS_LOG_LOGIC("Got last fragment, Packet is complete " << *p);
        ipHeader.SetFragmentOffset(0);
        ipHeader.SetPayloadSize(p->GetSize());
    }

    m_localDeliverTrace(ipHeader, p, iif);

    Ptr<Ipv4Interface> ipv4Interface = GetInterface(iif);

    for (auto& socket : m_sockets)
    {
        NS_LOG_INFO("Delivering to raw socket " << socket);
        socket->ForwardUp(p, ipHeader, ipv4Interface);
    }

    Ptr<IpL4Protocol> protocol = GetProtocol(ipHeader.GetProtocol(), iif);
    if (protocol)
    {
        // we need to make a copy in the unlikely event we hit the
        // RX_ENDPOINT_UNREACH codepath
        Ptr<Packet> copy = p->Copy();
        IpL4Protocol::RxStatus status = protocol->Receive(p, ipHeader, GetInterface(iif));
        switch (status)
        {
        case IpL4Protocol::RX_OK:
        // fall through
        case IpL4Protocol::RX_ENDPOINT_CLOSED:
        // fall through
        case IpL4Protocol::RX_CSUM_FAILED:
            break;
        case IpL4Protocol::RX_ENDPOINT_UNREACH:
            if (ipHeader.GetDestination().IsBroadcast() || ipHeader.GetDestination().IsMulticast())
            {
                break; // Do not reply to broadcast or multicast
            }
            // Another case to suppress ICMP is a subnet-directed broadcast
            bool subnetDirected = false;
            for (uint32_t i = 0; i < GetNAddresses(iif); i++)
            {
                Ipv4InterfaceAddress addr = GetAddress(iif, i);
                if (addr.GetLocal().CombineMask(addr.GetMask()) ==
                        ipHeader.GetDestination().CombineMask(addr.GetMask()) &&
                    ipHeader.GetDestination().IsSubnetDirectedBroadcast(addr.GetMask()))
                {
                    subnetDirected = true;
                }
            }
            if (!subnetDirected)
            {
                GetIcmp()->SendDestUnreachPort(ipHeader, copy);
            }
        }
    }
}

bool
Ipv4L3Protocol::AddAddress(uint32_t i, Ipv4InterfaceAddress address)
{
    NS_LOG_FUNCTION(this << i << address);
    Ptr<Ipv4Interface> interface = GetInterface(i);
    bool retVal = interface->AddAddress(address);
    if (m_routingProtocol)
    {
        m_routingProtocol->NotifyAddAddress(i, address);
    }
    return retVal;
}

Ipv4InterfaceAddress
Ipv4L3Protocol::GetAddress(uint32_t interfaceIndex, uint32_t addressIndex) const
{
    Ptr<Ipv4Interface> interface = GetInterface(interfaceIndex);
    return interface->GetAddress(addressIndex);
}

uint32_t
Ipv4L3Protocol::GetNAddresses(uint32_t interface) const
{
    Ptr<Ipv4Interface> iface = GetInterface(interface);
    return iface->GetNAddresses();
}

bool
Ipv4L3Protocol::RemoveAddress(uint32_t i, uint32_t addressIndex)
{
    NS_LOG_FUNCTION(this << i << addressIndex);
    Ptr<Ipv4Interface> interface = GetInterface(i);
    Ipv4InterfaceAddress address = interface->RemoveAddress(addressIndex);
    if (address != Ipv4InterfaceAddress())
    {
        if (m_routingProtocol)
        {
            m_routingProtocol->NotifyRemoveAddress(i, address);
        }
        return true;
    }
    return false;
}

bool
Ipv4L3Protocol::RemoveAddress(uint32_t i, Ipv4Address address)
{
    NS_LOG_FUNCTION(this << i << address);

    if (address == Ipv4Address::GetLoopback())
    {
        NS_LOG_WARN("Cannot remove loopback address.");
        return false;
    }
    Ptr<Ipv4Interface> interface = GetInterface(i);
    Ipv4InterfaceAddress ifAddr = interface->RemoveAddress(address);
    if (ifAddr != Ipv4InterfaceAddress())
    {
        if (m_routingProtocol)
        {
            m_routingProtocol->NotifyRemoveAddress(i, ifAddr);
        }
        return true;
    }
    return false;
}

Ipv4Address
Ipv4L3Protocol::SourceAddressSelection(uint32_t interfaceIdx, Ipv4Address dest)
{
    NS_LOG_FUNCTION(this << interfaceIdx << " " << dest);
    if (GetNAddresses(interfaceIdx) == 1) // common case
    {
        return GetAddress(interfaceIdx, 0).GetLocal();
    }
    // no way to determine the scope of the destination, so adopt the
    // following rule:  pick the first available address (index 0) unless
    // a subsequent address is on link (in which case, pick the primary
    // address if there are multiple)
    Ipv4Address candidate = GetAddress(interfaceIdx, 0).GetLocal();
    for (uint32_t i = 0; i < GetNAddresses(interfaceIdx); i++)
    {
        Ipv4InterfaceAddress test = GetAddress(interfaceIdx, i);
        if (test.GetLocal().CombineMask(test.GetMask()) == dest.CombineMask(test.GetMask()))
        {
            if (!test.IsSecondary())
            {
                return test.GetLocal();
            }
        }
    }
    return candidate;
}

Ipv4Address
Ipv4L3Protocol::SelectSourceAddress(Ptr<const NetDevice> device,
                                    Ipv4Address dst,
                                    Ipv4InterfaceAddress::InterfaceAddressScope_e scope)
{
    NS_LOG_FUNCTION(this << device << dst << scope);
    Ipv4Address addr("0.0.0.0");
    Ipv4InterfaceAddress iaddr;
    bool found = false;

    if (device)
    {
        int32_t i = GetInterfaceForDevice(device);
        NS_ASSERT_MSG(i >= 0, "No device found on node");
        for (uint32_t j = 0; j < GetNAddresses(i); j++)
        {
            iaddr = GetAddress(i, j);
            if (iaddr.IsSecondary())
            {
                continue;
            }
            if (iaddr.GetScope() > scope)
            {
                continue;
            }
            if (dst.CombineMask(iaddr.GetMask()) == iaddr.GetLocal().CombineMask(iaddr.GetMask()))
            {
                return iaddr.GetLocal();
            }
            if (!found)
            {
                addr = iaddr.GetLocal();
                found = true;
            }
        }
    }
    if (found)
    {
        return addr;
    }

    // Iterate among all interfaces
    for (uint32_t i = 0; i < GetNInterfaces(); i++)
    {
        for (uint32_t j = 0; j < GetNAddresses(i); j++)
        {
            iaddr = GetAddress(i, j);
            if (iaddr.IsSecondary())
            {
                continue;
            }
            if (iaddr.GetScope() != Ipv4InterfaceAddress::LINK && iaddr.GetScope() <= scope)
            {
                return iaddr.GetLocal();
            }
        }
    }
    NS_LOG_WARN("Could not find source address for " << dst << " and scope " << scope
                                                     << ", returning 0");
    return addr;
}

void
Ipv4L3Protocol::SetMetric(uint32_t i, uint16_t metric)
{
    NS_LOG_FUNCTION(this << i << metric);
    Ptr<Ipv4Interface> interface = GetInterface(i);
    interface->SetMetric(metric);
}

uint16_t
Ipv4L3Protocol::GetMetric(uint32_t i) const
{
    Ptr<Ipv4Interface> interface = GetInterface(i);
    return interface->GetMetric();
}

uint16_t
Ipv4L3Protocol::GetMtu(uint32_t i) const
{
    Ptr<Ipv4Interface> interface = GetInterface(i);
    return interface->GetDevice()->GetMtu();
}

bool
Ipv4L3Protocol::IsUp(uint32_t i) const
{
    Ptr<Ipv4Interface> interface = GetInterface(i);
    return interface->IsUp();
}

void
Ipv4L3Protocol::SetUp(uint32_t i)
{
    NS_LOG_FUNCTION(this << i);
    Ptr<Ipv4Interface> interface = GetInterface(i);

    // RFC 791, pg.25:
    //  Every internet module must be able to forward a datagram of 68
    //  octets without further fragmentation.  This is because an internet
    //  header may be up to 60 octets, and the minimum fragment is 8 octets.
    if (interface->GetDevice()->GetMtu() >= 68)
    {
        interface->SetUp();

        if (m_routingProtocol)
        {
            m_routingProtocol->NotifyInterfaceUp(i);
        }
    }
    else
    {
        NS_LOG_LOGIC(
            "Interface "
            << int(i)
            << " is set to be down for IPv4. Reason: not respecting minimum IPv4 MTU (68 octets)");
    }
}

void
Ipv4L3Protocol::SetDown(uint32_t ifaceIndex)
{
    NS_LOG_FUNCTION(this << ifaceIndex);
    Ptr<Ipv4Interface> interface = GetInterface(ifaceIndex);
    interface->SetDown();

    if (m_routingProtocol)
    {
        m_routingProtocol->NotifyInterfaceDown(ifaceIndex);
    }
}

bool
Ipv4L3Protocol::IsForwarding(uint32_t i) const
{
    Ptr<Ipv4Interface> interface = GetInterface(i);
    NS_LOG_LOGIC("Forwarding state: " << interface->IsForwarding());
    return interface->IsForwarding();
}

void
Ipv4L3Protocol::SetForwarding(uint32_t i, bool val)
{
    NS_LOG_FUNCTION(this << i);
    Ptr<Ipv4Interface> interface = GetInterface(i);
    interface->SetForwarding(val);
}

Ptr<NetDevice>
Ipv4L3Protocol::GetNetDevice(uint32_t i)
{
    return GetInterface(i)->GetDevice();
}

void
Ipv4L3Protocol::SetIpForward(bool forward)
{
    NS_LOG_FUNCTION(this << forward);
    m_ipForward = forward;
    for (auto i = m_interfaces.begin(); i != m_interfaces.end(); i++)
    {
        (*i)->SetForwarding(forward);
    }
}

bool
Ipv4L3Protocol::GetIpForward() const
{
    return m_ipForward;
}

void
Ipv4L3Protocol::SetStrongEndSystemModel(bool model)
{
    NS_LOG_FUNCTION(this << model);
    m_strongEndSystemModel = model;
}

bool
Ipv4L3Protocol::GetStrongEndSystemModel() const
{
    return m_strongEndSystemModel;
}

void
Ipv4L3Protocol::RouteInputError(Ptr<const Packet> p,
                                const Ipv4Header& ipHeader,
                                Socket::SocketErrno sockErrno)
{
    NS_LOG_FUNCTION(this << p << ipHeader << sockErrno);
    NS_LOG_LOGIC("Route input failure-- dropping packet to " << ipHeader << " with errno "
                                                             << sockErrno);
    m_dropTrace(ipHeader, p, DROP_ROUTE_ERROR, this, 0);

    // \todo Send an ICMP no route.
}

void
Ipv4L3Protocol::DoFragmentation(Ptr<Packet> packet,
                                const Ipv4Header& ipv4Header,
                                uint32_t outIfaceMtu,
                                std::list<Ipv4PayloadHeaderPair>& listFragments)
{
    // BEWARE: here we do assume that the header options are not present.
    // a much more complex handling is necessary in case there are options.
    // If (when) IPv4 option headers will be implemented, the following code shall be changed.
    // Of course also the reassembly code shall be changed as well.

    NS_LOG_FUNCTION(this << *packet << outIfaceMtu << &listFragments);

    Ptr<Packet> p = packet->Copy();

    NS_ASSERT_MSG((ipv4Header.GetSerializedSize() == 5 * 4),
                  "IPv4 fragmentation implementation only works without option headers.");

    uint16_t offset = 0;
    bool moreFragment = true;
    uint16_t originalOffset = ipv4Header.GetFragmentOffset();
    bool isLastFragment = ipv4Header.IsLastFragment();
    uint32_t currentFragmentablePartSize = 0;

    // IPv4 fragments are all 8 bytes aligned but the last.
    // The IP payload size is:
    // floor( ( outIfaceMtu - ipv4Header.GetSerializedSize() ) /8 ) *8
    uint32_t fragmentSize = (outIfaceMtu - ipv4Header.GetSerializedSize()) & ~uint32_t(0x7);

    NS_LOG_LOGIC("Fragmenting - Target Size: " << fragmentSize);

    do
    {
        Ipv4Header fragmentHeader = ipv4Header;

        if (p->GetSize() > offset + fragmentSize)
        {
            moreFragment = true;
            currentFragmentablePartSize = fragmentSize;
            fragmentHeader.SetMoreFragments();
        }
        else
        {
            moreFragment = false;
            currentFragmentablePartSize = p->GetSize() - offset;
            if (!isLastFragment)
            {
                fragmentHeader.SetMoreFragments();
            }
            else
            {
                fragmentHeader.SetLastFragment();
            }
        }

        NS_LOG_LOGIC("Fragment creation - " << offset << ", " << currentFragmentablePartSize);
        Ptr<Packet> fragment = p->CreateFragment(offset, currentFragmentablePartSize);
        NS_LOG_LOGIC("Fragment created - " << offset << ", " << fragment->GetSize());

        fragmentHeader.SetFragmentOffset(offset + originalOffset);
        fragmentHeader.SetPayloadSize(currentFragmentablePartSize);

        if (Node::ChecksumEnabled())
        {
            fragmentHeader.EnableChecksum();
        }

        NS_LOG_LOGIC("Fragment check - " << fragmentHeader.GetFragmentOffset());

        NS_LOG_LOGIC("New fragment Header " << fragmentHeader);

        std::ostringstream oss;
        oss << fragmentHeader;
        fragment->Print(oss);

        NS_LOG_LOGIC("New fragment " << *fragment);

        listFragments.emplace_back(fragment, fragmentHeader);

        offset += currentFragmentablePartSize;

    } while (moreFragment);
}

bool
Ipv4L3Protocol::ProcessFragment(Ptr<Packet>& packet, Ipv4Header& ipHeader, uint32_t iif)
{
    NS_LOG_FUNCTION(this << packet << ipHeader << iif);

    uint64_t addressCombination =
        uint64_t(ipHeader.GetSource().Get()) << 32 | uint64_t(ipHeader.GetDestination().Get());
    uint32_t idProto =
        uint32_t(ipHeader.GetIdentification()) << 16 | uint32_t(ipHeader.GetProtocol());
    FragmentKey_t key;
    bool ret = false;
    Ptr<Packet> p = packet->Copy();

    key.first = addressCombination;
    key.second = idProto;

    std::shared_ptr<Fragments> fragments;

    auto it = m_fragments.find(key);
    if (it == m_fragments.end())
    {
        fragments = std::make_shared<Fragments>();
        m_fragments.insert(std::make_pair(key, fragments));

        auto iter = SetTimeout(key, ipHeader, iif);
        fragments->SetTimeoutIter(iter);
    }
    else
    {
        fragments = it->second;
    }

    NS_LOG_LOGIC("Adding fragment - Size: " << packet->GetSize()
                                            << " - Offset: " << (ipHeader.GetFragmentOffset()));

    fragments->AddFragment(p, ipHeader.GetFragmentOffset(), !ipHeader.IsLastFragment());

    if (fragments->IsEntire())
    {
        packet = fragments->GetPacket();
        m_timeoutEventList.erase(fragments->GetTimeoutIter());
        fragments = nullptr;
        m_fragments.erase(key);
        ret = true;
    }

    return ret;
}

Ipv4L3Protocol::Fragments::Fragments()
    : m_moreFragment(false)
{
    NS_LOG_FUNCTION(this);
}

void
Ipv4L3Protocol::Fragments::AddFragment(Ptr<Packet> fragment,
                                       uint16_t fragmentOffset,
                                       bool moreFragment)
{
    NS_LOG_FUNCTION(this << fragment << fragmentOffset << moreFragment);

    std::list<std::pair<Ptr<Packet>, uint16_t>>::iterator it;

    for (it = m_fragments.begin(); it != m_fragments.end(); it++)
    {
        if (it->second > fragmentOffset)
        {
            break;
        }
    }

    if (it == m_fragments.end())
    {
        m_moreFragment = moreFragment;
    }

    m_fragments.insert(it, std::pair<Ptr<Packet>, uint16_t>(fragment, fragmentOffset));
}

bool
Ipv4L3Protocol::Fragments::IsEntire() const
{
    NS_LOG_FUNCTION(this);

    bool ret = !m_moreFragment && !m_fragments.empty();

    if (ret)
    {
        uint16_t lastEndOffset = 0;

        for (auto it = m_fragments.begin(); it != m_fragments.end(); it++)
        {
            // overlapping fragments do exist
            NS_LOG_LOGIC("Checking overlaps " << lastEndOffset << " - " << it->second);

            if (lastEndOffset < it->second)
            {
                ret = false;
                break;
            }
            // fragments might overlap in strange ways
            uint16_t fragmentEnd = it->first->GetSize() + it->second;
            lastEndOffset = std::max(lastEndOffset, fragmentEnd);
        }
    }

    return ret;
}

Ptr<Packet>
Ipv4L3Protocol::Fragments::GetPacket() const
{
    NS_LOG_FUNCTION(this);

    auto it = m_fragments.begin();

    Ptr<Packet> p = it->first->Copy();
    uint16_t lastEndOffset = p->GetSize();
    it++;

    for (; it != m_fragments.end(); it++)
    {
        if (lastEndOffset > it->second)
        {
            // The fragments are overlapping.
            // We do not overwrite the "old" with the "new" because we do not know when each
            // arrived. This is different from what Linux does. It is not possible to emulate a
            // fragmentation attack.
            uint32_t newStart = lastEndOffset - it->second;
            if (it->first->GetSize() > newStart)
            {
                uint32_t newSize = it->first->GetSize() - newStart;
                Ptr<Packet> tempFragment = it->first->CreateFragment(newStart, newSize);
                p->AddAtEnd(tempFragment);
            }
        }
        else
        {
            NS_LOG_LOGIC("Adding: " << *(it->first));
            p->AddAtEnd(it->first);
        }
        lastEndOffset = p->GetSize();
    }

    return p;
}

Ptr<Packet>
Ipv4L3Protocol::Fragments::GetPartialPacket() const
{
    NS_LOG_FUNCTION(this);

    auto it = m_fragments.begin();

    Ptr<Packet> p = Create<Packet>();
    uint16_t lastEndOffset = 0;

    if (m_fragments.begin()->second > 0)
    {
        return p;
    }

    for (it = m_fragments.begin(); it != m_fragments.end(); it++)
    {
        if (lastEndOffset > it->second)
        {
            uint32_t newStart = lastEndOffset - it->second;
            uint32_t newSize = it->first->GetSize() - newStart;
            Ptr<Packet> tempFragment = it->first->CreateFragment(newStart, newSize);
            p->AddAtEnd(tempFragment);
        }
        else if (lastEndOffset == it->second)
        {
            NS_LOG_LOGIC("Adding: " << *(it->first));
            p->AddAtEnd(it->first);
        }
        lastEndOffset = p->GetSize();
    }

    return p;
}

void
Ipv4L3Protocol::Fragments::SetTimeoutIter(FragmentsTimeoutsListI_t iter)
{
    m_timeoutIter = iter;
}

Ipv4L3Protocol::FragmentsTimeoutsListI_t
Ipv4L3Protocol::Fragments::GetTimeoutIter()
{
    return m_timeoutIter;
}

void
Ipv4L3Protocol::HandleFragmentsTimeout(FragmentKey_t key, Ipv4Header& ipHeader, uint32_t iif)
{
    NS_LOG_FUNCTION(this << &key << &ipHeader << iif);

    auto it = m_fragments.find(key);
    Ptr<Packet> packet = it->second->GetPartialPacket();

    // if we have at least 8 bytes, we can send an ICMP.
    if (packet->GetSize() > 8)
    {
        Ptr<Icmpv4L4Protocol> icmp = GetIcmp();
        icmp->SendTimeExceededTtl(ipHeader, packet, true);
    }
    m_dropTrace(ipHeader, packet, DROP_FRAGMENT_TIMEOUT, this, iif);

    // clear the buffers
    it->second = nullptr;

    m_fragments.erase(key);
}

bool
Ipv4L3Protocol::UpdateDuplicate(Ptr<const Packet> p, const Ipv4Header& header)
{
    NS_LOG_FUNCTION(this << p << header);

    // \todo RFC 6621 mandates SHA-1 hash.  For now ns3 hash should be fine.
    uint8_t proto = header.GetProtocol();
    Ipv4Address src = header.GetSource();
    Ipv4Address dst = header.GetDestination();
    uint64_t id = header.GetIdentification();

    // concat hash value onto id
    uint64_t hash = id << 32;
    if (header.GetFragmentOffset() || !header.IsLastFragment())
    {
        // use I-DPD (RFC 6621, Sec 6.2.1)
        hash |= header.GetFragmentOffset();
    }
    else
    {
        // use H-DPD (RFC 6621, Sec 6.2.2)

        // serialize packet
        Ptr<Packet> pkt = p->Copy();
        pkt->AddHeader(header);

        std::ostringstream oss(std::ios_base::binary);
        pkt->CopyData(&oss, pkt->GetSize());
        std::string bytes = oss.str();

        NS_ASSERT_MSG(bytes.size() >= 20, "Degenerate header serialization");

        // zero out mutable fields
        bytes[1] = 0;                        // DSCP / ECN
        bytes[6] = bytes[7] = 0;             // Flags / Fragment offset
        bytes[8] = 0;                        // TTL
        bytes[10] = bytes[11] = 0;           // Header checksum
        if (header.GetSerializedSize() > 20) // assume options should be 0'd
        {
            std::fill_n(bytes.begin() + 20, header.GetSerializedSize() - 20, 0);
        }

        // concat hash onto ID
        hash |= (uint64_t)Hash32(bytes);
    }

    // set cleanup job for new duplicate entries
    if (!m_cleanDpd.IsPending() && m_purge.IsStrictlyPositive())
    {
        m_cleanDpd = Simulator::Schedule(m_expire, &Ipv4L3Protocol::RemoveDuplicates, this);
    }

    // assume this is a new entry
    DupTuple_t key{hash, proto, src, dst};
    NS_LOG_DEBUG("Packet " << p->GetUid() << " key = (" << std::hex << std::get<0>(key) << ", "
                           << std::dec << +std::get<1>(key) << ", " << std::get<2>(key) << ", "
                           << std::get<3>(key) << ")");

    // place a new entry, on collision the existing entry iterator is returned
    auto [iter, inserted] = m_dups.emplace(key, Seconds(0));
    bool isDup = !inserted && iter->second > Simulator::Now();

    // set the expiration event
    iter->second = Simulator::Now() + m_expire;
    return isDup;
}

void
Ipv4L3Protocol::RemoveDuplicates()
{
    NS_LOG_FUNCTION(this);

    DupMap_t::size_type n = 0;
    Time expire = Simulator::Now();
    auto iter = m_dups.cbegin();
    while (iter != m_dups.cend())
    {
        if (iter->second < expire)
        {
            NS_LOG_LOGIC("Remove key = (" << std::hex << std::get<0>(iter->first) << ", "
                                          << std::dec << +std::get<1>(iter->first) << ", "
                                          << std::get<2>(iter->first) << ", "
                                          << std::get<3>(iter->first) << ")");
            iter = m_dups.erase(iter);
            ++n;
        }
        else
        {
            ++iter;
        }
    }

    NS_LOG_DEBUG("Purged " << n << " expired duplicate entries out of " << (n + m_dups.size()));

    // keep cleaning up if necessary
    if (!m_dups.empty() && m_purge.IsStrictlyPositive())
    {
        m_cleanDpd = Simulator::Schedule(m_purge, &Ipv4L3Protocol::RemoveDuplicates, this);
    }
}

Ipv4L3Protocol::FragmentsTimeoutsListI_t
Ipv4L3Protocol::SetTimeout(FragmentKey_t key, Ipv4Header ipHeader, uint32_t iif)
{
    Time now = Simulator::Now() + m_fragmentExpirationTimeout;

    if (m_timeoutEventList.empty())
    {
        m_timeoutEvent =
            Simulator::Schedule(m_fragmentExpirationTimeout, &Ipv4L3Protocol::HandleTimeout, this);
    }
    m_timeoutEventList.emplace_back(now, key, ipHeader, iif);

    auto iter = --m_timeoutEventList.end();

    return iter;
}

void
Ipv4L3Protocol::HandleTimeout()
{
    Time now = Simulator::Now();

    while (!m_timeoutEventList.empty() && std::get<0>(*m_timeoutEventList.begin()) == now)
    {
        HandleFragmentsTimeout(std::get<1>(*m_timeoutEventList.begin()),
                               std::get<2>(*m_timeoutEventList.begin()),
                               std::get<3>(*m_timeoutEventList.begin()));
        m_timeoutEventList.pop_front();
    }

    if (m_timeoutEventList.empty())
    {
        return;
    }

    Time difference = std::get<0>(*m_timeoutEventList.begin()) - now;
    m_timeoutEvent = Simulator::Schedule(difference, &Ipv4L3Protocol::HandleTimeout, this);
}

} // namespace ns3
