This repository contains:
- In
P4R-Type/
: The code for the P4R-Type API and type generator, as well as examples that use the P4R-Type API. - In
vm/
: A virtual machine that sets up a simulation network using mininet, which can be used to test the API.
Note for OOPSLA artifact version: As an alternative to building the VM image with Vagrant, the top folder also contains the file P4R-Type_Demo_VM.ova
, a ready-to-use VM image. It can be imported with the VirtualBox interface using the default settings. In case you use this method for setting up the VM, skip the first three steps of the Kick-the-Tires Guide and start the VM directly from VirtualBox instead.
The OOPSLA artifact version which includes the VM image can be found here.
This artifact requires:
- sbt (for building the P4R-Type API and type generator)
- VirtualBox (for running the examples)
- Vagrant (for running the examples)
From now on, we write $ROOT
to denote the root directory of the artifact.
- Navigate to the
$ROOT/vm/
directory. - Run
vagrant up
to build and run the VM. Building the VM will take 10-15 minutes. - When the
vagrant
building procedure is complete, the VM will reboot. (From now on, you can launch the VM from VirtualBox, without usingvagrant
again.) - When the VM presents a graphical log-in prompt:
- Log on as user P4RType with the password
sdn
. - Open a terminal in the VM and run
make test
(in the home directory of the user P4RType). This will start the mininet network simulation with four hosts and four switchess1
..s4
(see thetopology.json
file for the layout). It also applies the P4 configurationconfig1
tos1
ands2
, andconfig2
tos3
ands4
. You will see themininet>
prompt and the messageReady to receive requests!
.
- Log on as user P4RType with the password
- Now, on the host machine that is running the VM, navigate to the
$ROOT/P4R-Type/
directory. - Run
sbt "runMain p4rtypetest"
. This will compile and run the programsrc/main/scala/examples/p4rtypetest.scala
, which connects to the mininet network in the VM and sends some test queries to thes1
switch.
If everything goes well, after the last step you will see Test successful!
followed by [success]
.
NOTE: you may also see the following message, that you can ignore:
[ERROR] io.grpc.StatusRuntimeException: UNAVAILABLE: Channel shutdown
At this point, the artifact should be working.
The source code directory $ROOT/P4R-Type/src/main/scala/
contains several subfolders:
protobuf/
: The "untyped" P4Runtime API code, generated by ScalaPB from the protobuf specification (which itself is found inP4R-Type/src/protobuf/
).typegen/
: The P4R-Type type generator, which parses a given P4info file and outputs corresponding Scala 3 types (as per the encoding described in Definition 5.2 of the companion paper)api/
: The type-parametric P4R-Type API used for making P4Runtime queries, outlined in Section 7 of the companion paper.examples/
: Examples that use the P4R-Type API with generated types. All these examples require the P4R-Type VM to be running.
For inspecting the code, we recommend using VS Code with the Scala Metals extension, in order to explore the files more easily and view type errors in real-time.
The following instructions must be followed from inside the directory $ROOT/P4R-Type/
.
To compile a P4info file into a Scala 3 package, use
sbt "runMain parseP4info <p4info-file> <package-name>"
where <p4info-file>
is the relative path of the P4info file to be compiled, and
<package-name>
is the name of the package to be generated.
The generated Scala package is written to stdout.
As an example of how to generate a package, navigate to the $ROOT/P4R-Type/
directory and run:
sbt "runMain parseP4info src/main/scala/examples/config1.p4info.json config1"
This will generate a Scala package based on the configuration in the config1.p4info.json
file
and write it to stdout. The generated Scala package contains:
- A set of match types capturing the dependencies between P4Runtime entities (tables, actions, ...)
- A
connect
function, which establishes a connection to a P4Runtime server and returs aChan
nel - A
Chan
class, usable to perform the P4Runtime operations (insert, delete, ...) supported by our P4R-Type API.
This section outlines the match types generated by our tool, which are also described in the paper.
As an example, consider a P4Info file with these tables and actions:
"tables": [
{
"preamble": {
"id": 50014192,
"name": "Process.ipv4_lpm",
"alias": "ipv4_lpm"
},
"matchFields": [
{
"id": 1,
"name": "hdr.ipv4.dstAddr",
"bitwidth": 32,
"matchType": "LPM"
}
],
"actionRefs": [
{ "id": 26706864 },
{ "id": 22338797 }
],
"size": "1024"
}
],
"actions": [
{
"preamble": {
"id": 22338797,
"name": "Process.drop",
"alias": "drop"
}
},
{
"preamble": {
"id": 26706864,
"name": "Process.ipv4_forward",
"alias": "ipv4_forward"
},
"params": [
{
"id": 1,
"name": "dstAddr",
"bitwidth": 48
},
{
"id": 2,
"name": "port",
"bitwidth": 9
}
]
}
]
This would produce the following types:
type TableMatchFields[TN] =
TN match
case "Process.ipv4_lpm" => (Option[("hdr.ipv4.dstAddr", LPM)]) | "*"
case "*" => "*"
type ActionName = "Process.drop" | "Process.ipv4_forward" | "*"
type TableAction[TN] <: ActionName =
TN match
case "Process.ipv4_lpm" => "Process.ipv4_forward" | "Process.drop" | "*"
case "*" => "*"
type ActionParams[AN] =
AN match
case "Process.drop" => Unit
case "Process.ipv4_forward" => (("dstAddr", ByteString), ("port", ByteString))
case "*" =&g
8000
t; "*"
The connect
function connects the controller to a target (i.e. a device that
supports P4 and P4Runtime, e.g. a network switch), returning a Chan
object
representing the connection (see below).
Why is the connect
function generated as part of a package, and not
type-parametric like the other P4R-Type API functions? This is done to instantiate
the type parameters of Chan
objects with the aforementioned match types, according
to the P4Info of the target device. This way, we can hide the complexity from the user
--- who is only required to invoke <package>.connect(...)
(where <package>
is
tenerated from the P4 configuration of the switch they are trying to connect to).
A Chan
object represents a connection between a target (i.e. a device that
supports P4 and P4Runtime, e.g. a network switch) and a controller (the program
acting as P4Runtime client). The Chan
class generated by our tool inherits from
an abstract Chan
class in the p4rtype
package, which is type-parametric.
The generated Chan
class is always instantiated with the match types from its
own package. By doing this, the type-parametric P4R-Type API functions that
take Chan
objects (e.g. insert
, delete
, ...) will have their types
constrained by the types from the associated package: this ensures the correctness
of API calls, and also helps the Scala compiler infer the type arguments whitout
the user having to provide them explicitly.
Internally, the Chan
class contains two functions, toProto
and fromProto
,
which convert table entries from their strongly-typed P4R-Type representation to
their underlying "loosely-typed" protobuf representations, and vice versa.
The following instructions must be followed from inside the directory $ROOT/P4R-Type/
.
The instructions for running all examples are given below. In general, each example can be run by launching:
sbt "runMain <main-function>"
where <main-function>
is the @main
function to be run
(usually named the same as the example file itself).
NOTE: when execution of an example terminates, the connection to each target device is closed, which causes the following message that you can ignore:
[ERROR] io.grpc.StatusRuntimeException: UNAVAILABLE: Channel shutdown
All provided examples require the P4R-Type VM to be running. Moreover, each example assumes a clean configuration where the network switches have no preconfigured table entries.
For this reason, before and in-between running any of the examples below, you need to perform the following steps on the VM:
- if the mininet network simulation is already running, close it (Ctrl+d);
- run
make clean
; - run
make build
; - run
make network
.
All of the examples described below affect the connectivity of the network to some degree.
To verify that the examples have had any effect, you can use e.g. a ping
command at the
mininet>
prompt on the VM, such as:
h1 ping h2
With this command, host h1
will then attempt to periodically send packets to h2
. The
sending of packets can be stopped with Ctrl+c.
If you attempt a ping
command before running one of the examples below, you will see
no output: this is because there is no route between any of the hosts in the virtual network
simulated by mininet
.
After you run one of the examples below, the P4 tables of the devices in the virtual network
will be updated, and packets will be able to flow (at least along some routes) As a
consequence, a ping
command like the one above will produce an output similar to:
mininet> h1 ping h2
PING 10.0.2.2 (10.0.2.2) 56(84) bytes of data.
64 bytes from 10.0.2.2: icmp_seq=1 ttl=62 time=6.15 ms
64 bytes from 10.0.2.2: icmp_seq=2 ttl=62 time=3.43 ms
64 bytes from 10.0.2.2: icmp_seq=3 ttl=62 time=3.27 ms
...
The example can be found in $ROOT/P4R-Type/src/main/scala/examples/forward_c1.scala
(with the erroneous, non-compiling code commented out) and can be executed by running
(on the host machine, from inside the directory $ROOT/P4R-Type/
):
sbt "runMain forward_c1"
Effect: The program will insert table entries for s1
and s2
such that
h1
and h2
can communicate with (ping) each other. To test connectivity, use
h1 ping h2
To see how the update changes connectivity, run the ping
command before
running the control program, and observe how the pings only start to succeed
after running the program.
The example can be found in $ROOT/P4R-Type/src/main/scala/examples/forward_c2.scala
and can be executed by running (on the host machine, from inside the directory
$ROOT/P4R-Type/
):
sbt "runMain forward_c2"
Effect: The program will insert table entries for s3
and s4
such that
h3
and h4
can communicate with (ping) each other. To test connectivity, use
h3 ping h4
To see how the update changes connectivity, run the ping
command before
running the control program, and observe how the pings only start to succeed
after running the program.
The example can be found in $ROOT/P4R-Type/src/main/scala/examples/bridge.scala
and
can be executed by running (on the host machine, from inside the directory
$ROOT/P4R-Type/
):
sbt "runMain bridge"
Effect: The program will insert table entries for all switches such that each host can communicate with any other host. To test connectivity, use
h1 ping h4
To see how the update changes connectivity, run the ping
command before
running the control program, and observe how the pings only start to succeed
after running the program.
The example can be found in $ROOT/P4R-Type/src/main/scala/examples/firewall.scala
and can be executed by running (on the host machine, from inside the directory
$ROOT/P4R-Type/
):
sbt "runMain firewall"
Effect: The program will first establish full connectivity between hosts
(using the previous example), then insert table entries into the firewall
table in
each switch, causing packets with destination addresses to h1
or h4
to be
dropped. Effectively, this means that communication is only possible between
h2
and h3
. To test connectivity, use
h2 ping h3
To see how the update changes connectivity, run the ping
command before
running the control program, and observe how the pings only start to succeed
after running the program.
The example can be found in $ROOT/P4R-Type/src/main/scala/examples/router.scala
and can be executed by running (on the host machine, from inside the directory
$ROOT/P4R-Type/
):
sbt "runMain router"
Effect: The program first establishes full connectivity between hosts, then
provides a CLI for inserting network address translation rules into s4
.
For the purpose of this example, we assume that h1
to h3
are subnets
part of the same local network, and h4
is an external network with a separate
address scheme.
For this example, first start mininet (in the VM) by running
make test_nat
Then, start the port forwarding program (to set up full connectivity).
Now, try to use the CLI to insert a rule. Each rule needs an external IP and port to
map to, and the corresponding internal host (which always uses an IP from 10.0.1.1
to 10.0.3.3
and listens on port 8080
). Add a rule for the external IP 1.1.1.1
,
external port 1111
, and internal host 1
. Then, try to read the rule.
You should see an output similar to
Ingress rules:
(1.1.1.1:1111) -> (10.0.1.1:8080)
Egress rules:
(10.0.1.1:8080) -> (1.1.1.1:1111)
Let us now test that the rule behaves correctly. For this example, we will not
use the standard ping
command to send packets, since it does not allow
us to set the values of packet header fields (such as TCP ports). Instead,
run the following command in mininet:
xterm h1 h4
This will open a small console window for hosts h1
and h4
.
In the window for h1
, run:
./receive.py
This will start a program on host h1
which listens for TCP packets on port 8080.
You can now use the send.py
program in the window for h4
to send packets to one
of the other hosts. Try to run:
./send.py 1.1.1.1 1111 "hello"
The message should then appear in the h1
window as a message from 10.0.4.4:8080
.
Finally, try to swap such that you listen with receive.py
on h4
and send from h1
.
In the window for h1
, run:
./send.py 10.0.4.4 8080 "hello again"
The window for h4
should display the message as received from the "translated" address
of h1
, namely 1.1.1.1:1111
.
The example can be found in $ROOT/P4R-Type/src/main/scala/examples/loadbalancer.scala
and can be executed by running (on the host machine, from inside the directory
$ROOT/P4R-Type/
):
sbt "runMain loadbalancer"
Effect: The program manages load balancing of packets going through s1
to h4
.
It will first establish full connectivity between hosts. Then, in intervals of 5
seconds (for a total of one minute), it will read from the CounterEntry at s1
and
modify its forwarding rules accordingly such that packets are evenly sent between
s2
, s3
and s4
.
For this example, first start mininet (in the VM) by running:
make test_lb
To send packets, use:
h1 ping h4
To see how the packets are distributed (and counted), run the ping
command after
running the control program. Observe how the control program counts the outgoing
packets and updates the outgoing port accordingly.
We now provide an small example to demonstrate how updating/extending a P4 configuration can cause existing P4Runtime programs to "go out of sync". Our P4R-Type API can detect these situations and produce type errors, thus preventing incorrect P4Runtime programs from compiling and running.
-
In the VM, replace the contents of file
config1.p4
(in the home directory of the user P4RType), with the contents ofconfig1_new.p4
(also in the same directory). The files are almost identical, except that thedstAddr
field in the IPv4 header is renamed todstIP
. -
In the VM, recompile the P4 file and P4Info file by running
make clean
followed bymake build
. This will generate a new P4Info file inbuild/config1.p4.p4info.json
. -
Copy the contents of the new P4Info file onto a file in your host machine, such as
$ROOT/P4R-Type/src/main/scala/examples/config1_new.p4info.json
. -
On the host machine, generate new types from the updated P4info file. If you use the file name above, the command is:
sbt "runMain parseP4info src/main/scala/examples/config1_new.p4info.json config1_new"
-
On the host machine, create a new Scala file
$ROOT/P4R-Type/src/main/scala/examples/config1_new.scala
, and copy&paste there the Scala code produced by the command at point 4. -
On the host machine, edit the file
$ROOT/P4R-Type/src/main/scala/examples/forward_c1.scala
by replacing the package nameconfig1
in lines 6 and 7 withconfig1_new
. -
On the host machine, try to compile the modified program above by running:
sbt compile
The compilation should fail, reporting an error around line 11 and 27: this reflects the fact that the code does not match the updated P4 configuration (it is referring to a field called
dstAddr
, but the field is now calleddstIP
). -
Fix the program by replacing all occurrences of
hdr.ipv4.dstAddr
withhdr.ipv4.dstIP
(line 11 and 27). The program should now compile. -
To test that the program works as intended, follow the same steps as for running the Simple IPv4 table update example (remember to also start the network in the VM).
If you are familiar with the P4 language, you can follow the steps above to try more experiments: you
can apply other changes to the file config1.p4
(e.g. rename actions, change their parameter types,
modify the associations between tables and actions...) and observe how the changes are reflected in
the types generated by P4R-Type, and how a program using the P4R-Type API must be updated in order to
type-check and compile after such changes.