NFTables (NFTables v0.8.2)

View Source

Elixir interface to Linux nftables via libnftables JSON API.

NFTables provides a high-level, idiomatic Elixir API for managing nftables rules, using the official libnftables library with JSON for all communication.

Quick Start

Start the NFTables port process and build firewall rules:

# Start the port process
{:ok, pid} = NFTables.Port.start_link()

# Create a simple firewall
import NFTables.Expr

NFTables.add(table: "filter", family: :inet)
|> NFTables.add(chain: "INPUT", type: :filter, hook: :input, priority: 0, policy: :drop)
|> NFTables.add(rule: state([:established, :related]) |> accept())
|> NFTables.add(rule: tcp() |> dport(22) |> accept())
|> NFTables.submit(pid: pid)

# Clean up when done
NFTables.Port.stop(pid)

Import Options

You have two ways to import expression building functions:

Option 1: Use Macro (Import Everything)

The simplest approach - automatically imports all expression modules:

use NFTables

This imports NFTables.Expr and all sub-modules, giving you access to all expression building functions. Best for interactive use or when you need many different types of expressions.

Option 2: Selective Imports (Import What You Need)

For production code, you may prefer explicit imports:

import NFTables.Expr
import NFTables.Expr.{Port, TCP, Verdict}

This gives you fine-grained control and makes dependencies explicit. Best for production code where you want to minimize namespace pollution.

Both approaches are equally valid - choose based on your preferences and use case.

Main API Functions

Building Rules

  • add/1-2 - Add tables, chains, rules, sets, etc.
  • delete/1-2 - Delete objects
  • flush/1-2 - Flush objects (remove contents)
  • flush_ruleset/0-2 - Flush entire ruleset
  • insert/1-2 - Insert rules at specific positions
  • replace/1-2 - Replace rules at specific handles
  • rename/1-2 - Rename chains

Submitting Changes

  • submit/1-2 - Submit configuration to nftables

Helper Functions

Module Organization

Core APIs

Convenience APIs

  • NFTables.Policy - Pre-built security policies (accept_established, allow_ssh, etc.)
  • NFTables.NAT - NAT operations (port forwarding, masquerading, etc.)

Execution & Port Management

Internal APIs

Pipeline Pattern

All functions return a builder struct that can be piped:

NFTables.add(table: "filter")
|> NFTables.add(chain: "INPUT", type: :filter, hook: :input)
|> NFTables.add(rule: tcp() |> dport(80) |> accept())
|> NFTables.add(rule: tcp() |> dport(443) |> accept())
|> NFTables.submit(pid: pid)

Context Tracking

The builder automatically tracks context (table, chain) so you don't need to repeat it:

NFTables.add(table: "filter", chain: "INPUT", type: :filter, hook: :input)
|> NFTables.add(rule: tcp() |> dport(22) |> accept())
|> NFTables.add(rule: tcp() |> dport(80) |> accept())
# Both rules automatically use filter/INPUT

Examples

Basic Firewall

import NFTables.Expr

NFTables.add(table: "filter")
|> NFTables.add(chain: "INPUT", type: :filter, hook: :input, policy: :drop)
|> NFTables.add(chain: "FORWARD", type: :filter, hook: :forward, policy: :drop)
|> NFTables.add(chain: "OUTPUT", type: :filter, hook: :output, policy: :accept)
|> NFTables.add(rule: iif("lo") |> accept())
|> NFTables.add(rule: state([:established, :related]) |> accept())
|> NFTables.add(rule: tcp() |> dport(22) |> accept())
|> NFTables.submit(pid: pid)

IP Blocking with Sets

NFTables.add(table: "filter")
|> NFTables.add(set: "blocklist", type: :ipv4_addr)
|> NFTables.add(element: ["1.2.3.4", "5.6.7.8"], set: "blocklist")
|> NFTables.add(chain: "INPUT", type: :filter, hook: :input)
|> NFTables.add(rule: ip_saddr() |> set_lookup("@blocklist") |> drop())
|> NFTables.submit(pid: pid)

NAT / Port Forwarding

NFTables.add(table: "nat", family: :ip)
|> NFTables.add(chain: "PREROUTING", type: :nat, hook: :prerouting)
|> NFTables.add(rule: tcp() |> dport(8080) |> dnat("192.168.1.100:80"))
|> NFTables.submit(pid: pid)

JSON API

The underlying JSON format follows the official nftables JSON schema. See: https://wiki.nftables.org/wiki-nftables/index.php/JSON_API

For advanced use cases requiring direct builder access, see NFTables.Builder documentation.

Summary

Functions

Convenience macro to import all NFTables.Expr modules.

Contextual add operation (arity-1) - starts new builder.

Contextual add operation (arity-2) - continues existing builder.

delete/1 delete an object, starts a new builder.

delete/2 operation same as delete/1 but continues existing builder.

Contextual flush operation (arity-1) - starts new builder.

Contextual flush operation (arity-2) - continues existing builder.

Flush the entire ruleset (remove all tables, chains, and rules).

Flush the entire ruleset (arity-2) - continues existing builder.

Contextual insert operation (arity-1) - starts new builder.

Contextual insert operation (arity-2) - continues existing builder.

Contextual rename operation (arity-1) - starts new builder.

Contextual rename operation (arity-2) - continues existing builder.

Contextual replace operation (arity-1) - starts new builder.

Contextual replace operation (arity-2) - continues existing builder.

Submit the builder configuration using the configured requestor.

Submit the builder configuration with options or override requestor.

Types

nft_family()

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

t()

@type t() :: NFTables.Builder.t()

Functions

__using__(opts)

(macro)

Convenience macro to import all NFTables.Expr modules.

When you use NFTables, all expression building modules are automatically imported, making all expression functions available without explicit imports.

Example

defmodule MyFirewall do
  use NFTables

  def build_rules(pid) do
    NFTables.add(table: "filter")
    |> NFTables.add(chain: "INPUT", type: :filter, hook: :input)
    |> NFTables.add(rule: tcp() |> dport(22) |> accept())
    |> NFTables.submit(pid: pid)
  end
end

This is equivalent to:

import NFTables.Expr
import NFTables.Expr.{IP, Port, TCP, Layer2, CT, Advanced, Actions, NAT, Verdict, Meter, Protocols}

Alternative: Selective Imports

For production code, you may prefer explicit imports for clarity:

import NFTables.Expr
import NFTables.Expr.{Port, TCP, Verdict}

Both approaches are equally valid.

add(opts)

Contextual add operation (arity-1) - starts new builder.

Detects what to add based on keyword options provided.

Examples

# Add table
NFTables.add(table: "filter", family: :inet)

# Add chain
NFTables.add(chain: "INPUT", type: :filter)

# Add table and chain together
NFTables.add(table: "filter", chain: "INPUT", family: :inet)

# Add multiple rules
import NFTables.Expr
NFTables.add(rules: [
  rule() |> tcp() |> dport(22) |> accept(),
  rule() |> tcp() |> dport(80) |> accept()
])

add(builder, opts)

Contextual add operation (arity-2) - continues existing builder.

Examples

NFTables.add(table: "filter")
|> NFTables.add(chain: "INPUT")
|> NFTables.add(rule: [%{accept: nil}])

delete(opts)

delete/1 delete an object, starts a new builder.

Examples

builder |> delete(table: "filter")
builder |> delete(chain: "input")
builder |> delete(rule: [...], handle: 123)

delete(builder, opts)

delete/2 operation same as delete/1 but continues existing builder.

flush(opts)

Contextual flush operation (arity-1) - starts new builder.

flush(builder, opts)

Contextual flush operation (arity-2) - continues existing builder.

options: :scope - when set to :all will flush everything (limited by :family option if that is specified) :family - limits flush to particular nft family: :inet | :ip | :ip6 | :arp | :bridge | :netdev

flush_ruleset(opts \\ [])

@spec flush_ruleset(keyword()) :: NFTables.Builder.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
NFTables.flush_ruleset()
|> NFTables.submit(pid: pid)

# Flush only inet family
NFTables.flush_ruleset(family: :inet)
|> NFTables.submit(pid: pid)

flush_ruleset(builder, opts)

@spec flush_ruleset(
  NFTables.Builder.t(),
  keyword()
) :: NFTables.Builder.t()

Flush the entire ruleset (arity-2) - continues existing builder.

insert(opts)

Contextual insert operation (arity-1) - starts new builder.

Inserts a rule at a specific position in a chain.

Examples

import NFTables.Expr
NFTables.insert(table: "filter", chain: "INPUT", rule: tcp() |> dport(22) |> accept(), index: 0)

insert(builder, opts)

Contextual insert operation (arity-2) - continues existing builder.

rename(opts)

Contextual rename operation (arity-1) - starts new builder.

Renames a chain.

Examples

NFTables.rename(table: "filter", chain: "input", newname: "INPUT")

rename(builder, opts)

Contextual rename operation (arity-2) - continues existing builder.

replace(opts)

Contextual replace operation (arity-1) - starts new builder.

Replaces a rule at a specific handle.

Examples

import NFTables.Expr
NFTables.replace(table: "filter", chain: "INPUT", rule: tcp() |> dport(80) |> accept(), handle: 123)

replace(builder, opts)

Contextual replace operation (arity-2) - continues existing builder.

set_family(builder, family)

See NFTables.Builder.set_family/2.

submit(builder)

@spec submit(NFTables.Builder.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).

Examples

{:ok, pid} = NFTables.Port.start_link()

NFTables.add(table: "filter")
|> NFTables.add(chain: "INPUT")
|> NFTables.submit(pid: pid)

submit(builder, opts)

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

Submit the builder configuration with options or override requestor.

Examples

NFTables.add(table: "filter")
|> NFTables.submit(pid: pid, timeout: 10_000)

# Override requestor
NFTables.add(table: "filter")
|> NFTables.submit(requestor: MyApp.RemoteRequestor, node: :remote)

to_json(builder)

See NFTables.Builder.to_json/1.

to_map(builder)

See NFTables.Builder.to_map/1.