Skip to content

Writing a BESS Configuration Script

Matt Mukerjee edited this page Jul 19, 2018 · 29 revisions

If you've reached this page, we suspect you've read the BESS Overview and Build and Install BESS. So far, you've configured BESS by telling it to load some built-in BESS sample scripts. This page will tell you how to write your own configuration scripts. If you have any questions or comments, please post them to the BESS issue tracker to get help.

Looking Inside a Script

First let's look at a familiar configuration script: samples/acl. From the bess/ main directory, the file is stored in bess/bessctl/conf/samples/acl.bess; go ahead and open it in your editor of choice (which we presume is vim if you are a cool person).

Here's what you will see:

import scapy.all as scapy
import socket

def aton(ip):
     return socket.inet_aton(ip)

# Craft a packet with the specified IP addresses
def gen_packet(proto, src_ip, dst_ip):
    eth = scapy.Ether(src='02:1e:67:9f:4d:ae', dst='06:16:3e:1b:72:32')
    ip = scapy.IP(src=src_ip, dst=dst_ip)
    udp = proto(sport=10001, dport=10002)
    payload = 'helloworld'
    pkt = eth/ip/udp/payload
    return str(pkt)

packets = [gen_packet(scapy.UDP, '172.16.100.1', '10.0.0.1'),
           gen_packet(scapy.UDP, '172.12.55.99', '12.34.56.78'),
           gen_packet(scapy.UDP, '172.12.55.99', '10.0.0.1'),
           gen_packet(scapy.UDP, '172.16.100.1', '12.34.56.78'),
           gen_packet(scapy.TCP, '172.12.55.99', '12.34.56.78'),
           gen_packet(scapy.UDP, '192.168.1.123', '12.34.56.78'),
          ]

fw::ACL(rules=[{'src_ip': '172.12.0.0/16', 'drop': False}])

Source() -> Rewrite(templates=packets) -> fw -> Sink()

At first glance, a couple of things should become clear to you:

  • BESS scripts are really just Python programs with a few additional features glued in.
    • You can see how these additional features are glued in in bess/bessctl/sugar.py if you are super curious.
    • The key "sugar" features to know about: you can create modules by declaring module objects, you can connect modules to each other (connect their gates) using arrows -> (which represent unidirectional packet flow) and you can give a module a name by assigning a name with :: (like fw::ACL(... in the example above).
  • You can create packets in your script using scapy, which is a convenient packet-manipulation library in Python.
  • You can write functions, create lists, etc. all like in normal Python.

To understand what this script says in particular, really all the action is happening on the line Source() -> Rewrite(templates=packets) -> fw -> Sink()

Here we have a chain of four modules. Since we are not connected to any network interfaces or applications to give us "real traffic", this example creates all of its own packets using a Source() module. A Source() module creates literally empty packets with no data in them. With a -> we connect the output of Source() to the input of a Rewrite module. Rewrite is a module that takes in a parameter -- a packet "template" -- and Rewrite fills in all the packets that come in to it with a copy of one of the packet templates.

After the packets have been filled in with data, we send the packets to fw, our ACL. Recall above that we named our ACL module fw, so now we can reference it in Python by this name. When we instantiated fw, we gave ACL a parameter as well. By default, ACL drops all packets that comes in. However, we gave it a rule to allow through (drop:False) all packets where src_ip matches 172.12.0.0/16, so packets in this prefix will be allowed through. All of the packets that are allowed through by fw are passed (via another ->) to Sink(), which is just a module that deletes every packet that comes in to it. Once again, normally we would probably pass all of the traffic out on a port to an application or to a network interface, but since this is just an example we will just drop all of the traffic.

What is happening under the hood when I run a script?

Recall from the overview that bessd actually forwards the packets, and bessctl is just a program that tells bessd what to do.

When you declare a module object in Python (e.g., Sink(), fw::ACL(...parameters...)), bessctl tells bessd to create a Sink module inside bessd; this Sink module that runs inside bessd is actually implemented in C++! You can look at all of the implementations of all modules in bess/core/modules. Whenever you write a new module in C++, the bess build script will create corresponding objects in Python for you to use in bessctl. Whatever you tell the Python object to do will be sent to the C++ object in bessd. Similarly, whenever you declare a -> in Python connecting two Python objects, bessd will connect the C++ modules representing those same modules on the dataplane.

All of this magic glue between your Python script and the actual, running C++ code in bessd is achieved using protobufs and grpc.

You can read all of the commands and parameters for all built-in modules here.

Writing a new script

To start your first script, cd into bess/bessctl/conf/. You can create any file in the conf/ directory (or subdirectories of conf/ for it to show up in bessctl. Let's create a file in conf/ called my_script.bess.

touch my_script.bess
cat >my_script.bess
> Source() -> Sink()
Ctrl^C

Ta-da! You have created your simplest, easiest BESS script. It creates a Source module and connects it to a Sink module -- that is, it creates blank packets and then immediately destroys them. How boring!

Open up your script in your editor of choice, and let's replace our Source with a FlowGen, which generates packet by simulating TCP connections. Delete the line that says Source() -> Sink() and replace it with this:

import scapy.all as scapy

pkt_size = int($SN_PKT_SIZE!'60')
assert(60 <= pkt_size <= 1522)

#use scapy to build a packet template
eth = scapy.Ether(src='02:1e:67:9f:4d:ae', dst='06:16:3e:1b:72:32')
ip = scapy.IP(src='10.0.0.1', dst='10.0.0.2')   # dst IP is overwritten
tcp = scapy.TCP(sport=10001, dport=10002)
payload = ('hello' + '0123456789' * 200)[:pkt_size-len(eth/ip/tcp)]
pkt = eth/ip/tcp/payload
pkt_data = str(pkt)

FlowGen(template=pkt_data, pps=1000, flow_rate = 10, flow_duration = 5.0, arrival='uniform', duration='uniform', quick_rampup=True, ip_src_range=100000) -> Sink()

Now we have replaced Source() with a module FlowGen that takes some initialization parameters. These parameters tell the FlowGen to create packets based on a template packet we created, to generate 1000 packets every second, to generate 10 new flows every second, that each flow should last for 5 seconds, that the arrival and duration of flows should be generated with uniform distributions, that the FlowGen should start generating 1000 packets per second from the startup, and that new flows should be generated by modifying the IP source address by changing its value to one of 100,000 values (phew!). Some modules have no parameters (like Source), others have a few required parameters (like FlowGen) and others have some optional parameters.

You can read all of the commands and parameters for all built-in modules here.

You can run this script in bess (run my_script, followed by monitor pipeline) and see packets flowing through it. To see samples of packets running through, we can add another module in between FlowGen and Sink adding in a module -> Dump(interval=1) -> between the two. This module will print out the contents of one packet every 1 second to the BESS log file, which is stored in /tmp/bessd.INFO.

Okay! So far, you have written a new script, used some parameters to configure the modules in the script, and seen how things are logged to /tmp/bessd.INFO.

Using modules with multiple gates

We discussed in the BESS Overview how modules have gates that accept and receive packets.

  • Source and Flowgen have no input gates and one output gates.
  • Sink has one input gate and no output gates.
  • Dump has one input gate and one output gate.

Some modules have more than one input gate or output gates. When this happens, we specify the gates by number. For example, the RoundRobin module can divide packets that come in on one input gate and release them over multiple output gates. Going back to our FlowGen example:

rr::RoundRobin(gates=[0,1]) #create a RoundRobin with two gates
FlowGen(...) -> rr  # push packets from Flowgen to RoundRobin (parameters omitted for brevity)
rr:0 -> Sink()
rr:1 -> Sink()

If you run this new script, you will see that half the packets go out over port 0, and half the packets go out over port 1.

The same syntax works for input ports. The Merge module is just like RoundRobin, only backwards: it takes in packets over multiple input ports and pushes them out over a single output port, e.g.

m::Merge() 
Source() -> 0:m
Source() -> 1:m
Merge -> Sink()

Calling a function on a module

Some modules have functions, which let you update properties of the module after you have initialized them. Let's say we wanted to replace our RoundRobin module with a module that sends packets with even-numbered source addresses out one port, and with odd-numbered source addresses out another port.

#create an ExactMatch module selecting over the last bit in the IP src
em::ExactMatch(fields=[size=1, mask=0x1, offset=29]) 
#create a rule telling ExactMatch to send odd numbered packets out gate 1, and even numbered packets out gate 0.
em.add(fields=[0x1], gate=1)
em.add(fields=[0x0], gate=0)
FlowGen(...) -> em #push packets from Flowgen to ExactMatch (parameters omitted for brevity)
em:0 -> Sink()
em:1 -> Sink()

Here we used the add(...) function from ExactMatch to provide additional information to the ExactMatch module. Some functions even return data from the module, e.g., see the Measure module in the Module documentation.

Metadata attributes vs. Packet Data

Many modules read or write to Packet Data -- e.g., data in the IP header, TCP header, packet payload, etc. BESS also allows you to attach to packets metadata attributes -- these are pieces of data that are not actually part of the packet (if you push the packet out to the network, these values are deleted) but carried with the packet so long as they stay inside BESS.

Some modules expect to read or write from these metadata attributes. For example the EtherEncap module takes in an IP packet with Ethernet addresses stored in its metadata, and creates an Ethernet header in the packet data using these addresses from the metadata, like this example from bessctl/conf/samples/vxlan.bess:

...
-> SetMetadata(attrs=
        [{'name': 'ether_src', 'size': 6, 'value_bin': '\x02\x01\x02\x03\x04\x05'},
         {'name': 'ether_dst', 'size': 6, 'value_bin': '\x02\x0a\x0b\x0c\x0d\x0e'}]) \
-> EtherEncap()  \
-> ...

Namespace notes

You may have noticed that BESS-script variable names like rr and RoundRobin in rr::RoundRobin(gates=[0,1]) appear out of nowhere. A BESS script acts a lot like a Python module: it has a global name space, pre-populated with all the C++ module classes and port driver classes. Using rr::RoundRobin(...) invokes the existing RoundRobin. It then creates or overwrites the global variable rr, which you can use from then on. For all the Python-specific details, click here.

Check out some more examples for fun

Ta-da! You know all of the core concepts required to develop in BESS! To try out some more advanced techniques or build more complex scripts, we recommend you dig in to the many sample scripts that ship with BESS in bess/bessctl/conf/. And as always, if you have any questions or confusions, feel free to post them in the BESS issue tracker to get help. To deploy your new BESS configuration script, you'll need to connect the script to some ports to a network interface, VM, etc.