Packages
NFTables.Port is an Elixir module that bridges Elixir and the Linux kernel's nftables firewall via an Elixir port.
Current section
Files
Jump to
Current section
Files
nftables_port
README.md
README.md
# NFTables.Port
NFTables.Port is the low-level communication layer that bridges Elixir and the Linux kernel's nftables firewall.
Port component for [NFTables](https://github.com/dcoai/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_ADMIN` capability
- **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
```mermaid
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`:
```elixir
def deps do
[
{:nftables_port, "~> 0.5"}
]
end
```
## Requirements
- **Linux kernel** with nftables support (kernel 3.13+)
- **libnftables** library installed (`nftables` package 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:
```bash
# 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
```elixir
# 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`:
```elixir
%{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](https://github.com/dcoai/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:
1. `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.
2. `/usr/local/sbin/port_nftables` (system-wide installation)
3. `/usr/sbin/port_nftables` (system-wide installation)
4. `port_nftables` in the application's priv directory
For production deployments, either:
- Set `PORT_NFTABLES_PATH` to specify a custom location
- Install to `/usr/local/sbin/port_nftables`
## Building from Source
The Zig port executable is built automatically during `mix compile`:
```bash
mix deps.get
mix compile
```
The build process:
1. Compiles the Zig source code in `native/src/`
2. Installs the executable in the build: `_build/<env>/lib/nftables_port/priv/port_nftables`,
rebuilt whenever a file under `native/`, 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](dev_docs/capabilities.md)).
3. Checks the capability with `getcap` and warns if it is missing (it never
sets it)
## Installing to System Location
See [dev_docs/security.md](dev_docs/security.md) for how to keep your system secure.
For production deployments, install the port executable to a system location:
```bash
# 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_ADMIN` capability 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 `setcap` is
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:
```bash
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
```bash
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:
```bash
sudo unshare --net -- env NFTABLES_PORT_NETNS=1 mix test
```
### Continuous integration
Every branch push runs `.gitlab-ci.yml`
([spec 08](spec/08-testing-ci.md), 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/02-wire-protocol.md),
[spec 03](spec/03-responses.md)).
## License
MIT License - see [LICENSE](LICENSE) for details.
## Related Projects
- [NFTables](https://github.com/dcoai/nftables) - High-level Elixir API for nftables
- [nftables](https://netfilter.org/projects/nftables/) - Linux kernel firewall
## Documentation
Full documentation is available at [HexDocs](https://hexdocs.pm/nftables_port).