NFTables.Builder (NFTables v0.8.2)

View Source

Internal 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/1 or submit/2 is 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

family()

@type family() :: :inet | :ip | :ip6 | :arp | :bridge | :netdev

t()

@type t() :: %NFTables.Builder{
  chain: String.t() | nil,
  collection: String.t() | nil,
  commands: [map()],
  family: family(),
  requestor: module() | nil,
  spec: map(),
  table: String.t() | nil,
  type: atom() | {atom(), atom()} | nil
}

Functions

apply_with_opts(builder, cmd_op, opts)

@spec apply_with_opts(t(), atom(), keyword()) :: t()

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_command(builder, cmd_op, object_type, opts)

@spec build_command(t(), atom(), atom(), keyword()) :: t()

Build a complete command from options using the unified pipeline.

This is the main entry point that orchestrates the entire command building process:

  1. Extract context objects (lower priority than main object)
  2. Update builder with context for chaining
  3. Build base spec using main object + context
  4. Update spec with optional fields
  5. Wrap in command structure
  6. Update builder with main object for next operation
  7. 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(opts, main_object_type)

@spec extract_context(
  keyword(),
  atom()
) :: map()

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_highest_priority(opts)

@spec find_highest_priority(keyword()) :: {atom(), any()}

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_highest_priority(opts, obj_priority_map)

@spec find_highest_priority(
  keyword(),
  map()
) :: {atom(), any()}

find_priority_group(priority, obj_priority_map)

@spec find_priority_group(integer(), map()) :: [atom()]

Find all objects at a given priority level. Used for error messages when multiple objects have the same priority.

flush_ruleset(builder, opts \\ [])

@spec flush_ruleset(
  t(),
  keyword()
) :: t()

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)

from_ruleset(pid, opts \\ [])

@spec from_ruleset(
  pid(),
  keyword()
) :: {:ok, t()} | {:error, term()}

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:

  1. Query existing firewall configuration
  2. Modify it programmatically
  3. Reapply the modified configuration

Parameters

  • pid - NFTables.Port process pid
  • opts - 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_chain(builder, chain_map)

@spec import_chain(t(), map()) :: t()

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 instance
  • chain_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_rule(builder, map)

@spec import_rule(t(), map()) :: t()

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 instance
  • rule_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_set(builder, set_map)

@spec import_set(t(), map()) :: t()

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 instance
  • set_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_table(builder, map)

@spec import_table(t(), map()) :: t()

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 instance
  • table_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)

new(opts \\ [])

@spec new(keyword()) :: t()

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)

object_priority_map()

Get the object priority map.

set(builder, opts)

@spec set(
  t(),
  keyword()
) :: t()

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_family(builder, family)

@spec set_family(t(), family()) :: t()

Set the address family.

Examples

builder |> Builder.set_family(:ip6)

set_requestor(builder, requestor)

@spec set_requestor(t(), module() | nil) :: t()

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 instance
  • requestor - Module implementing NFTables.Requestor behaviour (or nil to 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

spec(builder, cmd_op, atom, opts)

@spec spec(t(), atom(), atom(), keyword()) :: t()

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(builder)

@spec submit(t()) :: :ok | {:ok, term()} | {:error, term()}

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

submit(builder, opts)

@spec submit(
  t(),
  keyword()
) :: :ok | {:ok, term()} | {:error, term()}

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 commands
  • opts - 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

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

to_json(builder)

@spec to_json(t()) :: String.t()

Convert builder to JSON string for inspection.

Examples

builder |> Builder.to_json()
#=> "{"nftables":[{"add":{"table":{...}}}]}"

to_map(builder)

@spec to_map(t()) :: map()

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.

type_to_obj(type)

@spec type_to_obj(atom()) :: atom()

Map object type to nftables JSON object key.

Converts our internal object type names to the keys used in nftables JSON.

update_builder_context(builder, context)

@spec update_builder_context(t(), map()) :: t()

Update builder context from extracted context objects.

Updates builder.table and builder.chain fields based on context.

update_main_object_context(builder, arg2, opts)

@spec update_main_object_context(t(), atom(), keyword()) :: t()

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(arg1, arg2, spec, opts)

@spec update_spec(atom(), atom(), map(), keyword()) :: map()

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_builder_opt(builder, opts, key)

validate_command_object(cmd_op, object_type)

@spec validate_command_object(atom(), atom()) :: :ok

Validate that a command operation is valid for an object type.

Raises ArgumentError if the combination is invalid.

validate_opts(builder, opts, expect_list)

validate_required_opt(opts, key)