I/O-free dual-stack Ethernet forward/filter/NAT firewall
0

Configure Feed

Select the types of activity you want to include in your feed.

OCaml 92.6%
Dune 2.3%
Other 5.1%
45 1 0

Clone this repository

https://git.vm.fail/gazagnaire.org/ocaml-firewall https://git.vm.fail/did:plc:3sha7yemsdi6wupjpq77e3ai
ssh://git@git.recoil.org:2222/gazagnaire.org/ocaml-firewall ssh://git@git.recoil.org:2222/did:plc:3sha7yemsdi6wupjpq77e3ai

For self-hosted knots, clone URLs may differ based on your setup.


README.md

firewall#

An I/O-free IPv4/Ethernet forward/filter/NAT firewall, in the shape of the Qubes mirage-firewall core, built as a zero-copy proxy data plane. Two layers: the engine (Firewall) makes the per-packet decision and rewrites it in place; the Ethernet plane (Firewall.Eth) drives the engine over whole frames — framing, ARP and NDP, a learned neighbour table and next-hop selection — so it switches and NATs in userspace for a one-LAN, one-uplink topology with no kernel bridge. Both layers are I/O-free; the companion firewall-eio package adds an Eio backend that runs the plane over a pair of ports. Dual-stack: IPv4 with masquerade NAT, IPv6 as stateless forward/filter. Ethernet only.

Installation#

Install with opam:

$ opam install firewall

If opam cannot find the package, it may not yet be released in the public opam-repository. Add the overlay repository, then install it:

$ opam repo add samoht https://tangled.org/gazagnaire.org/opam-overlay.git
$ opam update
$ opam install firewall

Overview#

Hand Firewall.ingress the IPv4 packet of a received frame, in your own buffer. The engine reads the packet's fields in place through the wire codec, consults a policy and a connection-tracking table, and returns a verdict. On a forward or NAT decision it rewrites the packet -- addresses, ports, TTL, and the IP and transport checksums -- in place in that buffer and never builds a new packet; you proxy the same bytes on to the egress interface.

It is parameterised over a policy through a functor, so the engine never mentions a tenant, a zone, or a capability: the rules come only through POLICY.

Fast path#

The design follows how production NAT data planes (Linux nftables flowtable, OVS, VPP) stay fast:

  • Zero-copy field access. Reads and writes go straight to the buffer via the wire codec's staged Codec.get / Codec.set; no intermediate record is allocated on the hot path.
  • Incremental checksums (RFC 1624). A NAT rewrite adjusts the IP and transport checksums by the delta of the changed words -- O(1) -- rather than re-summing the packet.
  • Flow cache. The first packet of a flow takes the slow path (policy, then a conntrack insert); every later packet is a table lookup and an in-place rewrite.

Features#

  • Masquerade (source) NAT (RFC 3022) for TCP, UDP and ICMP echo, with stateful reverse translation of replies.
  • Destination NAT / port forwarding (RFC 3022, the `Dnat action) for TCP and UDP: publish a service on the routed address and forward it to a private tenant, with the reply's source translated back.
  • RFC 1918 private-range classification (Packet.is_private) for policies.
  • Connection tracking with per-protocol idle timeouts and a gc sweep.
  • An ARP responder (arp_reply) for the firewall's own addresses.
  • TTL decrement with the time-exceeded drop, as a router.

Usage#

module Policy = struct
  type t = unit
  let mac () = Macaddr.of_string_exn "..."
  let local_ips () = [ Ipaddr.V4.of_string_exn "203.0.113.1" ]
  let nat_ip () = Ipaddr.V4.of_string_exn "203.0.113.1"
  (* masquerade traffic from the private network, drop the rest *)
  let action () (pkt : Firewall.Packet.t) =
    if Firewall.Packet.is_private pkt.src then `Nat else `Drop
end

module Fw = Firewall.Make (Policy)

let proxy engine ~now buf ~off ~len =
  match Firewall.ingress engine ~now buf ~off ~len with
  | engine, Firewall.Forward dst -> (engine, `Send dst)  (* buf is rewritten in place *)
  | engine, Firewall.Drop reason -> (engine, `Dropped reason)

Scope#

The engine is the decision and rewrite core; with the Firewall.Eth plane on top it is the forwarding/filtering/NAT data plane of a userspace router for one LAN behind one uplink, and firewall-eio drives the I/O. It moves packets between addresses that already exist — a data plane, not a whole gateway appliance.

Separate by design — composed, not built in:

  • Address assignment: IPAM (ipam) and DHCP (dhcp). Which addresses exist and how a host obtains one are control-plane services with their own state and I/O, not per-packet decisions; building them in would cost the data plane its pure, I/O-free, unikernel-portable shape. Like gVisor's netstack and the QubesOS mirage-firewall this mirrors — both take their addressing from the runtime and run no DHCP — addresses here are assigned statically or by the orchestrator. (slirp / vpnkit bundle a DHCP+DNS server instead, because they emulate a whole network for an unmodified guest; ours are configured by the orchestrator.)
  • A general routing table. One LAN, one uplink; multi-route / longest-prefix lookup is a router's job, not this.
  • The ruleset (POLICY) and the I/O driver (the caller's, or firewall-eio's).

IPv6 is supported as a sibling of the IPv4 path: Firewall.ingress6 is a stateless forward/filter (no NAT — IPv6 hosts are globally routable; the hop limit is decremented in place and the extension-header chain is walked to find the real transport), and the plane answers NDP Neighbor Solicitations from Firewall.Eth (the ICMPv6 messages live in nox-icmp's Icmp.V6). A v6 flow is not connection-tracked.

Not built yet — a clean addition when a consumer needs it, a sibling plane over the same engine rather than a redesign:

  • Non-Ethernet uplinks (point-to-point / raw IP: no ARP/NDP, next-hop is the link itself).

Accept-flow offload (tracking an accepted non-NAT flow so later packets skip the policy) is a natural extension of the conntrack table.

  • mirage-firewall - the Qubes unikernel firewall whose forward/filter/NAT core this mirrors, here generalised over a policy and reduced to a I/O-free engine.
  • Linux netfilter conntrack / nftables flowtable - the reference fast-path NAT design (flow offload + incremental checksums).
  • RFC 3022 (traditional NAT/NAPT), RFC 1624 (incremental checksum update), RFC 5382 (TCP NAT timeouts), RFC 1918 (private address ranges).

License#

ISC License. See LICENSE.md for details.