Computer networks are complex objects: several devices, several interfaces, several protocols running, physical interconnections that originate complex topologies. Performing experiments on a live network is unfeasible — it hosts services that are critical for the company, it would require coordinating different departments, and network equipment is expensive. The answer is a virtual environment for tests, experiments and measures, and the course is careful to distinguish the two flavours:
| Simulation | Emulation |
|---|---|
Aims at reproducing the performance of a real-life system — latency, packet loss, throughput. Examples: ns, real. |
Aims at accurately reproducing the functionalities of a real-life system — configurations, architectures, protocols — with limited attention to performance. |
The basic idea of emulation: several containers are created inside a single host machine, connected to virtual collision domains so they can communicate. Each container can play the role of a regular host, a router, a switch, an application. Chapter 4 built this by hand with network namespaces, and this chapter is where the course explains why that approach stops scaling. Namespaces give isolation for processes, interfaces and routing stacks — they are fine for simple point-to-point or small manual experiments, but they become hard to manage when the lab includes many nodes, startup scripts, multiple images and repeatable experiments.
With raw namespaces you still manage manually: how many nodes you need, how nodes are connected, startup commands for each node, configuration order, cleanup and reset, reproducibility across machines. Isolation was solved in chapter 4; the open problem is orchestration. Kathará exists to automate exactly that list.
Kathará is based on Docker: each emulated network device is a container. Several container images are available out of the box:
Each device has a console (a terminal window), a memory, a filesystem and zero, one or more network interfaces. Each interface is connected to a single virtual collision domain, and each virtual collision domain can be connected to several interfaces — so a domain is exactly the shared segment of chapter 4, but created and destroyed by Kathará instead of by hand.
Kathará provides three sets of commands:
| Family | Role | Commands |
|---|---|---|
| v-commands | Low-level tools for configuring and starting a single device | vstart, vconfig, vclean |
| l-commands | Easier environment for complex labs with several devices | lstart, lclean, lconfig, lrestart, linfo |
| Global commands | Management | check, connect, list, settings, wipe |
A Kathará lab is a set of preconfigured devices that can be started and halted together. A basic lab is a directory tree containing:
lab.conf file describing the network topology;<device_name>.startup files describing actions performed when a device starts.lab.conf contains a list of machine[arg]=value lines: if arg is a number, value is the name of the collision domain to which etharg is attached; if arg is not a number, it is an option and value its argument.
lab.conf, configuration in subdirectories, behaviour in .startup scripts.Files can be shared between host and devices in two ways. Mirrored sharing reflects changes in both directions: the /shared directory inside a device points to the lab's shared directory (enabled by default), and /hosthome points to the host's home directory (disabled by default). Copied sharing creates two independent copies, through the device subdirectories. The typical workflow is simple: enter the lab directory, then kathara lstart, kathara lclean, kathara lrestart.
The two industrial labs take the two halves of the course and aim them at one problem. Lab 1 (lab1_P4_modbus.zip) focuses on data-plane programmability: Modbus packet processing inside a P4 switch, using P4 to inspect industrial traffic at packet level and building the foundation for secure conduits with symmetric encryption. Lab 2 (lab2_P4_AAS.7z) focuses on control-plane programmability: using the Asset Administration Shell to expose network elements as assets, treating both data plane and control plane as manageable entities.
The Lab 1 testbed is a Kathará lab: a modbus client and a modbus server (simulating a working machine and a plant controller that collects its data), two P4 switches s1 and s2 running BMv2 between them, plus a wireshark node attached to domains A, B and C with port 3000/tcp published to the host. The switches carry the same pipeline; the secure conduit is realized by the two switches together, exactly as in chapter 10 — the devices are unchanged and speak plain Modbus.
The P4 parsing pipeline handles the variable length of the Modbus payload in five steps:
TCP), the TCP header is extracted.Section 4.3 of the P4 language specification introduces the extern: P4 syntax used to define interfaces between P4 programs and architecture-specific functionality that resides outside those programs. Two varieties exist: extern functions (interface described as a function declaration) and extern objects (interface described as an object providing method declarations, like a C++ class).
Externs are fixed-function objects that a P4 program can call, but not implement in P4 itself. P4 describes parsing, header processing and match-action behaviour; some operations are too target-specific or too complex to express directly. The extern mechanism extends the program with custom functionality that resides outside it — especially useful when an efficient implementation in P4 is difficult. Recall chapter 6's table: encryption is exactly the operation the course maps to externs.
The extern interface is declared in P4 and implemented in C++: declaration.p4 defines the extern functions visible to the P4 program; definition.cpp contains the actual implementation; the C++ code is compiled as a shared object library (.so); and the shared library is loaded by BMv2 at switch startup and dynamically linked to the compiled P4 program.
From declaration.p4 the program can invoke four functions:
| Extern function | Purpose |
|---|---|
Encrypt(...) / Decrypt(...) | Confidentiality through symmetric encryption: AES with keys of 128, 192 or 256 bits. |
sha256_hash_1024(...) | Integrity and authentication: HMAC-SHA256 tag computed over the data. |
verify_hash_equals(...) | Checks the HMAC tag on reception. |
AES is a symmetric block cipher operating on 128-bit blocks with 128/192/256-bit keys. Each round applies four steps: SubBytes (replace each byte using a lookup table), ShiftRows (move bytes around in the rows), MixColumns (Galois multiplication on the columns), AddRoundKey (XOR with the round key). HMAC (Hash-Based Message Authentication Code) ensures integrity and authenticity using a hash function and a secret key, returning a fixed-size digest. Together, the four externs let the switch build a protected conduit for industrial traffic.
In ingress, the pipeline is deliberately ordered: ipv4_lpm decides basic forwarding first, and modbus_sec is applied only for recognized industrial traffic. The security table matches on standard_metadata.egress_spec — the output port chosen by forwarding — and offers three actions:
no_cipher — default, traffic passes untouched;cipher — encrypt with AES and tag with HMAC;decipher — verify the tag and decrypt.The interpretation is worth stating slowly: forwarding is decided first; security treatment is decided after the egress direction is known. The secure conduit is enforced through table-driven switch behaviour — the same philosophy as chapter 10's in-network encryption, now with an explicit table you can inspect and modify.
table modbus_sec {
key = {
standard_metadata.egress_spec: exact;
}
actions = {
no_cipher;
cipher;
decipher;
}
size = 2;
default_action = no_cipher();
}
apply {
if (hdr.ipv4.isValid()) {
ipv4_lpm.apply();
if (hdr.tcp.isValid() && hdr.modbus_tcp.isValid()) {
modbus_sec.apply();
}
}
}
The lab then asks the question that turns the toy table into a policy: the secure conduit is specified per output port, but a real requirement is per destination. The exercise: make the secure conduit apply only to a specific IP address. Start from the table above, add a second Modbus server with a different IP to the lab, change the client script to connect to it — and remember to update the P4 rules. The solution changes the match key from egress_spec to hdr.ipv4.dstAddr, grows size to 16, and the CLI rules become address-based:
# s1 or s2 — cipher towards the protected servers
table_add modbus_sec cipher 200.1.1.7 =>
table_add modbus_sec cipher 200.1.1.8 =>
table_add modbus_sec cipher 200.1.1.9 =>
# and decipher towards the clients
table_add modbus_sec decipher 195.11.14.5 =>
table_add modbus_sec decipher 195.11.14.6 =>
table_add modbus_sec decipher 195.11.14.7 =>
The same pipeline runs on both switches. The actual behaviour comes from CLI-installed rules and key registers, not from the P4 code itself. This is the course's recurring lesson in concrete form: the data plane is a programmable asset whose behaviour is configuration, and the comparison with Modbus TLS is the chapter 10 evaluation — in-network encryption performs better than Modbus TLS and almost in line with unencrypted Modbus.
How are the P4 rules installed? The initial configuration happens at switch startup: s1.startup and s2.startup wait for simple_switch_CLI and then load auto_test_commands.txt. That file installs:
ipv4_lpm forwarding rules;register_write key value commands — the key words written into keys[0..7];modbus_sec rules: decipher on port 1, cipher on port 2.# both s1.startup and s2.startup do, after BMv2 is up:
simple_switch_CLI <<< $(cat auto_test_commands.txt)
# auto_test_commands.txt installs:
# ipv4_lpm entries
# register_write keys 0..7 <key words>
# table_add modbus_sec decipher 1 =>
# table_add modbus_sec cipher 2 =>
The keys live in registers on the data plane — the same in-memory state chapter 7 introduced — and the externs read them at cipher/decipher time. The client side is a Python script inside the modbusclient node: python3 modbus_client.py --read, --write, --rw, --test-rtt-read 128, --test-rtt-write 256, --test-read no-encryption. The Wireshark node uses the image lscr.io/linuxserver/wireshark, is attached to domains A, B and C, publishes port 3000/tcp to the host, and is reached at http://localhost:3000 after kathara lstart — that is how you verify the payload is ciphertext between the switches and plaintext at the ends.
Lab 1 focused on packet treatment inside the switch: parsing protocol fields, applying packet-level security treatment, programming the data plane. Lab 2 changes perspective: the network is no longer only a forwarding system — network components become explicitly modelled entities, and control and data plane can be treated as assets. The course states the transition in one line: in the first lab we programmed packet behaviour; in the second lab we program a network that can describe and expose itself.
This is exactly the Asset Administration Shell of chapter 10 applied to the network: the AAS is the standardized digital twin of an asset; it provides a structured digital representation, defines software structure, interfaces and semantics, and describes different aspects through submodels. Submodels are the content of the AAS — each represents one aspect, functional, structural or content-related — so an asset becomes a composition of meaningful views. The reason matters here is stated bluntly in the lab: we do not want a monolithic "network object"; we want separate views for infrastructure, control, and managed behaviour.
A programmable network includes forwarding elements, control logic, installed policies and exposed operational behaviour. With AAS, these components are modelled as assets instead of hidden implementation details: data plane = programmable asset, control plane = programmable asset, network behaviour = asset-exposed functionality. The AAS is not the physical asset; it is the standardized digital representation used to expose information and functions.
The provided Java project (built with Gradle) already creates the shell and two controllers:
App.java creates a Network Control Plane AAS;NetworkControlPlane.java creates two controller submodels — Controller(1) and Controller(2);Controller.java currently exposes GetSwitchRules, DeleteSwitchRules and RefreshRules, plus CreateRules_CNTx submodels populated from getRuleDescribers(...);App/app/src/main/resources/net-config.yml: top-level keys identify the switches (s1, s2), under each switch a list of program definitions, each program starting with a program: field and containing a rules: section, each rule specifying the target table, match key values, action and action parameters.The current AAS already allows users to inspect, add, delete and refresh switch rules. The exercise asks to extend the Java code so that each controller asset also exposes:
with the expected behaviour that after the change, the rule-describer submodel is refreshed — the AAS stays a truthful view of the control plane. Build with gradle build in the App folder after every source change.
The final project for this course is a set of nine challenges built on one shared environment: an industrial network composed of 2 hosts with 2 switches plus the Asset Administration Shell management infrastructure. The two hosts are a working machine (client) and a plant controller (server) that collects data of the working machines; they communicate using the Modbus protocol. Every challenge is phrased the same way — implement a P4 behaviour, then implement a new AAS submodel that exposes it — which is chapter 10's note made literal: the submodel is the unit of work in the project.
| # | P4 behaviour to implement | AAS submodel to expose |
|---|---|---|
| 1 | Mirror to the observer all Modbus traffic with function code 4 | Enable / disable the mirroring |
| 2 | Mirror all Modbus traffic with function codes 1 and 2 | How many packets have been mirrored since start-up |
| 3 | Count all packets with function codes 1 to 6 | How many packets counted per function code since start-up |
| 4 | Count packets with function codes 1 to 6 | Count of function-code-4 packets, with an alarm if larger than 20 |
| 5 | Monitor traffic (function codes 1 to 6) | Block client↔server traffic when packets of a user-selected function code exceed 20 |
| 6 | Monitor traffic | Block client↔server traffic when function-code-1 packets exceed a user-selected threshold |
| 7 | Encrypt traffic with the provided P4 program | Activate the encrypted tunnel when packets of a user-selected function code exceed a user-selected threshold |
| 8 | Encrypt traffic, counting encrypted packets per function code (one counter per code 1–6) | Number of packets encrypted per function code (user selects the code) |
| 9 | Encrypt traffic only for specific function codes | Enable the encrypted tunnel for a user-selected function code |
Every challenge decomposes into the same three layers: (1) a P4 program that observes or modifies Modbus traffic (mirror, count, block, encrypt) keyed on function codes 1–6; (2) an AAS submodel that reads the resulting state (counters, alarms, mirror status) or invokes an operation (enable mirroring, block, activate tunnel) — with parameters selected by the user from the AAS UI; (3) the bridge between them, through registers/counters in the data plane and the controller's rule installation. If you can say, for a given challenge, which P4 construct (clone, counter, register, extern) and which BaSyx component (submodel repository, DataBridge) you would touch, you have the whole project in your head.
Simulation aims at reproducing the performance of a real-life system — latency time, packet loss, etc. (examples: ns, real). Emulation aims at accurately reproducing the functionalities of a real-life system — configurations, architectures, protocols — with limited attention to performance.
Namespaces give isolation for processes, interfaces and routing stacks — fine for simple point-to-point or small manual experiments, but hard to manage when the lab includes many nodes, startup scripts, multiple images and repeatable experiments. With raw namespaces you still manage manually how many nodes you need, how they are connected, startup commands, configuration order, cleanup/reset and reproducibility. The problem is no longer isolation; it is orchestration.
Kathará is an emulation system based on Docker: each emulated network device is a container with a console, a memory, a filesystem and zero or more network interfaces, each connected to a virtual collision domain. Images include Base (DNS, web server, utilities), Quagga, FRRouting (with EVPN/MPLS), Open vSwitch and Behavioral Model (BMv2, the software P4 switch).
v-commands configure and start a single device (vstart, vconfig, vclean). l-commands set up complex labs of several devices (lstart, lclean, lconfig, lrestart, linfo). Global commands manage the system (check, connect, list, settings, wipe).
A lab.conf file describing the topology (lines like pc1[0]=A, attaching eth0 of pc1 to collision domain A; non-numeric args are options), subdirectories whose contents are copied into the root of each device's filesystem, and <device>.startup files — shell scripts executed inside the device right after startup (e.g. ip address add 10.0.0.1/24 dev eth0, systemctl start frr).
Mirrored: changes reflect in both directions — /shared points to the lab's shared directory (enabled by default) and /hosthome to the host home (disabled by default). Copied: two independent copies, done through the device subdirectories of the lab (e.g. pc1/foo/file.txt becomes /foo/file.txt inside pc1).
(1) Extract Ethernet and IP headers; (2) if IPv4 protocol is 6 (TCP), extract the TCP header; (3) using IPv4 Total Length and TCP Data Offset, compute the real TCP header length and payload size; (4) if source and destination ports are 502, parse the MBAP header; (5) extract the Modbus payload based on the MBAP Length field.
An extern is P4 syntax defining an interface to architecture-specific functionality that resides outside the program — fixed-function objects the program can call but not implement in P4. Two varieties: extern functions and extern objects. Implementation: declaration.p4 declares the interface, definition.cpp implements it, compiled to a .so loaded by BMv2 at switch startup and dynamically linked. The four functions are sha256_hash_1024, verify_hash_equals, Encrypt and Decrypt (AES 128/192/256 + HMAC-SHA256).
It matches on standard_metadata.egress_spec (exact) with actions no_cipher (default), cipher, decipher. Forwarding (ipv4_lpm) is applied first, then security only for recognized industrial traffic (valid IPv4 + valid TCP + valid Modbus). The exercise changes the key to hdr.ipv4.dstAddr (size 16) so the conduit is specified per destination IP, with rules like table_add modbus_sec cipher 200.1.1.7 =>.
s1.startup and s2.startup wait for simple_switch_CLI and load auto_test_commands.txt, which installs ipv4_lpm forwarding entries, writes key words into keys[0..7] registers (register_write keys <n> <value>), and adds modbus_sec rules — decipher on port 1, cipher on port 2. Both switches run the same pipeline; the behaviour difference is pure configuration.
Lab 2 applies the AAS to the network: the network becomes a set of modelled assets with data plane, control plane and network behaviour exposed as asset functionality. App.java creates the Network Control Plane AAS; NetworkControlPlane.java creates two controller submodels; Controller.java exposes GetSwitchRules, DeleteSwitchRules, RefreshRules plus dynamic CreateRules_CNTx submodels from getRuleDescribers(...). Switch programs and rules are read from net-config.yml.
GetCurrentProgram (the currently active program), GetAvailablePrograms (the list of installable programs), and ChangeProgram (activate a selected program from the AAS UI). Expected behaviour: after a program change, the rule-creation submodel must be refreshed so the AAS stays truthful. Rebuild with gradle build.
All nine share the same environment (client, server, two switches, AAS infrastructure, Modbus) and the same two-part task: a P4 behaviour keyed on function codes 1–6 — mirror (1, 2), count (3, 4), block (5, 6), encrypt (7, 8, 9) — plus a new AAS submodel that reads its state (counters, alarms) or triggers its operation (enable mirroring, block, activate tunnel), with function code and threshold selected by the user from the AAS UI where required.