Network Simulation with ns-3
- 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 configurewrites yourPATHinto a Python lock file without escaping it. If any directory in yourPATHcontains an apostrophe — common on WSL, where the WindowsPATHis inherited — every later./ns3command dies withSyntaxError: invalid decimal literal. This bit during the preparation of this page. The fix is to delete.lock-ns3_linux_buildand re-runconfigurewith a sanitisedPATH.
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:
- 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.
- 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
Mbpscolumn 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.
PointToPointHelperproducesDLT_PPPcaptures — 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 assumeseth.fields will silently match nothing. If you need Ethernet semantics (ARP, MAC spoofing, broadcast domains, anything from the capture page), build the segment withCsmaHelperinstead, which models a shared Ethernet and producesDLT_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:
- The real-time scheduler. The default scheduler jumps between events as fast as it can, which is meaningless when a real host on the other side of a tap is running on wall-clock time.
RealtimeSimulatorImplpaces the simulation against the actual clock. The consequence is that you can no longer simulate faster than real time, and if your topology is too big to keep up, the simulator falls behind and your results become garbage. Watch for that. - Checksums. ns-3 does not compute real checksums by default — it is wasted work when both endpoints are models that will not check them. The moment a real kernel is receiving the packets, they must be correct, so this must be switched on. This is the identical failure mode described on the NFQUEUE page: a stale or absent checksum produces silence at the receiver and apparent success at the sender.
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.
- There is no attacker model. ns-3 ships no exploit, no scanner, no malware, and no botnet module. You model attacks as traffic — which is a good fit for volumetric DoS, congestion attacks, routing misbehaviour, and jamming, and a poor one for anything above the transport layer. There is no simulating a SQL injection.
- The stacks are models. ns-3’s TCP is written to the RFCs. Linux’s TCP has two decades of heuristics that are not in any RFC. When the question is “will this evade a specific IDS” or “how does this particular stack handle overlapping fragments”, you need real code — a VM, or the NFQUEUE work, or namespaces. Simulator-specific fingerprints are a real hazard: an ns-3 host does not look like a Linux host to
p0fornmap -O. - Direct Code Execution (DCE), which used to close that gap, is effectively stale. DCE ran unmodified Linux userspace applications and even the Linux network stack inside ns-3. It is a wonderful idea, and its most recent release targets ns-3.34 on Ubuntu 16.04. Do not build a term project on it without checking its current state first; prefer TapBridge with a container.
- Scale costs CPU, not bandwidth. Simulating a 10 Gbps link means simulating every packet on it. Packet-level simulation of a large, busy topology is slow, and this is the usual reason a student’s simulation “hangs”.
- The results are only as good as the parameters. A 5 Mbps bottleneck with a 1-packet device queue produced the numbers above. Different buffer sizing produces different numbers. If you cannot justify your parameters, you do not have a result — you have an anecdote with decimal places.
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
- ns-3 is a discrete-event simulator: virtual clock, modelled protocol stacks, no wire. It answers questions about scale and failure that you cannot ask on a production network.
- Reproducibility is the point. Identical runs produce byte-identical output, verified above by MD5. Report your
RngRunseed; a single run of a stochastic scenario is not a result. - Scripts are C++ programs built via
./ns3 buildand run via./ns3 run "prog --args". Anything dropped inscratch/is built automatically. - The traffic-control layer is where security-relevant queueing behaviour lives. Measured here: under a 20 Mbps flood, FqCoDel preserved 59% of the victim’s baseline throughput while PfifoFast preserved 29% — and under FqCoDel, raising the attack to 50 Mbps changed the victim’s numbers not at all.
- Choose your metric before the experiment. The FIFO baseline delivered more bytes than the AQM baseline, at 200 ms latency instead of 27 ms.
- FlowMonitor answers “how much did this flow get”; pcap connects ns-3 to tshark, Wireshark, and Suricata.
PointToPointHelperwrites PPP captures, not Ethernet. UseCsmaHelperwhen you need MACs, ARP, or a broadcast domain.- The attack outlives the attacker — queues drain long after the flood stops. Visible in both the FlowMonitor delay figure and the pcap time series.
- Emulation via TapBridge/FdNetDevice puts real tools against simulated networks, but requires the real-time scheduler and
ChecksumEnabled, plus root. - ns-3 has no attacker model and no real protocol code. It is the wrong tool for exploitation, IDS evasion against a specific stack, and OS fingerprinting. DCE is stale (ns-3.34 / Ubuntu 16.04).
References
- ns-3 project home. https://www.nsnam.org/
- ns-3 Tutorial (ns-3.48). https://www.nsnam.org/docs/release/3.48/tutorial/singlehtml/index.html
- ns-3 Installation Guide — Linux prerequisites and build. https://www.nsnam.org/docs/release/3.48/installation/html/linux.html
- ns-3 Model Library — the per-module reference. https://www.nsnam.org/docs/release/3.48/models/singlehtml/index.html
- ns-3 Manual — logging, attributes, tracing, RNG. https://www.nsnam.org/docs/release/3.48/manual/singlehtml/index.html
- ns-3 Model Library — TapBridge (NetDevices / Emulation). https://www.nsnam.org/docs/models/html/tap.html
- ns-3 Model Library — FlowMonitor. https://www.nsnam.org/docs/models/html/flow-monitor.html
- ns-3 Model Library — Traffic Control layer and queue discs. https://www.nsnam.org/docs/models/html/traffic-control.html
- ns-3 releases index. https://www.nsnam.org/releases/
- ns-3 App Store — contributed modules. https://apps.nsnam.org/
- Direct Code Execution (DCE) — note the ns-3.34 / Ubuntu 16.04 target. https://github.com/direct-code-execution/ns-3-dce
- RFC 8289 — Controlled Delay Active Queue Management (CoDel). https://www.rfc-editor.org/rfc/rfc8289
- RFC 8290 — FlowQueue-CoDel (FQ-CoDel). https://www.rfc-editor.org/rfc/rfc8290
- Wireshark — tshark(1) statistics options. https://www.wireshark.org/docs/man-pages/tshark.html
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, andPacketSinkHelperall 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 thescratch/example each term. TheTrafficControlHelper::Uninstallbehaviour noted in the code comment is an initialisation-order detail that could plausibly change. ThePATH-quoting bug in./ns3 configureis 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.