This project is a compact stateful IPv4 firewall for P4_16 v1model, BMv2, P4Runtime, and Mininet.
IPv4 forwarding, ACL evaluation, TCP state, UDP pseudo-sessions, and expiration all run in the P4 data plane.
The Go controller only installs and verifies static configuration.
The TCP tracker is intentionally small and is not a production or RFC-complete connection tracker.
h1 (INSIDE) -- port 1 \
s1 (P4 firewall) -- port 3 -- h3 (OUTSIDE)
h2 (INSIDE) -- port 2 /
| Host | Zone | IPv4 address | Host MAC | Gateway | Gateway MAC |
|---|---|---|---|---|---|
| h1 | INSIDE | 10.0.1.1/24 |
02:00:00:00:01:01 |
10.0.1.254 |
02:aa:00:00:01:fe |
| h2 | INSIDE | 10.0.2.1/24 |
02:00:00:00:02:01 |
10.0.2.254 |
02:aa:00:00:02:fe |
| h3 | OUTSIDE | 10.0.3.1/24 |
02:00:00:00:03:01 |
10.0.3.254 |
02:aa:00:00:03:fe |
The switch uses P4Runtime device ID 1, gRPC port 9559, and Thrift port 9090. Hosts use static default routes and permanent gateway neighbors, so the P4 program does not handle ARP. Relevant interface offloads are disabled.
The ingress path validates the packet, classifies the ingress port, performs an IPv4 route lookup, evaluates the static ACL, and then applies same-zone or stateful cross-zone policy. A route supplies the egress zone, output port, and Ethernet rewrite, so route validity is established before policy can forward a packet.
The installed ACL rules are evaluated by descending priority:
| Priority | Rule | Decision |
|---|---|---|
| 300 | INSIDE to OUTSIDE, TCP destination port 8081, source 10.0.1.0/24, destination 10.0.3.0/24 |
Drop |
| 200 | INSIDE to OUTSIDE, UDP destination port 9000 | Allow |
| 100 | INSIDE to OUTSIDE, TCP | Allow |
The ACL table can match ingress and egress zones, IPv4 source and destination, protocol, and source and destination transport ports. An explicit drop is terminal and takes precedence over lower-priority allow rules. An ACL allow is required to create new INSIDE-to-OUTSIDE state. An ACL miss cannot create state, although matching existing state may pass.
The default behavior is:
- valid INSIDE-to-INSIDE TCP or UDP is allowed unless explicitly denied;
- ACL-allowed INSIDE-to-OUTSIDE TCP or UDP may create state;
- matching OUTSIDE-to-INSIDE return traffic may use existing state;
- unsolicited OUTSIDE-to-INSIDE TCP or UDP is dropped;
- route misses, unknown ingress ports, and all other traffic are dropped.
Accepted packets preserve IPv4 and transport addresses, rewrite both Ethernet addresses, decrement TTL exactly once, and recompute the IPv4 header checksum.
Both directions use this canonical key:
internal IPv4, external IPv4, internal port, external port, IP protocol
CRC32 maps the key into one of 1,024 direct-mapped slots. Parallel registers store the full canonical key, state, and a 48-bit last-seen timestamp. Every stored key field must match before state can authorize a packet. A different flow that collides at the same index is dropped and cannot overwrite or refresh the resident entry.
Expiration is lazy. It is evaluated when the resident key next accesses its slot; there is no controller scan. Consequently, a different colliding key cannot reclaim an expired resident entry until the resident key accesses or clears that slot. This fixed-size design deliberately trades availability for fail-closed collision behavior.
The timestamp is BMv2's 48-bit ingress timestamp in microseconds. Unsigned age subtraction assumes an entry is not left untouched across a full timestamp period, approximately 8.9 years.
NONE -- allowed INSIDE SYN --> SYN_SENT
SYN_SENT -- matching OUTSIDE SYN+ACK --> SYN_REPLIED
SYN_REPLIED -- matching INSIDE ACK --> ESTABLISHED
Initial SYN and SYN+ACK packets use exact simplified flag combinations.
In ESTABLISHED, a matching packet must have ACK set and SYN, FIN, and RST clear.
Other transitions fail closed.
An exact RST or RST+ACK in tracked state is forwarded and immediately clears the slot.
A bare FIN is dropped without clearing state.
The tested inside-originating FIN+ACK in ESTABLISHED is forwarded and immediately clears state; a later peer ACK is therefore dropped.
No bidirectional TCP closing state is implemented.
Handshake state expires after 2 seconds. Established state expires after 3 seconds. Matching traffic that retains handshake or established state refreshes the applicable timer.
An ACL-allowed INSIDE-to-OUTSIDE datagram creates an active pseudo-session. Matching datagrams in either direction refresh it. Unsolicited return traffic and wrong address or port tuples are dropped. UDP state expires after 2 seconds.
Only Ethernet carrying IPv4 with TCP or UDP is supported. The firewall drops:
- non-IPv4 Ethernet and unsupported IPv4 protocols;
- IPv4 versions other than 4 and IPv4 options (
IHL != 5); - bad IPv4 header checksums, total lengths below 20 bytes, or total lengths larger than the received IPv4 packet;
- MF or a nonzero fragment offset;
- TTL values of 1 or less;
- TCP data offsets below five words or nonzero TCP reserved bits;
- UDP lengths below eight bytes or inconsistent with the IPv4 payload length;
- route misses, ACL denial, invalid state transitions, flow-key collisions, and unsolicited return traffic with missing or expired state.
TCP and UDP checksums are not validated. TCP sequence numbers, acknowledgment numbers, and options are not interpreted.
The controller uses p4runtime-go-controller and is a one-shot configurator.
It becomes primary, installs the P4Info and BMv2 device configuration, inserts three zone entries, three routes, and three ACL entries, reads the pipeline and table entries back, and exits.
It does not receive normal packets or maintain flow state.
Against a running configured switch, readback can be repeated without writing the pipeline or tables:
build/firewall-controller -verify-onlyVerification still participates briefly in P4Runtime primary arbitration.
A normal install uses insert operations and is intended for a fresh --no-p4 switch rather than as an idempotent reconciliation command.
- Linux with root privileges for Mininet and BMv2 interfaces;
p4c-bm2-sswith P4_16 andv1modelsupport;simple_switch_grpc;- Mininet, Python 3, and Scapy;
- Go 1.25.3 or a compatible newer toolchain;
ipandethtool.
The Go module pins the P4Runtime controller and protocol dependencies. No separate P4 toolchain installation is performed by this project.
make buildBuild output is written under ignored build/.
To start the topology, install and verify the pipeline, and enter the Mininet CLI:
make runThe runtime requires ports 9559 and 9090 and the deterministic interface names to be free.
Enter exit in the Mininet CLI to stop the owned switch, hosts, links, and temporary runtime directory.
Run all build, controller, artifact, and packet-level integration tests with:
make testmake test-unit does not create Mininet links.
make test-integration runs the privileged live topology suite.
The packet tests exercise real TCP and UDP sockets plus crafted traffic for ACL boundaries, TCP transitions, UDP expiry, deterministic CRC32 collisions, fragments, malformed headers, route misses, TTL, checksum recomputation, MAC rewriting, and duplicate forwarding.
Captures remain in memory; BMv2 logs, sockets, and other runtime files use temporary directories and are removed during cleanup.
Remove project-generated build output and Python bytecode with:
make clean- This is a reference firewall, not a production TCP connection tracker.
- IPv4 fragmentation and IPv4 options are unsupported and dropped.
- IPv6, ARP, ICMP, and protocols other than TCP and UDP are unsupported.
- The flow table has 1,024 direct-mapped slots and no collision chaining.
- TCP closing, sequence validation, normalization, and transport-checksum validation are intentionally out of scope.
- NAT, routing protocols, DPI, VPN functions, and controller-managed conntrack are not implemented.