NFTables.Builder (NFTables v0.8.2)
View SourceInternal builder implementation for nftables configurations.
Note
This module is an internal implementation detail. Most users should use the
NFTables module API instead, which provides the same functionality with
a cleaner interface.
Use NFTables.add/2, NFTables.submit/2, etc. instead of calling Builder directly.
For Library Users
Use the NFTables module for all nftables operations:
import NFTables.Expr
{:ok, pid} = NFTables.Port.start_link()
NFTables.add(table: "filter")
|> NFTables.add(chain: "INPUT", type: :filter, hook: :input)
|> NFTables.add(rule: tcp() |> dport(22) |> accept())
|> NFTables.submit(pid: pid)For Advanced Users
This module can be used directly for:
- Creating custom abstractions or libraries
- Implementing custom requestor behaviours
- Advanced builder manipulation
Design Philosophy
- Pure Building: Builder is immutable, no side effects during construction
- Explicit Submission: Commands submit only when
submit/1orsubmit/2is called - Atom Keys: All JSON uses atom keys (converted to strings during encoding)
- Context Tracking: Automatically tracks table/chain/collection context for chaining
- Unified API: Single set of functions (add/delete/flush/etc) for all object types
Internal Usage Example
For advanced users who need direct Builder access:
alias NFTables.Builder
import NFTables.Expr
# Create builder (automatically uses NFTables.Local as default requestor)
builder = Builder.new() # family: :inet is default if no options are specified
# Use apply_with_opts for operations
builder = builder
|> Builder.apply_with_opts(:add, table: "filter")
|> Builder.apply_with_opts(:add, chain: "input", type: :filter, hook: :input, priority: 0, policy: :drop)
|> Builder.apply_with_opts(:add, rule: state([:established, :related]) |> accept())
# Submit when ready (uses NFTables.Local by default)
{:ok, pid} = NFTables.Port.start_link()
NFTables.submit(builder, pid: pid)Option Specificity
Internally, options are given a priority to determine the main object being operated on:
NFTables.add(table: "filter") # creates a new table
NFTables.add( # creates a new chain "INPUT" in the existing table "filter"
table: "filter",
chain: "INPUT"
)
NFTables.add( # appends a new rule to existing chain "INPUT" in table "filter"
table: "filter",
chain: "INPUT",
rule: tcp() |> dport(22) |> accept()
)If a table does not exist, it must be created before adding a chain, and the chain must exist before adding rules.
The builder struct tracks the most recently used table and chain, enabling context reuse:
NFTables.add(table: "filter")
|> NFTables.add(chain: "INPUT", type: :filter, hook: :input)
|> NFTables.add(rules: [
tcp() |> dport(22) |> accept(),
udp() |> dport(53) |> accept()
])Options specified in operations must be non-conflicting. Only one of {:rule, :rules}, only one of
{:set, :map, :counter, :quota, :limit, :flowtable} can be specified. Unknown or unused options are ignored.
Composition
NFTables and Expr compose well, enabling custom functions for common patterns:
def ssh(expr \ Expr.expr()), do: expr |> tcp() |> dport(22)
def dns(expr \ Expr.expr()), do: expr |> udp() |> dport(53)
NFTables.add(table: "filter")
|> NFTables.add(chain: "INPUT", type: :filter, hook: :input)
|> NFTables.add(rules: [ssh(), dns()])Libraries of custom patterns can be built this way.
Setting Builder Context
For advanced builder manipulation, use set/2 to update context fields:
# Set context fields directly (advanced usage)
builder = Builder.new()
|> Builder.set(family: :inet, table: "filter", chain: "INPUT")
# Switch context mid-pipeline
builder
|> Builder.set(table: "filter", chain: "INPUT")
|> Builder.apply_with_opts(:add, rule: allow_ssh)
|> Builder.set(chain: "FORWARD") # Switch to different chain
|> Builder.apply_with_opts(:add, rule: allow_forwarding)
# Clear context
builder |> Builder.set(chain: nil, collection: nil)Unified API Pattern
All object types use the same operations via NFTables module:
NFTables.add(table: "filter", family: :inet)
|> NFTables.add(chain: "input", type: :filter, # Adds chain
hook: :input, priority: 0, policy: :drop)
|> NFTables.add(set: "blocklist", type: :ipv4_addr) # Adds set
|> NFTables.add(rule: [%{accept: nil}]) # Adds rule
|> NFTables.submit(pid: pid)Context Chaining
The builder automatically tracks context (table, chain) eliminating repetition:
NFTables.add(table: "filter", chain: "input") # Sets context
|> NFTables.add(rule: [%{accept: nil}]) # Uses filter/input
|> NFTables.add(rule: [%{drop: nil}]) # Still uses filter/input
Summary
Functions
Apply a command operation using options.
Build a complete command from options using the unified pipeline.
Extract context objects from opts.
Find the object with highest priority from opts.
Find all objects at a given priority level. Used for error messages when multiple objects have the same priority.
Flush the entire ruleset (remove all tables, chains, and rules).
Import an entire ruleset from Query results.
Import a chain from Query results into the builder.
Import a rule from Query results into the builder.
Import a set from Query results into the builder.
Import a table from Query results into the builder.
Create a new builder.
Get the object priority map.
Set multiple builder fields at once using a keyword list.
Set the address family.
Set the requestor module for this builder.
Build base specification for an object.
Submit the builder configuration using the configured requestor.
Submit the builder configuration with options or override requestor.
Convert builder to JSON string for inspection.
Convert builder to Elixir map structure.
Map object type to nftables JSON object key.
Update builder context from extracted context objects.
Update builder with main object context for chaining.
Update spec with optional fields based on object type and command operation.
Validate that a command operation is valid for an object type.
Types
@type family() :: :inet | :ip | :ip6 | :arp | :bridge | :netdev
Functions
Apply a command operation using options.
Automatically detects the object type using priority map and dispatches to the unified build_command pipeline.
Examples
# Add a table
builder |> add(table: "filter")
# Add a chain with context
builder |> add(table: "filter", chain: "input", type: :filter)
# Add a rule using builder context
builder |> add(rule: [%{accept: nil}])
# Delete a rule
builder |> delete(table: "filter", chain: "input", rule: [...], handle: 123)
Build a complete command from options using the unified pipeline.
This is the main entry point that orchestrates the entire command building process:
- Extract context objects (lower priority than main object)
- Update builder with context for chaining
- Build base spec using main object + context
- Update spec with optional fields
- Wrap in command structure
- Update builder with main object for next operation
- Add command to builder
Examples
# Build a chain command
build_command(builder, :add, :chain, table: "filter", chain: "input", type: :filter)
#=> Updated builder with chain command added
# Build a rule command (uses builder context)
build_command(builder, :add, :rule, expr: [...])
#=> Uses builder.table and builder.chain from context
Extract context objects from opts.
Returns a map of context objects that have lower priority than the main object. These will be used to update the builder state for chaining.
Examples
iex> extract_context([table: "filter", chain: "input"], :chain)
%{table: "filter"} # table has lower priority than chain
iex> extract_context([table: "filter", chain: "input", rule: [...]], :rule)
%{table: "filter", chain: "input"} # both have lower priority than rule
Find the object with highest priority from opts.
Returns the object type and its value. Higher priority number indicates the main object being operated on. Lower priorities are context specifiers.
Examples
iex> find_highest_priority([table: "filter", chain: "input"])
{:chain, "input"} # chain (priority 1) > table (priority 0)
iex> find_highest_priority([table: "filter", set: "blocklist"])
{:set, "blocklist"} # set (priority 3) > table (priority 0)
iex> find_highest_priority([map: "m", set: "s"])
** (ArgumentError) Ambiguous object: both :map and :set have priority 3
Find all objects at a given priority level. Used for error messages when multiple objects have the same priority.
Flush the entire ruleset (remove all tables, chains, and rules).
Options
:family- Optional family to flush (default: all families)
Examples
# Flush all tables/chains/rules for all families
builder |> Builder.flush_ruleset()
# Flush only inet family
builder |> Builder.flush_ruleset(family: :inet)
Import an entire ruleset from Query results.
Queries the current ruleset and converts all tables, chains, rules, and sets into Builder commands. This allows you to:
- Query existing firewall configuration
- Modify it programmatically
- Reapply the modified configuration
Parameters
pid- NFTables.Port process pidopts- Options::family- Protocol family to import (default::inet):exclude_handles- Exclude handle fields from import (default:true)
Examples
# Import existing ruleset
{:ok, builder} = Builder.from_ruleset(pid, family: :inet)
# Modify and reapply
builder
|> NFTables.add(
table: "filter",
chain: "INPUT",
rule: [
%{match: %{left: %{payload: %{protocol: "ip", field: "saddr"}}, right: "10.0.0.0/8", op: "=="}},
%{drop: nil}
]
)
|> NFTables.submit(pid: pid)
# Or start fresh and import specific elements
{:ok, tables} = Query.list_tables(pid)
{:ok, chains} = Query.list_chains(pid)
builder = Builder.new()
builder = Enum.reduce(tables, builder, &Builder.import_table(&2, &1))
builder = Enum.reduce(chains, builder, &Builder.import_chain(&2, &1))
Import a chain from Query results into the builder.
Converts a chain map from Query.list_chains/2 into an add_chain command.
Parameters
builder- The builder instancechain_map- Chain map from Query.list_chains/2
Examples
{:ok, chains} = Query.list_chains(pid)
builder = Enum.reduce(chains, Builder.new(), fn chain, b ->
Builder.import_chain(b, chain)
end)
Import a rule from Query results into the builder.
Converts a rule map from Query.list_rules/4 into an add_rule command.
The expr field from the query result is used directly as it matches
the Builder's expression format.
Parameters
builder- The builder instancerule_map- Rule map from Query.list_rules/4 with keys::family,:table,:chain,:expr
Examples
{:ok, rules} = Query.list_rules(pid, "filter", "INPUT")
builder = Enum.reduce(rules, Builder.new(), fn rule, b ->
Builder.import_rule(b, rule)
end)
Import a set from Query results into the builder.
Converts a set map from Query.list_sets/3 into an add_set command.
Parameters
builder- The builder instanceset_map- Set map from Query.list_sets/3
Examples
{:ok, sets} = Query.list_sets(pid, family: :inet)
builder = Enum.reduce(sets, Builder.new(), fn set, b ->
Builder.import_set(b, set)
end)
Import a table from Query results into the builder.
Converts a table map from Query.list_tables/2 into an add_table command.
Parameters
builder- The builder instancetable_map- Table map from Query.list_tables/2 with keys::name,:family
Examples
{:ok, tables} = Query.list_tables(pid)
builder = Enum.reduce(tables, Builder.new(), fn table, b ->
Builder.import_table(b, table)
end)
Create a new builder.
Options
:family- Address family (default::inet):requestor- Module implementing NFTables.Requestor behaviour (default:NFTables.Local)
Examples
Builder.new() # Uses NFTables.Local by default
Builder.new(family: :ip6)
Builder.new(family: :inet, requestor: MyApp.RemoteRequestor)
Get the object priority map.
Set multiple builder fields at once using a keyword list.
This function provides a convenient way to update multiple builder struct fields in a single call. It validates each field and value before updating.
Supported Fields
:family- Address family (:inet, :ip, :ip6, :arp, :bridge, :netdev):requestor- Requestor module (atom or nil):table- Table name (string or nil):chain- Chain name (string or nil):collection- Set/map name (string or nil):type- Type for sets/maps (atom, tuple, or nil)
Examples
# Set single field
builder |> Builder.set(family: :ip6)
# Set multiple fields at once
builder |> Builder.set(family: :inet, table: "filter", chain: "INPUT")
# Chain with other operations
Builder.new()
|> Builder.set(table: "nat", chain: "PREROUTING")
|> NFTables.add(rule: expr)
|> NFTables.submit(pid: pid)
# Clear context
builder |> Builder.set(chain: nil, collection: nil)
# Switch context mid-pipeline
builder
|> Builder.set(table: "filter", chain: "INPUT")
|> NFTables.add(rule: allow_ssh)
|> Builder.set(chain: "FORWARD")
|> NFTables.add(rule: allow_forwarding)Raises
ArgumentError- If field name is invalid or value doesn't match expected type
Set the address family.
Examples
builder |> Builder.set_family(:ip6)
Set the requestor module for this builder.
The requestor module must implement the NFTables.Requestor behaviour.
This allows custom submission handlers for use cases like remote execution,
audit logging, testing, or conditional execution.
Parameters
builder- The builder instancerequestor- Module implementing NFTables.Requestor behaviour (ornilto clear)
Examples
builder |> Builder.set_requestor(MyApp.RemoteRequestor)
# Clear requestor
builder |> Builder.set_requestor(nil)
# Chain with other builder operations
Builder.new()
|> NFTables.add(table: "filter")
|> Builder.set_requestor(MyApp.AuditRequestor)
|> NFTables.add(chain: "INPUT")
|> NFTables.submit(audit_id: "12345")See Also
NFTables.Requestor- Behaviour definition and examplessubmit/1- Submit with builder's requestorsubmit/2- Submit with options/override requestor
Build base specification for an object.
Uses priority-based approach: lower-priority objects provide context. Builder state is used as fallback when opts don't specify context.
Examples
# Table (priority 0) - only needs family
spec(builder, :table, table: "filter")
#=> %{builder | spec: %{family: :inet, name: "filter"}}
# Chain (priority 1) - needs table context
spec(builder, :chain, table: "filter", chain: "input")
#=> %{builder | spec: %{family: :inet, table: "filter", name: "input"}}
# Rule (priority 2) - needs table and chain context
spec(builder, :add, :rule, expr: [...]) # Uses builder.table and builder.chain
#=> %{builder | spec: %{family: :inet, table: "filter", chain: "input", expr: [...]}}
Submit the builder configuration using the configured requestor.
Uses the requestor module specified in the builder's requestor field
(defaults to NFTables.Local for local execution).
The requestor must implement the NFTables.Requestor behaviour.
This function is useful when you want to use custom submission handlers for scenarios like remote execution, audit logging, testing, or conditional execution strategies.
Parameters
builder- The builder with accumulated commands and configured requestor
Returns
:ok- Successful submission{:ok, result}- Successful submission with result{:error, reason}- Failed submission
Examples
# Use default local execution (NFTables.Local)
{:ok, pid} = NFTables.start_link()
builder = Builder.new()
|> NFTables.add(table: "filter")
|> NFTables.add(chain: "INPUT")
|> NFTables.submit(pid: pid) # Uses NFTables.Local
# Configure custom requestor when creating builder
builder = Builder.new(family: :inet, requestor: MyApp.RemoteRequestor)
|> NFTables.add(table: "filter")
|> NFTables.submit(node: :remote_host) # Uses MyApp.RemoteRequestor
# Or set requestor later
builder = Builder.new()
|> NFTables.add(table: "filter")
|> Builder.set_requestor(MyApp.AuditRequestor)
|> NFTables.submit(audit_id: "12345")See Also
NFTables.Requestor- Behaviour definition and examplesNFTables.Local- Default local execution requestorsubmit/2- Submit with options or override requestorset_requestor/2- Set requestor module
Submit the builder configuration with options or override requestor.
This function allows you to:
- Pass options to the requestor's submit callback
- Override the builder's requestor for this submission only
Parameters
builder- The builder with accumulated commandsopts- Keyword list options::requestor- Override the builder's requestor module (optional)- Other options are passed to the requestor's submit callback
Returns
:ok- Successful submission{:ok, result}- Successful submission with result{:error, reason}- Failed submission
Raises
ArgumentError- If no requestor is available (neither in builder nor opts)UndefinedFunctionError- If requestor doesn't implement submit/2
Examples
# Pass options to requestor
builder
|> NFTables.submit(node: :firewall@server, timeout: 10_000)
# Override requestor for this submission only
builder = Builder.new(requestor: MyApp.DefaultRequestor)
|> NFTables.add(table: "filter")
|> NFTables.submit(requestor: MyApp.SpecialRequestor, priority: :high)
# Use without pre-configured requestor
builder = Builder.new()
|> NFTables.add(table: "filter")
|> NFTables.submit(requestor: MyApp.RemoteRequestor, node: :remote_host)See Also
NFTables.Requestor- Behaviour definitionsubmit/1- Submit using builder's requestorset_requestor/2- Set requestor on builder
Convert builder to JSON string for inspection.
Examples
builder |> Builder.to_json()
#=> "{"nftables":[{"add":{"table":{...}}}]}"
Convert builder to Elixir map structure.
Returns the raw Elixir data structure that will be sent to nftables. No JSON encoding happens here - this is pure Elixir data.
Examples
builder |> Builder.to_map()
#=> %{nftables: [%{add: %{table: %{family: "inet", name: "filter"}}}]}For backwards compatibility, to_json/1 is an alias that returns JSON.
Map object type to nftables JSON object key.
Converts our internal object type names to the keys used in nftables JSON.
Update builder context from extracted context objects.
Updates builder.table and builder.chain fields based on context.
Update builder with main object context for chaining.
When the main object is :table or :chain, update the builder state so subsequent operations can use this context.
Examples
# After adding a table, builder.table is updated
update_main_object_context(builder, :table, table: "filter")
#=> %{builder | table: "filter"}
# After adding a chain, builder.chain is updated
update_main_object_context(builder, :chain, chain: "input")
#=> %{builder | chain: "input"}
# Other objects don't update builder context
update_main_object_context(builder, :rule, rule: [...])
#=> builder # unchanged
Update spec with optional fields based on object type and command operation.
Consolidates all *_update_opts functions into a single dispatch function.
Examples
# Chain with base chain options
update_spec(:chain, :add, spec, type: :filter, hook: :input, priority: 0)
# Rule with insert options
update_spec(:rule, :insert, spec, index: 0, comment: "Allow SSH")
# Set with flags
update_spec(:set, :add, spec, flags: [:interval], timeout: 3600)
Validate that a command operation is valid for an object type.
Raises ArgumentError if the combination is invalid.