NFTables.Port
NFTables.Port is the low-level communication layer that bridges Elixir and the Linux kernel's nftables firewall.
Port component for NFTables. Provides a Zig-based native port executable for communicating with Linux nftables via the official libnftables JSON API.
Overview
NFTables.Port provides:
- Native Zig Port Executable - High-performance port process with
CAP_NET_ADMINcapability - libnftables JSON API - Requests go to the official nftables JSON API unchanged, and replies come back unchanged
- Automatic Framing - 4-byte length-prefixed packets for reliable communication
- Process Isolation - Port process crashes don't affect the Elixir VM
- Synchronous API - Simple request/response pattern with timeout support
Architecture
flowchart TD
API[NFTables<br/>High-level Elixir API]
GenServer[NFTables.Port<br/>GenServer]
Port[Erlang Port<br/>Zig executable]
Lib[libnftables<br/>C library]
Kernel[Linux Kernel<br/>nftables]
API --> GenServer
GenServer --> Port
Port --> Lib
Lib --> Kernel
Installation
Add nftables_port to your dependencies in mix.exs:
def deps do
[
{:nftables_port, "~> 0.5"}
]
end
Requirements
- Linux kernel with nftables support (kernel 3.13+)
- libnftables library installed (
nftablespackage on most distros) - Zig compiler (for building from source)
- CAP_NET_ADMIN capability on the port executable
Setting Capabilities
The port executable requires CAP_NET_ADMIN to communicate with the kernel firewall:
# After compilation
sudo setcap cap_net_admin=ep _build/dev/lib/nftables_port/priv/port_nftables
mix compile does not set it; it checks with getcap and warns if it is
missing. mix nftables_port.install (below) installs the executable and sets
the capability in one step.
Usage
Direct Usage
# Start the port. name: is optional; the nftables library looks the port up
# as NFTables.Port when it is not given a pid.
{:ok, pid} = NFTables.Port.start_link(name: NFTables.Port)
# Send a request to list tables
request = ~s({"nftables": [{"list": {"tables": {}}}]})
# The reply is returned unmodified, and is not always JSON: libnftables' JSON
# output on success, "" for a success with no output (add, delete, flush),
# or libnftables' error text, which is plain text, on failure.
case NFTables.Port.commit(pid, request) do
{:ok, ""} -> :ok
{:ok, reply} ->
case JSON.decode(reply) do
{:ok, data} -> {:ok, data}
{:error, _} -> {:error, reply}
end
{:error, {:port_exit, status}} -> {:error, {:port_exit, status}}
end
# Stop the port
NFTables.Port.stop(pid)
Any number of processes may call commit/3 at once; each receives the
response to its own request. Start failures are returned from start_link/1
as {:error, reason}. See the NFTables.Port documentation for the reasons.
NFTables.Port.info/1 reports which executable a port is running and what it
announced at start: its path and OS pid, the protocol version, the
nftables_port version it was built from, and whether it holds
CAP_NET_ADMIN:
%{path: _, os_pid: _, protocol: 1, version: "0.5.0", net_admin: true} =
NFTables.Port.info(pid)
With NFTables
Most applications use NFTables.Port through the NFTables library, which builds requests and decodes responses. Its documentation covers that API. This package is only the transport.
Port Executable Location
The port executable is located using this resolution order:
PORT_NFTABLES_PATH, if set. It is then the only candidate: a missing file is{:error, {:port_not_found, path}}, never a fallback to another binary./usr/local/sbin/port_nftables(system-wide installation)/usr/sbin/port_nftables(system-wide installation)port_nftablesin the application's priv directory
For production deployments, either:
- Set
PORT_NFTABLES_PATHto specify a custom location - Install to
/usr/local/sbin/port_nftables
Building from Source
The Zig port executable is built automatically during mix compile:
mix deps.get
mix compile
The build process:
- Compiles the Zig source code in
native/src/ - Installs the executable in the build:
_build/<env>/lib/nftables_port/priv/port_nftables, rebuilt whenever a file undernative/, the Zig version or nftables_port's own version changes, so upgrading the dependency rebuilds it too. A rebuilt file has no capability: set it again (dev_docs/capabilities.md). - Checks the capability with
getcapand warns if it is missing (it never sets it)
Installing to System Location
See dev_docs/security.md for how to keep your system secure.
For production deployments, install the port executable to a system location:
# Install to default location (/usr/local/sbin/port_nftables)
sudo mix nftables_port.install
# Install to custom location
sudo mix nftables_port.install /usr/sbin/port_nftables
# Install to custom directory (will create port_nftables in that directory)
sudo mix nftables_port.install /opt/nftables/bin/
The install task:
- Copies the compiled executable next to the target, as
<target>.new - Sets executable permissions (750) and the
CAP_NET_ADMINcapability on it - Renames it over the target, so a running port is untouched and its next start runs the new file: no need to stop the application to upgrade
- Leaves the target unchanged if the copy or the mode fails, and installs but
says the capability is NOT set (with the command to set it) if
setcapis missing or refused
After installation to a standard location (/usr/local/sbin or /usr/sbin), NFTables.Port will automatically find the executable. For custom locations, set the PORT_NFTABLES_PATH environment variable:
export PORT_NFTABLES_PATH=/opt/nftables/bin/port_nftables
An installed executable is a copy, and it does not change when the dependency does: re-run the install after upgrading nftables_port. A port whose executable was built from a different nftables_port version starts, but logs a warning naming both versions; one speaking a different protocol version is refused.
Development
Toolchain
- Zig 0.15.2, pinned in
.tool-versions(asdf install). Zig 0.16 does not build the port. - System packages (Debian/Ubuntu):
libnftables-dev libcap-dev pkg-config build-essential.
Testing
mix test # Elixir suite
(cd native && zig build test) # the executable's Zig unit tests
Most of the suite drives a fake executable and needs nothing special. The
:kernel tests run the real port_nftables against libnftables, so they
need Linux with nftables, the libnftables headers and Zig to build it, and
CAP_NET_ADMIN (root, in practice). They only touch tables named
unified_test_*. The :netns tests write sysctls, so they run only when
NFTABLES_PORT_NETNS=1 says the suite is inside a network namespace made for
it. To keep everything off the host's firewall and sysctls, run as CI does:
as root, in a fresh network namespace:
sudo unshare --net -- env NFTABLES_PORT_NETNS=1 mix test
Continuous integration
Every branch push runs .gitlab-ci.yml
(spec 08, REQ-TEST-005):
| Job | Checks |
|---|---|
format |
mix format --check-formatted, zig fmt --check native/ |
compile |
mix compile --force --warnings-as-errors in dev and prod; zig build test; mix docs --warnings-as-errors |
audit |
mix hex.audit |
test |
the whole suite, :kernel and :netns included, with --warnings-as-errors, on a privileged runner inside unshare --net; this includes the check of the Hex package's contents |
surfex |
the spec, tests and code held to each other: every requirement verified by a test that passed in this pipeline, nothing unvalidated |
Every job first checks that its Zig is the ZIG_VERSION pinned in
.tool-versions. The test job refuses to run unless its network namespace
starts with an empty ruleset. Merges require a green pipeline.
Protocol
Each message is a 4-byte big-endian length followed by that many bytes:
Request: [4 bytes: length][N bytes: the request]
Response: [4 bytes: length][N bytes: the reply]
Erlang's {:packet, 4} adds and strips the framing. A request is nftables
JSON or a sysctl request; the reply is libnftables' output, "", its error
text, or a sysctl JSON reply (spec 02,
spec 03).
License
MIT License - see LICENSE for details.
Related Projects
Documentation
Full documentation is available at HexDocs.