Part III — Industrial networks · Chapter 11

Kathará and the industrial P4 labs

~55 min read5 interactive widgets7 plates

In this chapter

  1. From namespaces to orchestration
  2. Kathará: containers as network devices
  3. Commands, labs and startup files
  4. Lab 1 — the Modbus secure tunnel testbed
  5. Parsing Modbus with P4
  6. P4 externs: AES and HMAC in the data plane
  7. Ingress processing and the modbus_sec table
  8. Installing rules: CLI, registers and Wireshark
  9. Lab 2 — from programmable packets to programmable assets
  10. The nine final project challenges
  11. Check your understanding

1. From namespaces to orchestration

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:

SimulationEmulation
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.

Key idea — the problem is orchestration, not isolation

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.

2. Kathará: containers as network devices

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.

Three Kathará devices pc1, pc2, pc3 connected through two collision domains A and B, all running inside a single host machine with one console per device host machine — Docker engine + Kathará pc1 root@pc1:~$ █ pc2 root@pc2:~$ █ pc3 root@pc3:~$ █ collision domain A collision domain B pc1[0]=A · pc2[0]=A pc2[1]=B · pc3[0]=B Each device has a console, a memory, a filesystem and one or more interfaces. Interfaces connect to collision domains; a domain may host several interfaces.
Plate 11.1 — A Kathará lab. The emulated network is a set of containers inside one host; the topology is a graph of devices and virtual collision domains.

3. Commands, labs and startup files

Kathará provides three sets of commands:

FamilyRoleCommands
v-commandsLow-level tools for configuring and starting a single devicevstart, vconfig, vclean
l-commandsEasier environment for complex labs with several deviceslstart, lclean, lconfig, lrestart, linfo
Global commandsManagementcheck, 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 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.

A lab directory tree with lab.conf, device subdirectories and startup files my_lab/ lab.conf s1/ s2/ modbusclient/ modbusserver/ s1.startup s2.startup lab.conf — the topology: pc1[0]=A pc2[0]=A pc2[1]=B pc3[0]=B device subdirectories are copied into the root of the device filesystem, e.g. pc1/foo/file.txt → /foo/file.txt .startup files run inside the device right after its startup: ip address add 10.0.0.1/24 dev eth0 systemctl start frr
Plate 11.2 — The anatomy of a Kathará lab: topology in 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.

4. Lab 1 — the Modbus secure tunnel testbed

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 Lab 1 testbed: Modbus client, switches s1 and s2, Modbus server, Wireshark node and host modbus client s1 (BMv2) cipher s2 (BMv2) decipher modbus server AES-encrypted conduit host: http://localhost:3000 wireshark domains A, B, C start: kathara lstart
Plate 11.3 — The Lab 1 testbed. Both switches load the same P4 pipeline; the difference in behaviour comes from the rules installed at startup.

5. Parsing Modbus with P4

The P4 parsing pipeline handles the variable length of the Modbus payload in five steps:

  1. The parser extracts the Ethernet and IP headers from each packet.
  2. If the Protocol field of the IPv4 header is 6 (TCP), the TCP header is extracted.
  3. Knowing the IPv4 Total Length and the TCP Data Offset, the parser computes the actual TCP header length (including options) and the total payload size.
  4. If source and destination ports are equal to 502, the Modbus Application (MBAP) header is parsed.
  5. Finally, the Modbus payload is extracted depending on the value of the Length field in the MBAP header.
The parsing pipeline: Ethernet, IPv4, TCP, MBAP and variable-length Modbus payload Ethernet IPv4 TCP MBAP payload always proto = 6 ? ports = 502 ? (else stop parsing) length from MBAP IPv4 Total Length and TCP Data Offset give the TCP header length and payload size; the MBAP Length field then drives the variable-length payload extraction. Parsing stops early for non-TCP or non-Modbus traffic — that traffic is just forwarded.
Plate 11.4 — Parsing Modbus in P4. The pipeline is gated: only TCP on port 502 reaches the MBAP header, and the payload length is taken from the protocol itself.

6. P4 externs: AES and HMAC in the data plane

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).

Key idea — fixed-function, called but not implemented

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 functionPurpose
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.

The extern architecture: declaration.p4 visible to the P4 program, definition.cpp compiled to a shared library loaded by BMv2 declaration.p4 extern sha256_hash_1024 / verify_hash_equals extern Encrypt / Decrypt — callable by the program definition.cpp AES-128/192/256 · HMAC-SHA256 implementation compiled to shared object .so loaded by BMv2 at startup, dynamically linked to the switch's compiled P4 program BMv2 P4 program calls externs
Plate 11.5 — The extern interface. The P4 program sees only the declaration; the implementation lives in C++ and is loaded dynamically.

7. Ingress processing and the modbus_sec table

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:

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 exercise: from egress port to destination address

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 =>
Editor's note — the takeaway of Lab 1

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.

8. Installing rules: CLI, registers and Wireshark

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:

# 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.

9. Lab 2 — from programmable packets to programmable assets

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.

Key idea — why AAS in network programmability

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.

What is already implemented

The provided Java project (built with Gradle) already creates the shell and two controllers:

The Lab 2 AAS structure: Network Control Plane shell, two controller submodels and their operations Network Control Plane AAS Controller(1) submodel GetSwitchRules · DeleteSwitchRules · RefreshRules Controller(2) submodel GetSwitchRules · DeleteSwitchRules · RefreshRules YOUR TASK — add three operations per controller: GetCurrentProgram · GetAvailablePrograms · ChangeProgram after a program change, refresh the rule-creation submodel net-config.yml: s1/s2 → programs → rules (table, match keys, action, parameters)
Plate 11.6 — Lab 2's starting point and the exercise: expose the active program, the available programs and a change operation through the AAS.

The exercise: extend the Control Plane AAS with program management

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:

  1. GetCurrentProgram — the currently active program is shown in the AAS;
  2. GetAvailablePrograms — the list of installable programs is shown;
  3. ChangeProgram — the user activates a selected program from the AAS UI;

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.

10. The nine final project challenges

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 implementAAS submodel to expose
1Mirror to the observer all Modbus traffic with function code 4Enable / disable the mirroring
2Mirror all Modbus traffic with function codes 1 and 2How many packets have been mirrored since start-up
3Count all packets with function codes 1 to 6How many packets counted per function code since start-up
4Count packets with function codes 1 to 6Count of function-code-4 packets, with an alarm if larger than 20
5Monitor traffic (function codes 1 to 6)Block client↔server traffic when packets of a user-selected function code exceed 20
6Monitor trafficBlock client↔server traffic when function-code-1 packets exceed a user-selected threshold
7Encrypt traffic with the provided P4 programActivate the encrypted tunnel when packets of a user-selected function code exceed a user-selected threshold
8Encrypt traffic, counting encrypted packets per function code (one counter per code 1–6)Number of packets encrypted per function code (user selects the code)
9Encrypt traffic only for specific function codesEnable the encrypted tunnel for a user-selected function code
For the exam — the pattern behind all nine

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.

Check your understanding

What is the difference between simulation and emulation, as defined by the lab?

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.

Why are network namespaces "not enough anymore" for the labs?

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.

What is Kathará, and which container images does it provide?

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).

Name the three Kathará command families and give an example command for each.

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).

What does a basic Kathará lab directory contain?

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).

Describe the two ways to share files between host and devices.

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).

List the five steps of the Modbus parsing pipeline in P4.

(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.

What is a P4 extern, and how is the encryption extern implemented?

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).

What does the modbus_sec table match on, what are its actions, and what does the exercise change?

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 =>.

How are the P4 rules installed at switch startup?

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.

What is Lab 2 about, and what is its starting Java structure?

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.

What are the three operations to add per controller in Lab 2's exercise?

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.

Summarize the nine final project challenges in one pattern.

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.