16.1. Internet Stack

16.1.1. Internet stack aggregation

A bare class Node is not very useful as-is; other objects must be aggregated to it to provide useful node functionality.

The ns-3 source code directory src/internet provides implementation of TCP/IPv4- and IPv6-related components. These include IPv4, ARP, UDP, TCP, IPv6, Neighbor Discovery, and other related protocols.

Internet Nodes are not subclasses of class Node; they are simply Nodes that have had a bunch of IP-related objects aggregated to them. They can be put together by hand, or via a helper function InternetStackHelper::Install () which does the following to all nodes passed in as arguments:

void
InternetStackHelper::Install(Ptr<Node> node) const
{
  if (m_ipv4Enabled)
    {
      /* IPv4 stack */
      if (node->GetObject<Ipv4>() != 0)
        {
          NS_FATAL_ERROR("InternetStackHelper::Install(): Aggregating "
                         "an InternetStack to a node with an existing Ipv4 object");
          return;
        }

      CreateAndAggregateObjectFromTypeId(node, "ns3::ArpL3Protocol");
      CreateAndAggregateObjectFromTypeId(node, "ns3::Ipv4L3Protocol");
      CreateAndAggregateObjectFromTypeId(node, "ns3::Icmpv4L4Protocol");
      // Set routing
      Ptr<Ipv4> ipv4 = node->GetObject<Ipv4>();
      Ptr<Ipv4RoutingProtocol> ipv4Routing = m_routing->Create(node);
      ipv4->SetRoutingProtocol(ipv4Routing);
    }

  if (m_ipv6Enabled)
    {
      /* IPv6 stack */
      if (node->GetObject<Ipv6>() != 0)
        {
          NS_FATAL_ERROR("InternetStackHelper::Install(): Aggregating "
                         "an InternetStack to a node with an existing Ipv6 object");
          return;
        }

      CreateAndAggregateObjectFromTypeId(node, "ns3::Ipv6L3Protocol");
      CreateAndAggregateObjectFromTypeId(node, "ns3::Icmpv6L4Protocol");
      // Set routing
      Ptr<Ipv6> ipv6 = node->GetObject<Ipv6>();
      Ptr<Ipv6RoutingProtocol> ipv6Routing = m_routingv6->Create(node);
      ipv6->SetRoutingProtocol(ipv6Routing);

      /* register IPv6 extensions and options */
      ipv6->RegisterExtensions();
      ipv6->RegisterOptions();
    }

  if (m_ipv4Enabled || m_ipv6Enabled)
    {
      /* UDP and TCP stacks */
      CreateAndAggregateObjectFromTypeId(node, "ns3::UdpL4Protocol");
      node->AggregateObject(m_tcpFactory.Create<Object>());
      Ptr<PacketSocketFactory> factory = CreateObject<PacketSocketFactory>();
      node->AggregateObject(factory);
    }
}

Where multiple implementations exist in ns-3 (TCP, IP routing), these objects are added by a factory object (TCP) or by a routing helper (m_routing).

Note that the routing protocol is configured and set outside this function. By default, the following protocols are added:

void InternetStackHelper::Initialize()
{
  SetTcp("ns3::TcpL4Protocol");
  Ipv4StaticRoutingHelper staticRouting;
  Ipv4GlobalRoutingHelper globalRouting;
  Ipv4ListRoutingHelper listRouting;
  Ipv6ListRoutingHelper listRoutingv6;
  Ipv6StaticRoutingHelper staticRoutingv6;
  listRouting.Add(staticRouting, 0);
  listRouting.Add(globalRouting, -10);
  listRoutingv6.Add(staticRoutingv6, 0);
  SetRoutingHelper(listRouting);
  SetRoutingHelper(listRoutingv6);
}

By default, IPv4 and IPv6 are enabled.

16.1.1.1. Internet Node structure

An IP-capable Node (an ns-3 Node augmented by aggregation to have one or more IP stacks) has the following internal structure.

16.1.1.1.1. Layer-3 protocols

At the lowest layer, sitting above the NetDevices, are the “layer 3” protocols, including IPv4, IPv6, ARP and so on. The class Ipv4L3Protocol is an implementation class whose public interface is typically class Ipv4, but the Ipv4L3Protocol public API is also used internally at present.

In class Ipv4L3Protocol, one method described below is Receive ():

/**
  * Lower layer calls this method after calling L3Demux::Lookup
  * The ARP subclass needs to know from which NetDevice this
  * packet is coming to:
  *    - implement a per-NetDevice ARP cache
  *    - send back arp replies on the right device
  */
void Receive( Ptr<NetDevice> device, Ptr<const Packet> p, uint16_t protocol,
const Address &from, const Address &to, NetDevice::PacketType packetType);

First, note that the Receive () function has a matching signature to the ReceiveCallback in the class Node. This function pointer is inserted into the Node’s protocol handler when AddInterface () is called. The actual registration is done with a statement such as follows:

RegisterProtocolHandler( MakeCallback(&Ipv4Protocol::Receive, ipv4),
                         Ipv4L3Protocol::PROT_NUMBER, 0);

The Ipv4L3Protocol object is aggregated to the Node; there is only one such Ipv4L3Protocol object. Higher-layer protocols that have a packet to send down to the Ipv4L3Protocol object can call GetObject<Ipv4L3Protocol>() to obtain a pointer, as follows:

Ptr<Ipv4L3Protocol> ipv4 = m_node->GetObject<Ipv4L3Protocol>();
if (ipv4 != 0)
  {
    ipv4->Send(packet, saddr, daddr, PROT_NUMBER);
  }

This class nicely demonstrates two techniques we exploit in ns-3 to bind objects together: callbacks, and object aggregation.

Once IPv4 routing has determined that a packet is for the local node, it forwards it up the stack. This is done with the following function:

void
Ipv4L3Protocol::LocalDeliver(Ptr<const Packet> packet, Ipv4Header const&ip, uint32_t iif)

The first step is to find the right Ipv4L4Protocol object, based on IP protocol number. For instance, TCP is registered in the demux as protocol number 6. Finally, the Receive() function on the Ipv4L4Protocol (such as TcpL4Protocol::Receive is called.

We have not yet introduced the class Ipv4Interface. Basically, each NetDevice is paired with an IPv4 representation of such device. In Linux, this class Ipv4Interface roughly corresponds to the struct in_device; the main purpose is to provide address-family specific information (addresses) about an interface.

All the classes have appropriate traces in order to track sent, received and lost packets. The users is encouraged to use them so to find out if (and where) a packet is dropped. A common mistake is to forget the effects of local queues when sending packets, e.g., the ARP queue. This can be particularly puzzling when sending jumbo packets or packet bursts using UDP. The ARP cache pending queue is limited (3 datagrams) and IP packets might be fragmented, easily overfilling the ARP cache queue size. In those cases it is useful to increase the ARP cache pending size to a proper value, e.g.:

Config::SetDefault("ns3::ArpCache::PendingQueueSize", UintegerValue(MAX_BURST_SIZE/L2MTU*3));

The IPv6 implementation follows a similar architecture. Dual-stacked nodes (one with support for both IPv4 and IPv6) will allow an IPv6 socket to receive IPv4 connections as a standard dual-stacked system does. A socket bound and listening to an IPv6 endpoint can receive an IPv4 connection and will return the remote address as an IPv4-mapped address. Support for the IPV6_V6ONLY socket option does not currently exist.

16.1.1.1.2. Layer-4 protocols and sockets

We next describe how the transport protocols, sockets, and applications tie together. In summary, each transport protocol implementation is a socket factory. An application that needs a new socket

For instance, to create a UDP socket, an application would use a code snippet such as the following:

Ptr<Udp> udpSocketFactory = GetNode()->GetObject<Udp>();
Ptr<Socket> m_socket = socketFactory->CreateSocket();
m_socket->Bind(m_local_address);
...

The above will query the node to get a pointer to its UDP socket factory, will create one such socket, and will use the socket with an API similar to the C-based sockets API, such as Connect() and Send(). The address passed to the Bind(), Connect(), or Send() functions may be a Ipv4Address, Ipv6Address, or Address. If a Address is passed in and contains anything other than a Ipv4Address or Ipv6Address, these functions will return an error. The Bind() and Bind6() functions bind to “0.0.0.0” and “::” respectively.

The socket can also be bound to a specific NetDevice though the BindToNetDevice(Ptr<NetDevice> netdevice) function. BindToNetDevice(Ptr<NetDevice> netdevice) will bind the socket to “0.0.0.0” and “::”(equivalent to calling Bind() and Bind6(), unless the socket has been already bound to a specific address. Summarizing, the correct sequence is:

 Ptr<Udp> udpSocketFactory = GetNode()->GetObject<Udp>();
 Ptr<Socket> m_socket = socketFactory->CreateSocket();
 m_socket->BindToNetDevice(n_netDevice);
...

or:

Ptr<Udp> udpSocketFactory = GetNode()->GetObject<Udp>();
Ptr<Socket> m_socket = socketFactory->CreateSocket();
m_socket->Bind(m_local_address);
m_socket->BindToNetDevice(n_netDevice);
...

The following raises an error:

Ptr<Udp> udpSocketFactory = GetNode()->GetObject<Udp>();
Ptr<Socket> m_socket = socketFactory->CreateSocket();
m_socket->BindToNetDevice(n_netDevice);
m_socket->Bind(m_local_address);
...

See the chapter on ns-3 sockets for more information.

We have described so far a socket factory (e.g. class Udp) and a socket, which may be specialized (e.g., class UdpSocket). There are a few more key objects that relate to the specialized task of demultiplexing a packet to one or more receiving sockets. The key object in this task is class Ipv4EndPointDemux. This demultiplexer stores objects of class Ipv4EndPoint. This class holds the addressing/port tuple (local port, local address, destination port, destination address) associated with the socket, and a receive callback. This receive callback has a receive function registered by the socket. The Lookup() function to Ipv4EndPointDemux returns a list of Ipv4EndPoint objects(there may be a list since more than one socket may match the packet). The layer-4 protocol copies the packet to each Ipv4EndPoint and calls its ForwardUp() method, which then calls the Receive() function registered by the socket.

An issue that arises when working with the sockets API on real systems is the need to manage the reading from a socket, using some type of I/O (e.g., blocking, non-blocking, asynchronous, …). ns-3 implements an asynchronous model for socket I/O; the application sets a callback to be notified of received data ready to be read, and the callback is invoked by the transport protocol when data is available. This callback is specified as follows:

void Socket::SetRecvCallback(Callback<void, Ptr<Socket>,
                             Ptr<Packet>,
                             const Address&> receivedData);

The data being received is conveyed in the Packet data buffer. An example usage is in class PacketSink:

m_socket->SetRecvCallback(MakeCallback(&PacketSink::HandleRead, this));

To summarize, internally, the UDP implementation is organized as follows:

  • a UdpImpl class that implements the UDP socket factory functionality

  • a UdpL4Protocol class that implements the protocol logic that is socket-independent

  • a UdpSocketImpl class that implements socket-specific aspects of UDP

  • a class called Ipv4EndPoint that stores the addressing tuple (local port, local address, destination port, destination address) associated with the socket, and a receive callback for the socket.

16.1.1.2. IP-capable node interfaces

Many of the implementation details, or internal objects themselves, of IP-capable Node objects are not exposed at the simulator public API. This allows for different implementations; for instance, replacing the native ns-3 models with ported TCP/IP stack code.

The C++ public APIs of all of these objects is found in the src/network directory, including principally:

  • address.h

  • socket.h

  • node.h

  • packet.h

These are typically base class objects that implement the default values used in the implementation, implement access methods to get/set state variables, host attributes, and implement publicly-available methods exposed to clients such as CreateSocket.

16.1.1.3. Example path of a packet

These two figures show an example stack trace of how packets flow through the Internet Node objects.

_images/internet-node-send.png

Send path of a packet.

_images/internet-node-recv.png

Receive path of a packet.