Packages
Elixir library for working with Linux nftables rules. Provides high-level APIs for building tables, chains, rules, maps and sets. Works with NFTables.Port for communicating with the kernel firewall.
Current section
Files
Jump to
Current section
Files
usage-rules.md
# NFTables Usage Rules
## Overview
NFTables provides a pure functional API for building nftables firewall rules via Elixir.
---
## Core Principles
### 1. Pure Functional Expression API
Build expressions as **pure data** with **no side effects**.
```elixir
use NFTables
# Build expression (no side effects)
rule = tcp() |> dport(22) |> accept()
# Execute separately
{:ok, pid} = NFTables.Port.start_link()
NFTables.add(table: "filter", family: :inet)
|> NFTables.add(chain: "INPUT", type: :filter, hook: :input)
|> NFTables.add(rule: rule)
|> NFTables.submit(pid: pid)
```
### 2. Build/Submit Separation
- **NFTables.Expr and its submodules** - Build pure expression data (no execution)
- **NFTables** - Constructs nftables configurations (no execution)
- **NFTables.submit** - Sends configurations to kernel (execution only)
### 3. Dual-Arity Functions
```elixir
tcp() |> dport(22) |> accept() # Start new
expr() |> tcp() |> dport(22) |> accept() # Continue existing
```
### 4. Context Tracking
NFTables automatically tracks table/chain context:
```elixir
NFTables.add(table: "filter", chain: "INPUT")
|> NFTables.add(rule: tcp() |> dport(22) |> accept())
|> NFTables.add(rule: tcp() |> dport(80) |> accept())
```
---
## Key Modules
**NFTables** - Main API: `add/1-2`, `delete/1-2`, `insert/1-2`, `replace/1-2`,
`rename/1-2`, `flush/1-2`, `submit/1-2`, `to_json/1`, `to_map/1`
**NFTables.Expr** - The struct and the pipeline: `expr/0-1`, `to_list/1`,
`add_expr/2`, `comment/2`, `set_protocol/2`. It does **not** export the
matchers — those live in submodules, and `use NFTables` imports them:
| Module | Exports |
|--------|---------|
| `NFTables.Expr.TCP` | `tcp/0-1`, `tcp_flags/2-3` |
| `NFTables.Expr.UDP` | `udp/0-1` |
| `NFTables.Expr.Port` | `dport/2`, `sport/2`, `port/2` |
| `NFTables.Expr.IP` | `source/1-2`, `dest/1-2`, `ttl/2-3` |
| `NFTables.Expr.CT` | `state/1-2`, `ct_state/1-2`, `connmark/1-2` |
| `NFTables.Expr.Verdict` | `accept/0-1`, `drop/0-1`, `reject/0-2`, `jump/1-2`, `return_from_chain/0-1` |
| `NFTables.Expr.Actions` | `counter/0-1`, `log/1-3`, `limit/2-4`, `rate_limit/2-4`, `set_mark/1-2` |
| `NFTables.Expr.Sets` | `set/2-3` |
**NFTables.Port** - Process management: `start_link/0-1`, `stop/1`, `commit/2-3`
**NFTables.Policy** - Pre-built policies, each taking and returning a
builder: `accept_loopback/0-2`, `accept_established/0-2`, `allow_ssh/0-2`,
`allow_http/0-2`, `stateful/0-2`, `deny_all/0-2`. Note
`setup_basic_firewall/1-2` is the exception — it takes a **pid** and
submits.
---
## Expression Building
### Protocol and Port Matching
```elixir
use NFTables
tcp() |> dport(80) |> accept() # Single port
udp() |> dport(53) |> accept() # UDP
tcp() |> dport(8000..8999) |> accept() # Port range
```
`dport/2` takes an integer or a range — nothing else. For a set of
unrelated ports, use a named set and match against it, which is also
what you want operationally since the set can be updated without
rebuilding the rule:
```elixir
NFTables.add(table: "filter", family: :inet)
|> NFTables.add(set: "web_ports", type: :inet_service)
|> NFTables.add(element: [80, 443, 8080], set: "web_ports")
|> NFTables.add(chain: "INPUT", type: :filter, hook: :input)
|> NFTables.add(rule: tcp() |> set("web_ports", :dport) |> accept())
```
### State Tracking
```elixir
state([:established, :related]) |> accept() # Accept established
ct_state([:invalid]) |> drop() # Drop invalid
ct_state([:new]) |> log() |> accept() # Log new
```
### Actions and Verdicts
```elixir
# Terminal verdicts (end processing)
accept() # Accept packet
drop() # Silently drop
reject() # Reject with ICMP
# Non-terminal actions (continue processing)
counter() |> log("SSH: ") |> accept() # Chain actions
limit(10, :minute) |> accept() # Rate limiting
```
`log/1-3` takes the prefix as its first argument. Passing none now
raises `FunctionClauseError` rather than binding the expression struct
as the prefix and building a rule that logs nonsense.
A `limit` matches while the packet is **within** budget, so
`limit(10, :minute) |> accept()` admits the allowance and leaves the
excess to the chain policy. `limit |> drop()` is the inverse of what it
looks like: it drops the first ten a minute and passes the rest.
To match the excess instead, pass `over: true`:
```elixir
limit(10, :minute, over: true) |> drop() # drops what exceeds the rate
```
---
## Common Patterns
### Basic Firewall
```elixir
use NFTables
{:ok, pid} = NFTables.Port.start_link()
NFTables.add(table: "filter", family: :inet)
|> NFTables.add(chain: "INPUT", type: :filter, hook: :input, policy: :drop)
|> NFTables.add(rule: state([:established, :related]) |> accept())
|> NFTables.add(rule: tcp() |> dport(22) |> accept())
|> NFTables.submit(pid: pid)
```
### SSH with Rate Limiting
```elixir
rule = tcp() |> dport(22) |> limit(10, :minute) |> accept()
NFTables.add(table: "filter", chain: "INPUT")
|> NFTables.add(rule: rule)
|> NFTables.submit(pid: pid)
```
### Multiple Rules in One Submission
Add each rule with its own `add(rule:)`. The context carries, so the
table and chain are named once, and the whole list is submitted as a
single atomic batch:
```elixir
NFTables.add(table: "filter", chain: "INPUT")
|> NFTables.add(rule: tcp() |> dport(22) |> accept())
|> NFTables.add(rule: tcp() |> dport(80) |> accept())
|> NFTables.add(rule: tcp() |> dport(443) |> accept())
|> NFTables.submit(pid: pid)
```
Do **not** use `add(rules: [...])`. It emits one rule whose expression
list contains the others nested inside it — a malformed ruleset rather
than three rules. Tracked in #28.
### Using Policy Module
```elixir
alias NFTables.Policy
NFTables.add(table: "filter")
|> NFTables.add(chain: "INPUT", type: :filter, hook: :input, policy: :drop)
|> Policy.accept_loopback()
|> Policy.accept_established()
|> Policy.allow_ssh(rate_limit: 10)
|> NFTables.submit(pid: pid)
```
---
## Best Practices
### 1. Use `use NFTables`
```elixir
use NFTables # imports NFTables.Expr and every Expr.* matcher module
rule = tcp() |> dport(22) |> accept()
```
`import NFTables.Expr` alone is not enough: it brings in `expr/1`,
`to_list/1` and `add_expr/2`, but every matcher — `tcp/0`, `dport/2`,
`accept/0` — lives in a submodule. Importing only the parent gives
`CompileError: undefined function tcp/0`. Import the submodules you need
explicitly if you prefer a narrow scope:
```elixir
use NFTables
```
### 2. Use Context Tracking
```elixir
# Good - context tracked automatically
NFTables.add(table: "filter", chain: "INPUT")
|> NFTables.add(rule: rule1)
|> NFTables.add(rule: rule2)
```
### 3. Build Expressions as Data
```elixir
ssh_rule = tcp() |> dport(22) |> accept()
http_rule = tcp() |> dport(80) |> accept()
NFTables.add(table: "filter", chain: "INPUT")
|> NFTables.add(rule: ssh_rule)
|> NFTables.add(rule: http_rule)
|> NFTables.submit(pid: pid)
```
### 4. Test Without Side Effects
```elixir
rule = tcp() |> dport(22) |> accept()
assert %NFTables.Expr{} = rule # Test without kernel access
NFTables.add(table: "filter", chain: "INPUT")
|> NFTables.add(rule: rule)
|> NFTables.submit(pid: pid)
```
### 5. Clean Up in Tests
```elixir
use ExUnit.Case
use NFTables
setup do
{:ok, pid} = NFTables.Port.start_link()
on_exit(fn ->
NFTables.delete(table: "test") |> NFTables.submit(pid: pid)
NFTables.Port.stop(pid)
end)
{:ok, pid: pid}
end
```
---
## Anti-Patterns
### ❌ Don't Use Builder Directly
```elixir
alias NFTables.Builder
Builder.new() |> Builder.apply_with_opts(:add, table: "filter") # Bad
NFTables.add(table: "filter") # Good
```
### ❌ Don't Repeat Context
```elixir
# Bad - context repeated on every call
NFTables.add(table: "filter", chain: "INPUT", rule: rule1)
NFTables.add(table: "filter", chain: "INPUT", rule: rule2)
# Good - context named once and carried
NFTables.add(table: "filter", chain: "INPUT")
|> NFTables.add(rule: rule1)
|> NFTables.add(rule: rule2)
```
### ❌ Don't Mix Side Effects with Building
```elixir
def build_rule(pid), do: NFTables.add(table: "f") |> NFTables.submit(pid: pid) # Bad
def build_rule, do: tcp() |> dport(22) |> accept() # Good
```
---
## See Also
- **README.md** - Main documentation
- **Module docs** - `h NFTables`, `h NFTables.Expr`, `h NFTables.Policy`
- **dev_docs/** - Architecture, quick reference, advanced features
- **examples/** - Working code examples