ns-x

Go CodeQL

An easy-to-use, flexible network simulator library for Go.

Feature

  • Programmatically build customizable and scalable network topology from basic nodes.
  • Simulate packet loss, delay, etc. on any nodes, according to any parameters inputs in well-defined models.
  • Collect network and user-defined data in detail, from each and every node.
  • Cross-platform. High precision.

Introduction

Concept

  • Network: a topological graph consist of nodes, reflecting a real-world network for packet to transfer and route through.
  • Node: a physical or logical device in the network deciding what to do when a packet going through. A node usually connect to other nodes.
  • Event: an action to be done at a given time point.
  • Packet: simulated data packets transferring between nodes.
  • Transfer: the behavior of node when a packet going through.

Prerequisites

  • go mod must be supported and enabled.

Usage

Use in three steps: building network, starting network simulation, and collecting data.

1. Building network

The network is built by nodes and edges. Normally an edge connects only two nodes, each on one end. In some special cases, a node may connect to multiple (or none) incoming and/or outgoing nodes.

While nodes are highly customizable, some typical nodes are pre-defined as follows:

  • Broadcast: a node transfers packet from one source to multiple targets.

mermaid diagram 1

Mermaid markup
Broadcast –> Out1;
Broadcast –> Out2;
Broadcast –> Out3;
“>

graph LR;
    In --> Broadcast --> Out1;
    Broadcast --> Out2;
    Broadcast --> Out3;
  • Channel: a node delays, losses or reorders packets passing by.

mermaid diagram 2

Mermaid markup
Channel –>|Loss & Delay & Reorder| Out;
“>

graph LR;
    In --> Channel -->|Loss & Delay & Reorder| Out;
  • Endpoint: a node where to send and receive packets, usually acting as the endpoint of a chain.

mermaid diagram 3

Mermaid markup
Endpoint;
“>

graph LR;
   Nodes... --> Endpoint;
  • Gather: a node gathers packets from multiple sources to a single target.

mermaid diagram 4

Mermaid markup
Gather ==> Out;
In2 –> Gather;
In3 –> Gather;
“>

graph LR;
    In1 --> Gather ==> Out;
    In2 --> Gather;
    In3 --> Gather;
  • Restrict: a node limits pps or bps by dropping packets when its internal buffer overflows.

mermaid diagram 5

Mermaid markup
Restrict –>|Restricted Speed| Out;
“>

graph LR;
    In --> Restrict -->|Restricted Speed| Out;
  • Scatter: a node selects which node the incoming packet should be route to according to a given rule.

mermaid diagram 6

Mermaid markup
Scatter -.-> Out1;
Scatter –>|Selected Route| Out2;
Scatter -.-> Out3;
“>

graph LR;
    In --> Scatter -.-> Out1;
    Scatter -->|Selected Route| Out2;
    Scatter -.-> Out3;

After all necessary nodes created, connect them with code to build the network. To do so, just set the next node correctly for each node until all edges are defined as expected.

In addition, ns-x provides a builder to facilitate the process. Instead of connecting edges, it builds the network by connecting all paths in one line of code.

A Path, aka. a chain, is similar to the path concept in graph theory, representing a route along the edges of a graph.

Methods of the builder:

  • Chain(): saves current chain (path) and start to describe another chain.
  • Node(): appends a given node to current chain.
  • NodeWithName(): same as Node(), with a customizable name to refer to later.
  • NodeByName(): finds (refer to) a node with given name and appends it to current chain.
  • NodeGroup(): given a number of nodes, perform Node() operation on each of them in order.
  • NodeGroupWithName(): same as NodeGroup() with a customizable name.
  • NodeGroupByName(): finds a group with the given name, then perform NodeGroup() operation on it.
  • Build(): is the final trigger of the builder to really build the network. Note that in all nodes used in this builder line, all previously established connections will be overwritten.

2. Starting Network Simulation

Once the network built, start running it so packets can go through nodes.

Guaranteed behaviours of the simulation

  • Order: if any event e at time point t, only generate events at time point not before t, then the handling order of two events at different time point is guaranteed, and the order of events at same time point is undetermined.
  • Accuracy: each event will be handled at the given time point exactly in simulate clock, and the difference between the simulator clock and real clock is as small as possible, usually some microseconds.

See comments in the code for additional node-specific guarantees.

3. Collecting Data

Data could be collected by callback function node.OnTransferCallback(). Also note that time-costing callbacks would slow down the simulation and lead to inaccuracy, so it is highly recommended only collecting data in the callbacks. Further analyses should be done after the simulation.

Example

Following is an example of a network with two entries, one endpoint and two chains.

  • Chain 1: entry1 -> channel1(with 30% packet loss rate) -> restrict (1 pps, 1024 bps, buffer limited to 4096 bytes and 5 packets) -> endpoint
  • Chain 2: entry2 -> channel2(with 10% packet loss rate) -> endpoint

<div class="highlight highlight-source-go position-relative" data-snippet-clipboard-copy-content="package main

import (
"github.com/bytedance/ns-x/v2"
"github.com/bytedance/ns-x/v2/base"
"github.com/bytedance/ns-x/v2/math"
"github.com/bytedance/ns-x/v2/node"
"github.com/bytedance/ns-x/v2/tick"
"go.uber.org/atomic"
"math/rand"
"time"
)

func main() {
source := rand.NewSource(0)
random := rand.New(source)
helper := ns_x.NewBuilder()
callback := func(packet base.Packet, source, target base.Node, now time.Time) {
println("emit packet")
}
n1 := node.NewEndpointNode()
t := time.Now()
network, nodes := helper.
Chain().
NodeWithName("entry1", n1).
Node(node.NewChannelNode(node.WithTransferCallback(callback), node.WithLoss(math.NewRandomLoss(0.1, random)))).
Node(node.NewRestrictNode(node.WithPPSLimit(1, 20))).
NodeWithName("endpoint", node.NewEndpointNode()).
Chain().
NodeWithName("entry2", node.NewEndpointNode()).
Node(node.NewChannelNode(node.WithTransferCallback(callback), node.WithLoss(math.NewRandomLoss(0.1, random)))).
NodeOfName("endpoint").
Summary().
Build()
entry1 := nodes["entry1"].(*node.EndpointNode)
entry2 := nodes["entry2"].(*node.EndpointNode)
endpoint := nodes["endpoint"].(*node.EndpointNode)
count := atomic.NewInt64(0)
endpoint.Receive(func(packet base.Packet, now time.Time) []base.Event {
if packet != nil {
count.Inc()
println("receive packet at", now.String())
println("total", count.Load(), "packets received")
}
return nil
})
total := 20
events := make([]base.Event, 0, total*2)
for i := 0; i < 20; i++ {
events = append(events, entry1.Send(base.RawPacket([]byte{0x01, 0x02}), t))
}
for i := 0; i < 20; i++ {
events = append(events, entry2.Send(base.RawPacket([]byte{0x01, 0x02}), t))
}
event := base.NewPeriodicEvent(func(now time.Time) []base.Event {
for i := 0; i

package main

import (
	"github.com/bytedance/ns-x/v2"
	"github.com/bytedance/ns-x/v2/base"
	"github.com/bytedance/ns-x/v2/math"
	"github.com/bytedance/ns-x/v2/node"
	"github.com/bytedance/ns-x/v2/tick"
	"go.uber.org/atomic"
	"math/rand"
	"time"
)

func main() {
	source := rand.NewSource(0)
	random := rand.New(source)
	helper := ns_x.NewBuilder()
	callback := func(packet base.Packet, source, target base.Node, now time.Time) {
		println("emit packet")
	}
	n1 := node.NewEndpointNode()
	t := time.Now()
	network, nodes := helper.
		Chain().
		NodeWithName("entry1", n1).
		Node(node.NewChannelNode(node.WithTransferCallback(callback), node.WithLoss(math.NewRandomLoss(0.1, random)))).
		Node(node.NewRestrictNode(node.WithPPSLimit(1, 20))).
		NodeWithName("endpoint", node.NewEndpointNode()).
		Chain().
		NodeWithName("entry2", node.NewEndpointNode()).
		Node(node.NewChannelNode(node.WithTransferCallback(callback), node.WithLoss(math.NewRandomLoss(0.1, random)))).
		NodeOfName("endpoint").
		Summary().
		Build()
	entry1 := nodes["entry1"].(*node.EndpointNode)
	entry2 := nodes["entry2"].(*node.EndpointNode)
	endpoint := nodes["endpoint"].(*node.EndpointNode)
	count := atomic.NewInt64(0)
	endpoint.Receive(func(packet base.Packet, now time.Time) []base.Event {
		if packet != nil {
			count.Inc()
			println("receive packet at", now.String())
			println("total", count.Load(), "packets received")
		}
		return nil
	})
	total := 20
	events := make([]base.Event, 0, total*2)
	for i := 0; i < 20; i++ {
		events = append(events, entry1.Send(base.RawPacket([]byte{0x01, 0x02}), t))
	}
	for i := 0; i < 20; i++ {
		events = append(events, entry2.Send(base.RawPacket([]byte{0x01, 0x02}), t))
	}
	event := base.NewPeriodicEvent(func(now time.Time) []base.Event {
		for i := 0; i < 10; i++ {
			_ = rand.Int()
		}
		return nil
	}, time.Second, t)
	events = append(events, event)
	network.Run(events, tick.NewStepClock(t, time.Second), 300*time.Second)
	defer network.Wait()
}