A software-defined NAT/PAT system that implements network address translation entirely in the P4 data plane, paired with a Python P4Runtime controller that manages the translation state and enforces a stateful firewall. Built for the Programmable Networks module (SCC.333) at Lancaster University.
The project recreates the behaviour of a home or edge router on two programmable switches. A PAT switch sits between a private home network and the wider internet and performs many-to-one translation, so a whole private subnet can share a single public IP. A second NAT switch performs static one-to-one translation between a pair of server networks.
The split between data plane and control plane is the point of the project. The data plane
(the P4 programs) rewrites packets at line rate on the switch. The control plane
(controller.py) connects to each switch over gRPC and P4Runtime, installs the compiled
forwarding pipeline, manages the match-action table entries, and reacts to any packet the
switch cannot translate on its own.
- Static NAT (
nat.p4, switchs2): a one-to-one mapping that rewrites source and destination IP addresses as traffic crosses the boundary between two networks. - Dynamic PAT / NAPT (
pat.p4, switchs1): the many-to-one translation a real home router performs. A whole private subnet shares one public IP, with connections kept distinct by rewriting the transport-layer port as well as the IP. - Reactive flow installation: the first packet of an unknown outbound flow is punted to the controller, which installs matching forward and reverse rules. Every later packet of that flow is then translated entirely in the data plane with no controller involvement.
- Port forwarding: internal services can be exposed on a public port, configured at runtime through the REST API.
- Stateful inbound filtering: an unsolicited inbound TCP connection that matches no translation or port-forward rule is refused by having the controller build and send a TCP RST. This gives the NAT a default-deny posture and makes it act as a security boundary.
Data plane (P4). Each switch parses Ethernet, IPv4, TCP and UDP, matches packets against
its tables (t_nat_src and t_nat_dst on the NAT switch, t_pat_out and t_pat_in on the
PAT switch), rewrites the relevant addresses and ports in place, and recomputes the IPv4 and
L4 checksums in the egress pipeline. A packet that matches no entry hits the table default
action; on the PAT switch that action forwards the packet to the controller (CPU_PORT).
Control plane (Python). controller.py opens a P4Runtime session to each switch, pushes
the forwarding pipeline, and runs a packet-processing loop per switch. When a packet arrives
it decides whether to install a new PAT translation, apply a configured port-forward rule, or
refuse the connection with a TCP RST. The current state can be inspected and changed at
runtime through a small Flask REST API.
A private home network sits behind the PAT switch. Web and cloud server networks sit behind the NAT switch. Linux routers and the two BMv2 switches are wired together to emulate a small internet.
Home network (private) Server networks
192.168.0.0/24 10.0.0.0/16, 10.10.0.0/16
+------------+ +------------+
| homePC | s1 (PAT) s2 (NAT) | web1 / web2|
| tablet | +--------+ +--------+ | cloud1 |
| phone |--+--| pat.p4 |--+ +-| nat.p4 |--+---| |
+------------+ | +---+----+ | | +---+----+ | +------------+
home router | [ internet ] | web/cloud routers
| router |
| P4Runtime/gRPC |
+--------+---------+
|
+--------+--------+
| controller.py | (Python control plane
| + Flask REST | + REST API on :8080)
+-----------------+
The controller serves a small JSON API on port 8080.
| Method and path | Purpose |
|---|---|
GET /api/status |
Health check that reports the number of connected switches. |
GET /api/switches |
List the connected switches and their addresses. |
GET /api/nat/list |
Active PAT translations, read directly from the P4 table. |
GET /api/port-forward/list |
Configured port-forwarding rules. |
GET /api/port-forward/add/<internal_ip>/<internal_port>/<external_port> |
Add a port-forwarding rule at runtime. |
p4src/nat.p4: static one-to-one NAT data-plane program, loaded onto switchs2.p4src/pat.p4: dynamic PAT/NAPT and port forwarding data-plane program, loaded ontos1.controller.py: the P4Runtime control plane and Flask REST API, with aNATControllerand aPATControllerbuilt on a shared base class.controller.json: declares which switches to connect to and the P4 artifacts each one uses.mininet/topo.py: the Mininet topology (hosts, Linux routers, switches and links).mininet/flask_ip.py: a tiny HTTP server run on each host that returns the caller's source IP, used to confirm that translation is working.proto/andutil/: the P4Runtime protobuf definitions and client libraries, vendored from the p4lang tutorials (Apache-2.0).
- P4_16 on the
v1modelarchitecture for the data-plane programs. - BMv2 and Stratum software switches to run the compiled P4.
- Mininet for the emulated network topology.
- P4Runtime, gRPC and Protocol Buffers for the control-plane API.
- Python 3 with Flask for the REST API and Scapy for building the TCP RST packets.
- Docker and docker-compose for a reproducible switch and Mininet environment.
- Make to build the P4 programs and orchestrate the multi-process system.
The build and run flow is driven by make, and three terminals are convenient.
Terminal A builds the P4 programs and starts the network:
make stop # tear down any previous run
make p4-build # compile nat.p4 and pat.p4 with p4c (via Docker)
make start # bring up Mininet and the Stratum switches
make mn-cli # attach to the Mininet CLI (Ctrl-D detaches, the network keeps running)Terminal B starts the control plane:
make controller # connects to the switches and serves the REST API on :8080Terminal C drives traffic and inspects state. Generate traffic from the Mininet CLI in
terminal A, for example homePC curl <server>, then query the API:
curl localhost:8080/api/nat/list
# expose internal 192.168.0.10:80 on public port 9090:
curl localhost:8080/api/port-forward/add/192.168.0.10/80/9090See the Makefile for the full set of targets. make deps pulls the required Docker images
and make clean removes build artifacts.
This is an educational project. Choices such as the fixed set of NAT mappings, the in-memory
port-forward table, and the lack of an ARP responder are appropriate for a coursework demo
but would need more work for any real deployment. It is released under the Apache License 2.0
(see LICENSE). The protobuf definitions in proto/ and the client libraries in util/ are
vendored from the p4lang tutorials and keep their original Apache-2.0 headers; see NOTICE for
details.