courses

Network Simulation with ns-3

Why this matters

Every hands-on technique in this course so far has needed a network you are allowed to break. That is a real constraint. You cannot point a volumetric flood at the campus uplink to see how a bottleneck behaves under load, you cannot rent a thousand hosts to watch a botnet saturate a link, and you cannot ask a router vendor to let you swap its queueing discipline mid-attack to compare two designs. The interesting questions in network security are frequently questions about scale and failure, and those are exactly the questions a production network will not let you ask.

A discrete-event simulator answers them. ns-3 implements the protocol stack — TCP with real congestion control, IPv4/IPv6, 802.11, LTE, queueing disciplines — as software models, and runs a virtual clock over them. There is no wire, no NIC, and no wall-clock time. A twenty-second simulation of a saturated link finishes in about a second, produces a .pcap you can open in Wireshark, and produces byte-for-byte identical results every time you run it.

That last property is the one that matters most for security work. When you claim an attack degraded a service by 41%, someone should be able to reproduce that number. On real hardware they cannot: background traffic, interrupt coalescing, and CPU scheduling all move. In simulation they can, exactly, by running your script.

This page assumes you are comfortable reading packets in Wireshark and scapy, and that you have seen queueing and congestion control in networking. It complements rather than replaces the packet-level work: ns-3 is where you go when the question is “what happens to everyone else when this link is attacked” rather than “what is in this packet”.

⚠️ Simulation is a model, not the truth. ns-3’s TCP is an implementation of the RFCs, not a copy of Linux’s tcp_cubic.c. Results tell you what the model does. That is enormously useful for reasoning about mechanisms and for comparing designs against each other, and it is not evidence about a specific vendor’s box. Say which one you are claiming.

What ns-3 actually is

ns-3 is a discrete-event simulator. It keeps a priority queue of events ordered by virtual timestamp, pops the earliest, runs its handler (which may schedule more events), and advances the clock to that timestamp. Between events, no time passes at all — the simulator jumps. This is why an idle 10-hour network simulates instantly while a saturated 20-second one takes real CPU.

Simulation scripts are C++ programs that link against the ns-3 libraries. There is no configuration-file format and no GUI topology editor; you write main(). (Python bindings exist but are off by default and lag the C++ API — for this course, write C++.)

Five object types carry essentially the whole model:

Object What it represents Real-world analogue
Node A host or router A machine
NetDevice An interface with a MAC A NIC
Channel The medium connecting devices A cable or the air
Application Something that generates or consumes traffic A process
Protocol stack TCP/IP, queueing, routing The kernel

Because wiring those together by hand is tedious, ns-3 provides helper classes (PointToPointHelper, InternetStackHelper, Ipv4AddressHelper) that do the common cases in a line or two. Nearly all example code — and all the code below — is written in terms of helpers.

flowchart LR
    A[Application<br/>BulkSend / OnOff] --> B[Protocol stack<br/>TCP / IP]
    B --> C[Traffic control<br/>queue disc / AQM]
    C --> D[NetDevice<br/>+ driver queue]
    D --> E[Channel<br/>rate + delay]
    E --> F[NetDevice<br/>at the far end]
    F --> G[Stack + Application]
    C -.pcap / FlowMonitor.-> H[(Traces)]
    D -.pcap.-> H

In words, for anyone whose reader does not render the diagram: an application hands data to the protocol stack, which hands it to the traffic-control layer (where the queueing discipline and any AQM live), which hands it to the net device and its driver queue, which puts it on the channel. The channel applies a data rate and a propagation delay and delivers it to the net device at the far end, which passes it up through that node’s stack to its application. Tracing hooks at the traffic-control and net-device layers write pcap files and FlowMonitor statistics.

The traffic-control layer is worth noticing, because it is where a great deal of the security-relevant behaviour lives. It sits between IP and the device driver and holds the queueing discipline — FIFO, fair queueing, an AQM like CoDel. When a link is attacked, what happens to the legitimate traffic sharing it is very largely determined by what is sitting in that box. We will measure exactly that below.

Installing and building

The installation guide gives the minimal dependency set for ns-3.36 and later:

sudo apt install g++ python3 cmake ninja-build git
# recommended alongside it
sudo apt install ccache gdb valgrind
# you almost certainly want these for this course
sudo apt install tcpdump wireshark

Then fetch and build a release. ns-3.48 is current as of this writing:

curl -sSLO https://www.nsnam.org/release/ns-3.48.tar.bz2
tar xjf ns-3.48.tar.bz2
cd ns-3.48
./ns3 configure --enable-examples --enable-tests
./ns3 build

./ns3 is ns-3’s own wrapper around CMake — it is the interface you use, not cmake directly. Configuration prints what it found, which is how you learn whether the parts you need are actually available:

Emulation FdNetDevice         : ON
Examples                      : ON
File descriptor NetDevice     : ON
SQLite support                : ON
Tap Bridge                    : ON
Tap FdNetDevice               : ON
Tests                         : ON

The build takes a while the first time — on a 16-core machine, several minutes; on a course VM with two cores, expect the better part of an hour. Install ccache before you start and rebuilds afterwards are close to free.

Confirm it works with the canonical first example, which is two nodes and a UDP echo:

$ ./ns3 run first
At time +2s client sent 1024 bytes to 10.1.1.2 port 9
At time +2.00369s server received 1024 bytes from 10.1.1.1 port 49153
At time +2.00369s server sent 1024 bytes to 10.1.1.1 port 49153
At time +2.00737s client received 1024 bytes from 10.1.1.2 port 9

Note the timestamps: the round trip took 7.37 ms of virtual time on a link configured with 2 ms delay. The run itself took a fraction of a second.

⚠️ A quoting bug worth knowing about. ./ns3 configure writes your PATH into a Python lock file without escaping it. If any directory in your PATH contains an apostrophe — common on WSL, where the Windows PATH is inherited — every later ./ns3 command dies with SyntaxError: invalid decimal literal. This bit during the preparation of this page. The fix is to delete .lock-ns3_linux_build and re-run configure with a sanitised PATH.

Logging

ns-3 has a built-in logging framework driven by the NS_LOG environment variable, with per-component severity levels and optional prefixes:

$ NS_LOG="UdpEchoClientApplication=level_all|prefix_func|prefix_time" ./ns3 run first
+0.000000000s UdpEchoClientApplication:UdpEchoClient(0x5f2211d915a0)
+0.000000000s UdpEchoClientApplication:SetDataSize(0x5f2211d915a0, 1024)
+2.000000000s UdpEchoClientApplication:DoStartApplication(0x5f2211d915a0)
+2.000000000s UdpEchoClientApplication:ScheduleTransmit(0x5f2211d915a0, +0ns)
+2.000000000s UdpEchoClientApplication:Send(0x5f2211d915a0)
+2.000000000s UdpEchoClientApplication:Send(): At time +2s client sent 1024 bytes to 10.1.1.2 port 9
+2.007372800s UdpEchoClientApplication:HandleRead(0x5f2211d915a0, 0x5f2211d927d0)

Levels run level_error, level_warn, level_info, level_function, level_debug, level_all. Logging is compiled out of optimized builds, so if you see no output, check your build profile before you doubt your filter.

Worked example: measuring a volumetric DoS

Here is the kind of question ns-3 is actually for. A service sits behind a 5 Mbps bottleneck. An attacker floods UDP at that bottleneck from elsewhere on the network. How much does the legitimate TCP flow lose — and does the router’s queueing discipline change the answer?

That second half is the interesting one. It is easy to assert that fair queueing mitigates volumetric attacks. It is better to measure it.

The topology

  victim  (10.1.1.1) --100Mbps/2ms--\
                                     router --5Mbps/20ms-- server (10.1.3.2)
  attacker(10.1.2.1) --100Mbps/2ms--/          ^ bottleneck

The victim runs an unlimited TCP bulk transfer to the server for the whole run. The attacker sends constant-bitrate UDP — no congestion control, so it does not back off — from t=5 s to t=15 s. Both cross the same 5 Mbps link.

The code

Save as scratch/dos-bottleneck.cc inside the ns-3 tree. Anything in scratch/ is built automatically and run by basename.

#include "ns3/applications-module.h"
#include "ns3/core-module.h"
#include "ns3/flow-monitor-module.h"
#include "ns3/internet-module.h"
#include "ns3/network-module.h"
#include "ns3/point-to-point-module.h"
#include "ns3/traffic-control-module.h"

#include <iomanip>

using namespace ns3;

NS_LOG_COMPONENT_DEFINE("DosBottleneck");

int
main(int argc, char* argv[])
{
    std::string attackRate = "0Mbps";
    std::string aqm = "ns3::FqCoDelQueueDisc";
    double simTime = 20.0;
    bool pcap = false;

    CommandLine cmd(__FILE__);
    cmd.AddValue("attackRate", "UDP flood rate (0Mbps disables the attacker)", attackRate);
    cmd.AddValue("aqm", "Root queue disc on the bottleneck link", aqm);
    cmd.AddValue("simTime", "Simulation duration in seconds", simTime);
    cmd.AddValue("pcap", "Write pcap traces of the bottleneck", pcap);
    cmd.Parse(argc, argv);

    NodeContainer victim, attacker, router, server;
    victim.Create(1);
    attacker.Create(1);
    router.Create(1);
    server.Create(1);

    PointToPointHelper edge;
    edge.SetDeviceAttribute("DataRate", StringValue("100Mbps"));
    edge.SetChannelAttribute("Delay", StringValue("2ms"));

    PointToPointHelper bottleneck;
    bottleneck.SetDeviceAttribute("DataRate", StringValue("5Mbps"));
    bottleneck.SetChannelAttribute("Delay", StringValue("20ms"));
    // One-packet device queue: we want the queue disc, not the driver ring,
    // to be the thing that fills up. Otherwise the AQM never sees the backlog.
    bottleneck.SetQueue("ns3::DropTailQueue<Packet>", "MaxSize", StringValue("1p"));

    NetDeviceContainer vDev = edge.Install(victim.Get(0), router.Get(0));
    NetDeviceContainer aDev = edge.Install(attacker.Get(0), router.Get(0));
    NetDeviceContainer bDev = bottleneck.Install(router.Get(0), server.Get(0));

    InternetStackHelper stack;
    stack.InstallAll();

    // Set the bottleneck's root queue disc explicitly. ns-3 only applies its
    // default (FqCoDel) at initialisation to devices that do not already have
    // one, so installing here simply wins -- there is nothing to uninstall.
    TrafficControlHelper tch;
    tch.SetRootQueueDisc(aqm);
    QueueDiscContainer qdiscs = tch.Install(bDev);

    Ipv4AddressHelper addr;
    addr.SetBase("10.1.1.0", "255.255.255.0");
    Ipv4InterfaceContainer vIf = addr.Assign(vDev);
    addr.SetBase("10.1.2.0", "255.255.255.0");
    Ipv4InterfaceContainer aIf = addr.Assign(aDev);
    addr.SetBase("10.1.3.0", "255.255.255.0");
    Ipv4InterfaceContainer bIf = addr.Assign(bDev);
    Ipv4Address serverAddr = bIf.GetAddress(1);

    Ipv4GlobalRoutingHelper::PopulateRoutingTables();

    // Sinks: TCP/9 is the service under attack, UDP/9999 absorbs the flood.
    PacketSinkHelper tcpSink("ns3::TcpSocketFactory",
                             InetSocketAddress(Ipv4Address::GetAny(), 9));
    PacketSinkHelper udpSink("ns3::UdpSocketFactory",
                             InetSocketAddress(Ipv4Address::GetAny(), 9999));
    ApplicationContainer sinks = tcpSink.Install(server);
    sinks.Add(udpSink.Install(server));
    sinks.Start(Seconds(0.0));
    sinks.Stop(Seconds(simTime));

    // The legitimate flow: an unlimited TCP bulk transfer.
    BulkSendHelper bulk("ns3::TcpSocketFactory", InetSocketAddress(serverAddr, 9));
    bulk.SetAttribute("MaxBytes", UintegerValue(0));
    ApplicationContainer legit = bulk.Install(victim);
    legit.Start(Seconds(1.0));
    legit.Stop(Seconds(simTime - 1.0));

    // The attack: constant-bitrate UDP, no congestion control, 10 s burst.
    if (attackRate != "0Mbps")
    {
        OnOffHelper flood("ns3::UdpSocketFactory", InetSocketAddress(serverAddr, 9999));
        flood.SetAttribute("OnTime", StringValue("ns3::ConstantRandomVariable[Constant=1]"));
        flood.SetAttribute("OffTime", StringValue("ns3::ConstantRandomVariable[Constant=0]"));
        flood.SetAttribute("DataRate", StringValue(attackRate));
        flood.SetAttribute("PacketSize", UintegerValue(1024));
        ApplicationContainer atk = flood.Install(attacker);
        atk.Start(Seconds(5.0));
        atk.Stop(Seconds(15.0));
    }

    if (pcap)
    {
        bottleneck.EnablePcap("dos-bottleneck", bDev.Get(0), true);
    }

    FlowMonitorHelper fmHelper;
    Ptr<FlowMonitor> monitor = fmHelper.InstallAll();

    Simulator::Stop(Seconds(simTime));
    Simulator::Run();

    monitor->CheckForLostPackets();
    Ptr<Ipv4FlowClassifier> classifier =
        DynamicCast<Ipv4FlowClassifier>(fmHelper.GetClassifier());

    std::cout << "\n  attackRate=" << attackRate << "  aqm=" << aqm << "\n\n";
    std::cout << std::left << std::setw(26) << "  flow" << std::right << std::setw(10) << "rx MB"
              << std::setw(12) << "Mbps" << std::setw(9) << "loss%" << std::setw(11) << "delay ms"
              << "\n";
    std::cout << "  " << std::string(66, '-') << "\n";

    for (const auto& [id, st] : monitor->GetFlowStats())
    {
        Ipv4FlowClassifier::FiveTuple t = classifier->FindFlow(id);
        // Only the forward direction to the server is interesting here.
        if (t.destinationAddress != serverAddr)
        {
            continue;
        }
        double span = (st.timeLastRxPacket - st.timeFirstRxPacket).GetSeconds();
        double mbps = span > 0 ? st.rxBytes * 8.0 / span / 1e6 : 0.0;
        double loss = st.txPackets ? 100.0 * st.lostPackets / st.txPackets : 0.0;
        double delay = st.rxPackets ? st.delaySum.GetSeconds() * 1000.0 / st.rxPackets : 0.0;

        std::ostringstream label;
        label << (t.protocol == 6 ? "TCP " : "UDP ") << t.sourceAddress << " -> :"
              << t.destinationPort;

        std::cout << std::left << std::setw(26) << ("  " + label.str()) << std::right
                  << std::fixed << std::setprecision(2) << std::setw(10) << st.rxBytes / 1e6
                  << std::setw(12) << mbps << std::setw(9) << loss << std::setw(11) << delay
                  << "\n";
    }
    std::cout << "\n";

    Simulator::Destroy();
    return 0;
}

CommandLine gives you a real CLI for free, including generated help:

$ ./ns3 run "dos-bottleneck --PrintHelp"
dos-bottleneck [Program Options] [General Arguments]

Program Options:
    --attackRate:  UDP flood rate (0Mbps disables the attacker) [0Mbps]
    --aqm:         Root queue disc on the bottleneck link [ns3::FqCoDelQueueDisc]
    --simTime:     Simulation duration in seconds [20]
    --pcap:        Write pcap traces of the bottleneck [false]

Note the quoting: ./ns3 run "prog --flag" passes the arguments to your program. Without the quotes they are eaten by ns3 itself.

The baseline

With no attacker, the TCP flow gets essentially the whole link:

$ ./ns3 run "dos-bottleneck --attackRate=0Mbps"

  attackRate=0Mbps  aqm=ns3::FqCoDelQueueDisc

  flow                         rx MB        Mbps    loss%   delay ms
  ------------------------------------------------------------------
  TCP 10.1.1.1 -> :9            9.51        4.15     0.15      26.97

4.15 Mbps of goodput on a 5 Mbps link, with headers and TCP’s sawtooth accounting for the rest. 27 ms mean delay, consistent with a 20 ms one-way propagation delay plus a little queueing.

Under attack

Now switch the attacker on at four times the bottleneck capacity:

$ ./ns3 run "dos-bottleneck --attackRate=20Mbps"

  attackRate=20Mbps  aqm=ns3::FqCoDelQueueDisc

  flow                         rx MB        Mbps    loss%   delay ms
  ------------------------------------------------------------------
  TCP 10.1.1.1 -> :9            5.64        2.44     0.36      31.11
  UDP 10.1.2.1 -> :9999         5.65        3.02    56.34    4017.05

The legitimate transfer drops from 9.51 MB to 5.64 MB — it keeps 59% of its baseline. Look at what happened to the attacker, though: it offered 20 Mbps, got 3.02 Mbps through, and lost 56% of its own packets while accumulating four seconds of queueing delay. Fair queueing isolated the flows and made the flood mostly pay its own cost.

Changing the queueing discipline

Swap fair queueing for a plain FIFO — which is what an unconfigured or older router gives you:

$ ./ns3 run "dos-bottleneck --attackRate=20Mbps --aqm=ns3::PfifoFastQueueDisc"

  attackRate=20Mbps  aqm=ns3::PfifoFastQueueDisc

  flow                         rx MB        Mbps    loss%   delay ms
  ------------------------------------------------------------------
  TCP 10.1.1.1 -> :9            3.23        1.41     4.49     173.05
  UDP 10.1.2.1 -> :9999         7.01        4.88    72.70    1597.61

Now the attacker takes the majority of the link (7.01 MB against the victim’s 3.23 MB), TCP loss jumps from 0.36% to 4.49%, and latency rises from 31 ms to 173 ms. Same attack, same topology, one line of configuration different.

The full matrix, all six runs:

Queue disc Attack rate TCP delivered vs. baseline TCP loss TCP delay
FqCoDel none 9.51 MB 0.15% 27 ms
FqCoDel 20 Mbps 5.64 MB 59% 0.36% 31 ms
FqCoDel 50 Mbps 5.64 MB 59% 0.36% 31 ms
PfifoFast none 11.25 MB 0.00% 200 ms
PfifoFast 20 Mbps 3.23 MB 29% 4.49% 173 ms
PfifoFast 50 Mbps 2.65 MB 24% 6.25% 206 ms

Two findings worth stating plainly:

  1. Under fair queueing, raising the attack from 20 Mbps to 50 Mbps changes nothing. The victim’s numbers are byte-identical. The attacker has already claimed its share and additional volume is simply discarded. Under FIFO, the same increase costs the victim another 18% of its remaining throughput. This is the concrete argument for flow isolation as a DoS mitigation, and you can see it rather than take it on faith.
  2. The FIFO baseline delivers more bytes than the FqCoDel baseline — 11.25 MB against 9.51 MB — at 200 ms of latency instead of 27 ms. That is bufferbloat, and it is a reminder that the “better” queue discipline is worse on the metric a naive throughput test would report. Choose your metric before you run the experiment, not after.

⚠️ A measurement trap in the output above. The Mbps column is computed over each flow’s own first-to-last received packet span, not over the simulation window, so it is not comparable across runs with different attack durations — note that PfifoFast at 50 Mbps shows a higher Mbps than at 20 Mbps while delivering fewer bytes. Bytes delivered is the honest metric here. This is the sort of thing that quietly invalidates a result, and it is much easier to catch when you can re-run the experiment in one second.

Reproducibility

Simulation’s headline claim, verified:

$ for i in 1 2; do ./ns3 run "dos-bottleneck --attackRate=20Mbps" | md5sum; done
a8ede70933e5de4e85d9fb81f8132349  -
a8ede70933e5de4e85d9fb81f8132349  -

Identical. ns-3 seeds its random variables deterministically by default; to run genuine replicates you must change RngRun explicitly (--RngRun=2), and a claim based on a single run of a stochastic scenario is not a result. Report the seed with your numbers.

Getting packets out: pcap and FlowMonitor

The two output paths matter for different questions.

FlowMonitor aggregates per five-tuple — transmitted and received bytes and packets, lost packets, delay and jitter sums. It is the right tool when the question is “how much did this flow get”. It can also dump XML for offline processing via monitor->SerializeToXmlFile("results.xml", true, true).

pcap gives you every packet, and connects ns-3 to everything else in this course. Enable it and re-run:

$ ./ns3 run "dos-bottleneck --attackRate=20Mbps --pcap=1"
$ ls *.pcap
dos-bottleneck-2-2.pcap

The naming is prefix-<nodeId>-<deviceId>.pcap. Node 2 is the router, device 2 is its bottleneck-facing interface.

From here it is ordinary tshark work:

$ tshark -r dos-bottleneck-2-2.pcap -q -z io,phs
frame                                    frames:20477 bytes:11649910
  ppp                                    frames:20477 bytes:11649910
    ip                                   frames:20477 bytes:11649910
      tcp                                frames:15092 bytes:5974120
        discard                          frames:8243 bytes:4863370
      udp                                frames:5385 bytes:5675790
        data                             frames:5385 bytes:5675790

⚠️ These are PPP frames, not Ethernet. PointToPointHelper produces DLT_PPP captures — there is no Ethernet header and no MAC address, because a point-to-point link does not have them. Any filter or script of yours that assumes eth. fields will silently match nothing. If you need Ethernet semantics (ARP, MAC spoofing, broadcast domains, anything from the capture page), build the segment with CsmaHelper instead, which models a shared Ethernet and produces DLT_EN10MB.

A time series shows the attack window clearly:

$ tshark -r dos-bottleneck-2-2.pcap -q -z "io,stat,2,tcp,udp"
|          |1                 |2                |
| Interval | Frames |  Bytes  | Frames |  Bytes |
|-----------------------------------------------|
|  0 <> 2  |   2443 |  912938 |      0 |      0 |
|  2 <> 4  |   2822 | 1163284 |      0 |      0 |
|  4 <> 6  |    979 |  333674 |    897 | 945438 |
|  6 <> 8  |   1407 |  569538 |    671 | 707234 |
|  8 <> 10 |   1444 |  584936 |    657 | 692478 |
| 10 <> 12 |   1460 |  582936 |    660 | 695640 |
| 12 <> 14 |   1366 |  549724 |    690 | 727260 |
| 14 <> 16 |   1386 |  560900 |    679 | 715666 |
| 16 <> 18 |   1429 |  578766 |    662 | 697748 |
| 18 <> Dur|    356 |  137424 |    469 | 494326 |

TCP throughput halves the moment the flood starts at t=5 and does not recover. Note also that UDP is still arriving in the 16 <> 18 bucket even though the attacker stopped sending at t=15 — that is the queue draining, and it is the same four seconds of accumulated delay FlowMonitor reported. The attack outlives the attacker. That is a genuinely useful thing to have seen before you try to correlate an attack window against a victim’s logs.

Because the output is a normal pcap, you can also replay it at Suricata to test whether a detection rule fires on traffic you generated to spec — a much tighter feedback loop than waiting for the real thing.

Bridging simulation to reality: emulation modes

ns-3 has two facilities for connecting a simulation to real code. These are what make it a security tool rather than only a performance-modelling tool, because they let you point actual attack tooling at a simulated network.

TapBridge puts a real Linux host (or container, or VM) inside the simulated topology. ns-3 creates a tap device on your machine; traffic sent to it enters the simulation as if that host were a node. Real nmap, real curl, real Suricata — against a hundred simulated hosts across a modelled WiFi link.

FdNetDevice is the mirror image: a simulated node sends and receives on a real interface via a file descriptor, so simulated traffic goes out on a real wire.

Both impose two requirements that the shipped src/tap-bridge/examples/tap-csma.cc states in two lines:

GlobalValue::Bind("SimulatorImplementationType", StringValue("ns3::RealtimeSimulatorImpl"));
GlobalValue::Bind("ChecksumEnabled", BooleanValue(true));

Both are worth understanding rather than copying:

TapBridge has three modes: ConfigureLocal (the default; ns-3 creates and configures the tap itself), UseLocal (you pre-create the tap, and ns-3 spoofs MACs to bridge it), and UseBridge (extends an existing Linux bridge into the simulation, requiring devices that support SendFrom()). All of them need root, since creating tap devices and putting interfaces in promiscuous mode requires CAP_NET_ADMIN.

The working starting points ship with the source: src/tap-bridge/examples/tap-csma.cc, tap-wifi-dumbbell.cc, and the LXC-based tap-csma-virtual-machine.cc with its virtual-network-setup.sh.

Where ns-3 does not help

Being clear about this is the difference between a defensible result and a hand-wave.

Where this fits in the course

Use ns-3 when the question is about aggregate behaviour under conditions you cannot create safely, and reach for something else otherwise:

Question Right tool
What is in this packet? Wireshark, tshark
Can I forge/modify this packet? scapy, NFQUEUE
Does my detection rule fire? Suricata against a pcap
What happens to everyone else when this link is flooded? ns-3
How does this protocol behave across 200 nodes? ns-3
Does this exploit work against this service? A VM, not a simulator

For a final project, simulation earns its place when your claim is comparative and quantitative — this queueing discipline preserves 59% of legitimate throughput under flood while that one preserves 29% — and when you can hand over a script that reproduces the number exactly. That is a stronger deliverable than a screenshot of a tool running.

Key takeaways

References


Related course pages: Introduction to Networking · Network Traffic Capture · Packet Mangling with NFQUEUE · Suricata IDS/IPS · Using wireshark · Introduction to scapy

🛠️ Maintenance note: every transcript on this page was produced against ns-3.48 (released 2 June 2026) built with g++ 15.3 and analysed with tshark 4.x on Linux. ns-3 releases two or three times a year and does change helper signatures between releases — BulkSendHelper, OnOffHelper, and PacketSinkHelper all moved to (protocol, address) constructors in the recent past, so code copied from older tutorials will not compile. Re-verify the version number, the build commands, and the scratch/ example each term. The TrafficControlHelper::Uninstall behaviour noted in the code comment is an initialisation-order detail that could plausibly change. The PATH-quoting bug in ./ns3 configure is unreported upstream as of this writing and may be fixed without notice. Most importantly, re-check DCE’s status before recommending it to anyone — it is the item here most likely to have either revived or died outright.