lpf

lpf

Next-generation Linux firewall with PF-style policy

PF-style syntax eBPF datapath Z3 formal proofs auto-rollback
Why

One policy. Guarded apply. Explainable.

Linux packet policy spans nftables, routing, tc, conntrack, and logging. lpf unifies them into a single PF-style policy file with guarded deployment, packet explainability, and formal Z3 verification.

One policy

One PF-style file describes filtering, NAT, routing, queues, tables, and logging.

Safe changes

Plans, diffs, guarded apply, confirmation timers, history, and rollback.

Explainable

Ask what would change, why a packet matched, and what state gets restored before applying.

Consolidation
flowchart LR accTitle: lpf consolidates Linux networking subsystems accDescr: nftables, ip route, tc, conntrack, and logging are unified into a single lpf policy file NFT["nftables"] --> LPF["lpf"] RT["ip route"] --> LPF TC["tc"] --> LPF CT["conntrack"] --> LPF LOG["logging"] --> LPF LPF --> POL["one policy.lpf"] classDef data fill:#0a1322,stroke:#3a8dff,color:#dce9ff; classDef guard fill:#1a160c,stroke:#d6b162,color:#f2e6c4; class LPF data; class POL guard;

# nftables + routing + tc + conntrack + logging → lpf

Flow

From policy file to kernel datapath.

One source file is checked, lowered to a typed IR, planned, proven with Z3, then applied behind a rollback timer onto an nftables or eBPF datapath. Every stage is inspectable.

Control plane
flowchart LR accTitle: lpf control plane pipeline accDescr: policy file flows through check, typed IR, plan, Z3 verification, and guarded apply into nftables and eBPF datapaths P["policy.lpf"] --> C["check"] C --> IR["typed IR"] IR --> PL["plan"] PL --> V["verify · Z3"] V --> A["apply · confirm"] A --> NFT["nftables"] A --> BPF["eBPF · XDP"] IR -.-> EX["explain"] classDef verify fill:#0a1622,stroke:#5ed7ff,color:#dff4ff; classDef guard fill:#1a160c,stroke:#d6b162,color:#f2e6c4; classDef data fill:#0a1322,stroke:#3a8dff,color:#dce9ff; class V verify; class A guard; class NFT,BPF data;

# check → IR → plan → verify → apply → datapath

Guarded apply
stateDiagram-v2 accTitle: lpf guarded apply state machine accDescr: states flow from Checked through Planned to Pending, where lpf confirm reaches Confirmed and timer expiry triggers RolledBack [*] --> Checked Checked --> Planned Planned --> Pending: apply · confirm 60s Pending --> Confirmed: lpf confirm Pending --> RolledBack: timer expires Confirmed --> [*] RolledBack --> [*] classDef pend fill:#1a160c,stroke:#d6b162,color:#f2e6c4; classDef ok fill:#0b1a12,stroke:#69cc8d,color:#d4f3e0; classDef bad fill:#1c0d0d,stroke:#d26060,color:#f3d6d6; class Pending pend class Confirmed ok class RolledBack bad

# no confirm → automatic rollback

Packet decision
flowchart TD accTitle: lpf packet decision flow accDescr: packet matched against rules, no match defaults to deny, matching rule triggers pass, block, or reject PKT["packet"] --> M{"matches rule?"} M -- no --> DEF["default deny"] M -- yes --> ACT{"action"} ACT -- pass --> P["pass · keep state"] ACT -- block --> B["block · log"] ACT -- reject --> R["reject"] classDef ok fill:#0b1a12,stroke:#69cc8d,color:#d4f3e0; classDef bad fill:#1c0d0d,stroke:#d26060,color:#f3d6d6; class P ok; class B,DEF,R bad;

# lpf explain traces this path for any packet

eBPF · XDP datapath
flowchart LR accTitle: lpf eBPF XDP datapath accDescr: packets arrive at the NIC and hit the XDP hook which consults eBPF maps for a verdict, passing to the kernel stack or dropping, while events stream through a ring buffer to lpf userspace RX["packet · NIC"] --> XDP["XDP hook"] XDP --> MAPS["eBPF maps"] MAPS --> VRD{"verdict"} VRD -- pass --> STK["kernel stack"] VRD -- drop --> DROP["XDP_DROP"] XDP -.-> RB["ring buffer"] RB -.-> US["lpf userspace"] classDef ok fill:#0b1a12,stroke:#69cc8d,color:#d4f3e0; classDef bad fill:#1c0d0d,stroke:#d26060,color:#f3d6d6; classDef data fill:#0a1322,stroke:#3a8dff,color:#dce9ff; class STK ok; class DROP bad; class MAPS,RB data;

# XDP drops before sk_buff · per-CPU counters · ring buffer events

Install

Install from GitHub Releases or source.

Native .deb and .rpm packages on GitHub Releases. Clone the repo for the OCaml build, tests, and fixtures.

Debian / Ubuntu
$ OUT=/tmp/lpf-release && mkdir -p "$OUT"
$ gh release download \
  --repo ingresslabs/lpf \
  --pattern 'lpf_*_amd64.deb' \
  --dir "$OUT"
$ sudo apt install "$OUT"/lpf_*_amd64.deb
RPM hosts
$ OUT=/tmp/lpf-release && mkdir -p "$OUT"
$ gh release download \
  --repo ingresslabs/lpf \
  --pattern 'lpf-*.x86_64.rpm' \
  --dir "$OUT"
$ sudo dnf install "$OUT"/lpf-*.x86_64.rpm
Source checkout
$ git clone https://github.com/ingresslabs/lpf.git && cd lpf
$ opam switch create . ocaml-base-compiler.5.1
$ opam install . --deps-only --with-test
$ dune build && dune runtest
Usage

Check first. Diff live. Apply guarded.

Policy files as code: validate, inspect drift, trace packets, then apply with a confirmation timer.

Policy loop
$ lpf check /etc/lpf.conf
$ lpf fmt --check /etc/lpf.conf
$ lpf plan --json /etc/lpf.conf
$ lpf diff --live /etc/lpf.conf
Guarded deploy
$ lpf apply --confirm 60s /etc/lpf.conf
# verify the session still reaches the host
$ lpf confirm
Explain a packet
$ lpf explain --src 10.0.0.5 --dst 1.1.1.1 \
  --dport 443 --tcp --in /etc/lpf.conf
Configs

Policy examples

Web serverpublic HTTP/HTTPS, restricted SSH
set default deny

interface wan = "eth0"

table <admin> { 198.51.100.10, 203.0.113.0/24 }

pass in log on wan proto tcp from any to any port 80
pass in log on wan proto tcp from any to any port 443
pass in log (user) on wan proto tcp from <admin> to any port 22 keep state
block in log (all) on wan proto tcp from any to any port 22
pass out on wan proto udp from any to any port 53 keep state
pass out on wan proto tcp from any to any port 80 keep state
pass out on wan proto tcp from any to any port 443 keep state
block in log from any to any
Reverse proxypublic redirects to internal app listeners
set default deny

interface app = "eth1"
interface wan = "eth0"

table <admin> { 198.51.100.10, 203.0.113.0/24 }
table <apps> { 10.20.0.10, 10.20.0.11 }

rdr on wan proto tcp from any to any port 80 -> 10.20.0.10 port 8080
rdr on wan proto tcp from any to any port 443 -> 10.20.0.10 port 8443

pass in log on wan proto tcp from any to any port 80
pass in log on wan proto tcp from any to any port 443
pass in on app proto tcp from <apps> to any port 8080 keep state
pass in on app proto tcp from <apps> to any port 8443 keep state
pass in log (user) on wan proto tcp from <admin> to any port 22 keep state
pass out on wan proto tcp from any to any port 443 keep state
block in log from any to any
NAT gatewayLAN masquerade and controlled egress
set default deny

interface lan = "eth1"
interface wan = "eth0"

table <blocked> { 10.0.0.66, 10.0.0.67 }
table <lan_hosts> { 10.0.0.0/24 }

nat on wan from <lan_hosts> to any -> wan

block in log (user) on lan from <blocked> to any
pass in on lan from <lan_hosts> to any
pass out on wan proto udp from <lan_hosts> to any port 53 keep state
pass out on wan proto tcp from <lan_hosts> to any port 80 keep state
pass out on wan proto tcp from <lan_hosts> to any port 443 keep state
pass out on wan proto icmp from <lan_hosts> to any keep state
block out log (all) on wan from any to any
Workstation egressdefault-deny client outbound policy
set default deny

interface uplink = "wlan0"

table <dns> { 1.1.1.1, 9.9.9.9 }
table <update_mirrors> { 198.51.100.20, 203.0.113.20 }

pass out on uplink proto udp from any to <dns> port 53 keep state
pass out on uplink proto tcp from any to any port 80 keep state
pass out on uplink proto tcp from any to any port 443 keep state
pass out on uplink proto udp from any to any port 123 keep state
pass out on uplink proto tcp from any to <update_mirrors> port 22 keep state
block in log (all) on uplink from any to any
block out log (user) on uplink from any to any
DNS resolverLAN clients, upstream DNS, admin SSH
set default deny

interface lan = "eth1"
interface wan = "eth0"

table <admin> { 10.0.0.10, 10.0.0.11 }
table <clients> { 10.0.0.0/24, 192.168.10.0/24 }
table <upstream_dns> { 1.1.1.1, 9.9.9.9 }

pass in log on lan proto udp from <clients> to any port 53
pass in log on lan proto tcp from <clients> to any port 53
pass out on wan proto udp from any to <upstream_dns> port 53 keep state
pass out on wan proto tcp from any to <upstream_dns> port 53 keep state
pass in on lan proto tcp from <admin> to any port 22 keep state
block in log (user) on wan proto udp from any to any port 53
block in log (user) on wan proto tcp from any to any port 53
block in log from any to any