README.md

# NFTables - Elixir Interface to nftables

Elixir module for Linux nftables. NFTables provides both high-level helper functions for common firewall operations and flexible rule building with composable functions.

## Quickstart Guide

### Installation

Add `nftables` to your dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:nftables, "~> 0.6.4"}
  ]
end
```

Run the commands
```bash
mix deps.get
mix compile
```

### Setting Capabilities

The port executable requires `CAP_NET_ADMIN` to communicate with the kernel firewall:

```bash
# After compilation
sudo setcap cap_net_admin=ep deps/nftables_port/priv/port_nftables
chmod 700 deps/nftables_port/priv/port_nftables
```

### Build a Rule

**Note** - before running examples on a remote machine, *be aware* you are able block your remote access.  You may want to start by experimenting in a VM or local machine.

```elixir
alias NFTables.Builder
import NFTables.Expr

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

def ssh(rule), do: rule |> tcp() |> dport(22)

response =
  Builder.new()
  |> Builder.add(table: "filter", family: :inet)
  |> Builder.add(chain: "INPUT", hook: :input)
  |> Builder.add(rule: expr() |> ssh() |> accept())
  |> Builder.commit(pid: pid)

IO.inspect(response}
```

## Features

- **High-Level APIs** - Simple functions for blocking IPs, managing sets, creating rules
- **Pure Functional Expr API** - composable expression builder with no side effects
- **Sysctl Management** - Read/Write access to network kernel parameters
- **Batch Operations** - Atomic multi-command execution
- **Query Operations** - List tables, chains, rules, sets, and elements
- **Builder Pattern** - Clear separation between building and executing rules
- **Elixir Port-based Architecture** - Fault isolation (crashes don't affect BEAM VM)
- **Security** - Port runs with minimal privileges (CAP_NET_ADMIN only)

### Advanced Features

- **Flowtables** - Hardware-accelerated packet forwarding for established connections
- **Meters/Dynamic Sets** - Per-key rate limiting with composite key support
- **Raw Payload Matching** - Offset-based packet header access for custom protocols
- **Socket Matching & TPROXY** - Transparent proxy support without destination changes
- **SCTP** - Stream Control Transmission Protocol (WebRTC, telephony)
- **DCCP** - Datagram Congestion Control Protocol (streaming, gaming)
- **GRE** - Generic Routing Encapsulation (VPN tunnels)
- **OSF (OS Fingerprinting)** - Passive operating system detection via TCP SYN analysis

See [dev_docs/advanced_features.md](dev_docs/advanced_features.md) for comprehensive documentation of all advanced features.

### NFTables_Port 

The NFTables library depends on [NFTables.Port](https://hex.pm/packages/nftables_port) which is an elixir wrapper, and a program written in Zig which accepts json structures and sends them to Linux nftables using the libnftables (C library).  The Elixir module manages the Zig program as a Port.

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

# Send JSON commands (for structured operations)
json_cmd = ~s({"nftables": [{"list": {"tables": {}}}]})
{:ok, json_response} = NFTables.Port.call(pid, json_cmd)
```

Visit the [NFTables.Port](https://github.com/dcoai/nftables_port) project page for details.  Take some time to review the [Security](https://github.com/dcoai/nftables_port/dev_docs/security.md) document found there.

### NFTables

NFTables.Port takes JSON requests and passes them on to the Linux nftables service.  The Elixir NFTables library is a set of tools to query and build rule sets which can be applied via NFTables.Port.

**Generate JSON using NFTables library**

```elixir
json =
  Builder.new()
  |> Builder.add(table: "filter", family: :inet)
  |> Builder.add(chain: "INPUT", hook: :input, policy: :drop)
  |> Builder.add(rule: tcp() |> dport(22) |> accept())
  |> Builder.to_json()
```

**Putting these together**

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

json_cmd =
  Builder.new()
  |> Builder.add(table: "filter", family: :inet)
  |> Builder.add(chain: "INPUT", hook: :input, policy: :drop)
  |> Builder.add(rule: tcp() |> dport(22) |> accept())
  |> Builder.to_json()

{:ok, json_response} = NFTables.Port.call(pid, json_cmd)
```

Using this we can manage a local firewall from Elixir.

A couple possibilities:
- dynamic firewall which process events and updates firewall based on the events.
- distributed firewall on multiple nodes.

## System Requirements

- Linux kernel >= 3.18 (nf_tables support)
- Zig >= 0.11.0
- Elixir >= 1.14
- Erlang/OTP >= 24

### Required System Libraries

The following development packages must be installed:

- `libnftables-dev` >= 0.9.0 - Netfilter nftables userspace library (includes JSON API)
- `libcap-dev` >= 2.25 - POSIX capabilities library

### Installation on Debian/Ubuntu

```bash
sudo apt-get update
sudo apt-get install -y \
  libnftables-dev \
  libcap-dev \
  zig
```

### Verify Installation

Check that all dependencies are available:

```bash
# Check Zig
zig version

# Check nftables library
pkg-config --modversion libnftables

# Check capability library
ls /usr/include/sys/capability.h
```

## Building

The Zig port is automatically compiled when you build the Mix project:

```bash
# Fetch dependencies
mix deps.get

# Compile (includes Zig compilation)
mix compile
```

The compiled `port_nftables` binary will be placed in `deps/nftables_port/priv/port_nftables`.

### Manual Build

To build just the Zig port:

```bash
cd native
zig build
```

The binary will be in `native/zig-out/bin/port_nftables`.

### Setting Capabilities

The port binary needs CAP_NET_ADMIN capability to manage firewall rules:

```bash
sudo setcap cap_net_admin=ep deps/nftables_port/priv/port_nftables
```

Verify:

```bash
getcap priv/port_nftables
# Should show: priv/port_nftables = cap_net_admin+ep
```

### Security considerations

Once port_nftables has CAP_NET_ADMIN capability set, it can be used to set network related parameters (like enable ip_forwarding) and
configure nftables (create/delete/update tables, chains, rules, etc...).  Considering this it would be wise to protect this executable.

the `nftables_port` executable should fail to run if it has any `rwx` permissions for `other`.  if this is the case you will see a message similar to:

```
    \\
    \\SECURITY ERROR: Executable has world permissions enabled!
    \\
    \\Current permissions: 755
    \\
    \\This executable has CAP_NET_ADMIN capability and MUST NOT be
    \\world-readable, world-writable, or world-executable.
    \\
    \\To fix, run:
    \\  chmod 750 {s}
    \\  # or
    \\  chmod 700 {s}
    \\
    \\The mode must end in 0 (no permissions for "other").
    \\Access should be controlled via user/group ownership.
    \\
    \\Refusing to start for security reasons.
    \\
```

minimally for production, do the following:

1. create a special user that the nftables_port will run as such as `exfw`.  Feel free to be more creative with the name.
2. `chown exfw nftables_port`  # make the executable belong to the new user `exfw`
3. `chmod 700 nftables_port`   # make the executable only runnable by the user `exfw`

## Useage Examples

### Block an IP Address 

```elixir
# Start NFTables
{:ok, pid} = NFTables.start_link()

# Build and execute a rule to block an IP
alias NFTables.{Builder, Expr}
import NFTables.Expr

Builder.new(family: :inet)
|> Builder.add(table: "filter")
|> Builder.add(chain: "INPUT")
|> Builder.add(rule: 
  expr()
  |> source_ip("192.168.1.100")
  |> drop()
)
|> Builder.submit(pid)
```

### Manage IP Blocklists with Sets

```elixir
{:ok, pid} = NFTables.start_link()
alias NFTables.Builder

# Add IPs to an existing blocklist set
malicious_ips = ["192.168.1.100", "10.0.0.99", "172.16.5.50"]

Builder.new()
|> Builder.add(element: malicious_ips, set: "blocklist", table: "filter", family: :inet)
|> Builder.submit(pid)

# Remove a false positive
Builder.new()
|> Builder.delete(element: ["192.168.1.100"], set: "blocklist", table: "filter", family: :inet)
|> Builder.submit(pid)

# List all blocked IPs
{:ok, %{set_elements: elements}} = NFTables.Query.list_set_elements(pid, "filter", "blocklist", family: :inet)
```

### Build Complex Rules (New API)

```elixir
alias NFTables.{Builder, Expr}
import NFTables.Expr

# Build a sophisticated firewall rule with the new fluent API
:ok = Builder.new(family: :inet)
  |> Builder.add(table: "filter")
  |> Builder.add(chain: "INPUT")
  |> Builder.add(rule: 
    expr()
    |> source_ip("10.0.0.0/8")
    |> protocol(:tcp)
    |> dport(22)
    |> ct_state([:new])
    |> limit(10, :minute, burst: 5)
    |> log("SSH_ACCESS: ", level: "info")
    |> counter()
    |> accept()
  )
  |> Builder.submit(pid)

# Or build multiple rules in a batch
:ok = Builder.new(family: :inet)
  |> Builder.add(table: "filter")
  |> Builder.add(chain: "INPUT")
  |> Builder.add(table: "filter")
  |> Builder.add(chain: "INPUT")
  |> Builder.add(rule: 
    expr() |> source_ip("10.0.0.0/8") |> drop()
  )
  |> Builder.add(rule: 
    expr() |> ct_state([:established, :related]) |> accept()
  )
  |> Builder.submit(pid)
```

### Setup Basic Firewall

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

# One command for secure defaults
:ok = NFTables.Policy.setup_basic_firewall(pid,
  allow_services: [:ssh, :http, :https],
  ssh_rate_limit: 10
)

# Creates:
# - DROP policy by default
# - Accept loopback traffic
# - Accept established/related connections
# - Drop invalid packets
# - Allow SSH with rate limiting
# - Allow HTTP and HTTPS
```

## New Builder + Expr API

NFTables now provides a powerful, composable API for building firewall rules:

### Builder Module - Command Construction

The `Builder` module provides a pure functional interface for constructing nftables commands:

```elixir
alias NFTables.Builder

# Build commands without executing
builder = Builder.new(family: :inet)
|> Builder.add(table: "filter")
|> Builder.add(chain: "INPUT", type: :filter, hook: :input, priority: 0, policy: :drop)
|> Builder.add(table: "filter")
|> Builder.add(chain: "INPUT")

# Execute when ready
:ok = Builder.submit(builder, pid)

# Or inspect the JSON that would be sent
json = Builder.to_json(builder)
```

### Expr Module - Composable Expression Building

The `Rule` module provides a fluent API for building rule expressions:

```elixir
alias NFTables.Expr

def established_related(rule), do: ct_state([:established, :related])

def ssh(rule, source), do: source_ip(source) |> tcp() |> dport(22)

# Build rule expressions
ssh_rule = expr()
|> ssh("10.0.0.0/8")
|> extablished_related()
|> limit(10, :minute, burst: 5)    # Rate limiting
|> log("SSH: ", level: "info")     # Logging
|> counter()                       # Add counter
|> accept()                        # Verdict
# No need to call to_list() - Builder handles conversion automatically!

# Use with Builder
Builder.new(family: :inet)
|> Builder.add(table: "filter")
|> Builder.add(chain: "INPUT")
|> Builder.add(rule: ssh_rule)          # Automatically converted to expression list
|> Builder.submit(pid)
```

### Available Expression Matchers

- **IP**: `source/1`, `dest/1` - Supports single IPs and CIDR notation
- **Ports**: `sport/1`, `dport/1`, `port/1` - TCP/UDP ports
- **Protocol**: `protocol/1` - tcp, udp, icmp, etc.
- **State**: `state/1` - Connection tracking states
- **Interface**: `iif/1`, `oif/1` - Input/output interfaces
- **TCP Flags**: `tcp_flags/2` - SYN, ACK, FIN, etc.
- **Many more**: See module documentation

### Available Actions & Verdicts

- **Actions**: `counter/0`, `log/2`, `limit/3`, `set_mark/1`
- **Verdicts**: `accept/0`, `drop/0`, `reject/1`, `jump/1`, `return/0`
- **NAT**: `snat/2`, `dnat/2`, `masquerade/1`

### Advanced Features - Named Objects

The `Builder` module also supports nftables named objects for advanced use cases:

#### Maps (Key-Value Dictionaries)

Maps allow you to create dynamic mappings from keys to values (e.g., port → verdict):

```elixir
# Create a map that maps ports to verdicts
Builder.new()
|> Builder.add(table: "filter")
|> Builder.add(map: "port_verdict", type: {:inet_service, :verdict})
|> Builder.add(element:  [
  {80, "accept"},
  {443, "accept"},
  {8080, "drop"}
])
|> Builder.submit(pid)

# Use the map in a rule
Builder.new()
|> Builder.add(table: "filter")
|> Builder.add(chain: "INPUT")
|> Builder.add(rule: 
  expr()
  |> protocol(:tcp)
  |> dport_map("port_verdict")  # Map lookup
)
|> Builder.submit(pid)
```

#### Named Counters

Named counters can be shared across multiple rules and queried independently:

```elixir
# Create a named counter
Builder.new()
|> Builder.add(table: "filter")
|> Builder.add(counter: "http_traffic")
|> Builder.submit(pid)

# Reference it in rules
Builder.new()
|> Builder.add(table: "filter")
|> Builder.add(chain: "INPUT")
|> Builder.add(rule: 
  expr()
  |> protocol(:tcp)
  |> dport(80)
  |> counter_ref("http_traffic")  # Reference named counter
  |> accept()
)
|> Builder.submit(pid)
```

#### Quotas

Quotas limit the total amount of traffic (in bytes) that can pass through:

```elixir
# Create a 1 GB quota
Builder.new()
|> Builder.add(table: "filter")
|> Builder.add(quota: "monthly_limit", 1_000_000_000)
|> Builder.submit(pid)

# Use in a rule - traffic stops when quota exceeded
Builder.new()
|> Builder.add(table: "filter")
|> Builder.add(chain: "OUTPUT")
|> Builder.add(rule: 
  expr()
  |> quota_ref("monthly_limit")
  |> accept()
)
|> Builder.submit(pid)
```

#### Named Limits

Named limits provide reusable rate limiting across multiple rules:

```elixir
# Create a rate limit object
Builder.new()
|> Builder.add(table: "filter")
|> Builder.add(limit: "ssh_limit", 10, :minute, burst: 5)
|> Builder.submit(pid)

# Use in multiple rules
Builder.new()
|> Builder.add(table: "filter")
|> Builder.add(chain: "INPUT")
|> Builder.add(rule: 
  expr()
  |> protocol(:tcp)
  |> dport(22)
  |> limit_ref("ssh_limit")  # Reference named limit
  |> accept()
)
|> Builder.submit(pid)
```

**Benefits of Named Objects:**
- **Reusability**: Define once, use in multiple rules
- **Dynamic Updates**: Update the object without modifying rules
- **Queryable**: Check counter values, quota usage independently
- **Performance**: More efficient than inline expressions for shared logic

### Query Round-Trip Support

NFTables now supports importing existing firewall configurations back into Builder commands, enabling powerful query-modify-reapply workflows:

#### Import Entire Ruleset

```elixir
# Query and import existing firewall configuration
{:ok, builder} = Builder.from_ruleset(pid, family: :inet)

# Modify and reapply
builder
|> Builder.add(table: "filter")
|> Builder.add(chain: "INPUT")
|> Builder.add(rule: 
  expr()
  |> source_ip("192.168.1.100")
  |> drop()
)
|> Builder.submit(pid)
```

#### Import Specific Elements

```elixir
# Import tables
{:ok, tables} = Query.list_tables(pid, family: :inet)
builder = Enum.reduce(tables, Builder.new(), fn table, b ->
  Builder.import_table(b, table)
end)

# Import chains
{:ok, chains} = Query.list_chains(pid, family: :inet)
builder = Enum.reduce(chains, builder, fn chain, b ->
  Builder.import_chain(b, chain)
end)

# Import rules
{:ok, rules} = Query.list_rules(pid, family: :inet)
builder = Enum.reduce(rules, builder, fn rule, b ->
  Builder.import_rule(b, rule)
end)

# Import sets
{:ok, sets} = Query.list_sets(pid, family: :inet)
builder = Enum.reduce(sets, builder, fn set, b ->
  Builder.import_set(b, set)
end)

# Execute modified configuration
Builder.submit(builder, pid)
```

#### Use Cases

**Backup and Restore:**
```elixir
# Backup existing rules
{:ok, builder} = Builder.from_ruleset(pid)
backup_json = Builder.to_json(builder)
File.write!("firewall_backup.json", backup_json)

# Restore later
backup = File.read!("firewall_backup.json")
{:ok, restored} = Jason.decode(backup)
# Apply restored configuration...
```

**Configuration Drift Detection:**
```elixir
# Import production config
{:ok, prod_builder} = Builder.from_ruleset(prod_pid)
prod_json = Builder.to_json(prod_builder)

# Compare with expected config
{:ok, expected_builder} = Builder.from_ruleset(staging_pid)
expected_json = Builder.to_json(expected_builder)

if prod_json != expected_json do
  Logger.warning("Configuration drift detected!")
end
```

**Incremental Updates:**
```elixir
# Query existing rules for a specific chain
{:ok, rules} = Query.list_rules(pid, "filter", "INPUT")

# Build update that preserves existing rules
builder = Builder.new()
|> Builder.add(table: "filter")
|> Builder.add(chain: "INPUT", type: :filter, hook: :input, priority: 0, policy: :drop)

# Import existing rules
builder = Enum.reduce(rules, builder, &Builder.import_rule(&2, &1))

# Add new rule
builder
|> Builder.add(chain: "INPUT")
|> Builder.add(rule: 
  expr()
  |> source_ip("10.0.0.0/8")
  |> accept()
)
|> Builder.submit(pid)
```

**Benefits:**
- **Query-Modify-Reapply**: Read existing config, modify, and apply changes
- **Backup/Restore**: Export configurations for disaster recovery
- **Configuration Management**: Track and manage firewall state programmatically
- **Drift Detection**: Compare actual vs expected configurations
- **Incremental Updates**: Add rules without affecting existing ones

## Migration Guide: Old API → New API

If you're upgrading from the old convenience functions, here's how to migrate to the new Builder + Expr API:

### Blocking an IP Address

**Old API:**
```elixir
Expr.block_ip(pid, "filter", "INPUT", "192.168.1.100")
```

**New API:**
```elixir
Builder.new(family: :inet)
|> Builder.add(table: "filter")
|> Builder.add(chain: "INPUT")
|> Builder.add(rule: 
  expr()
  |> source_ip("192.168.1.100")
  |> drop()
)
|> Builder.submit(pid)
```

### Accepting an IP Address

**Old API:**
```elixir
Expr.accept_ip(pid, "filter", "INPUT", "10.0.0.1")
```

**New API:**
```elixir
Builder.new(family: :inet)
|> Builder.add(table: "filter")
|> Builder.add(chain: "INPUT")
|> Builder.add(rule: 
  expr()
  |> source_ip("10.0.0.1")
  |> accept()
)
|> Builder.submit(pid)
```

### Rate Limiting

**Old API:**
```elixir
Expr.rate_limit(pid, "filter", "INPUT", 10, :minute, burst: 5)
```

**New API:**
```elixir
Builder.new(family: :inet)
|> Builder.add(table: "filter")
|> Builder.add(chain: "INPUT")
|> Builder.add(rule: 
  expr()
  |> limit(10, :minute, burst: 5)
  |> drop()  # or accept() depending on your use case
)
|> Builder.submit(pid)
```

### Deleting Rules

**Old API:**
```elixir
Expr.delete(pid, "filter", "INPUT", :inet, handle)
```

**New API:**
```elixir
# Query for rules first
{:ok, rules} = Query.list_rules(pid, "filter", "INPUT", family: :inet)

# Find the rule you want to delete and use Builder
rule = Enum.find(rules, fn r -> r.handle == target_handle end)

Builder.new(family: :inet)
|> Builder.add(table: "filter")
|> Builder.add(chain: "INPUT")
|> Builder.delete(rule: rule.handle)
|> Builder.submit(pid)
```

### Benefits of the New API

1. **Composability**: Build complex rules by chaining matchers and actions
2. **Type Safety**: Better compile-time validation of rule structure
3. **Testability**: Separate command building from execution
4. **Clarity**: Clear separation between matchers (source, protocol) and verdicts (accept, drop)
5. **CIDR Support**: Native support for CIDR notation like `"10.0.0.0/8"`
6. **Distributed Firewall**: Build commands centrally, execute on multiple nodes
7. **Batch Operations**: Combine multiple operations atomically

### Query Operations

**Old API:**
```elixir
Expr.list(pid, "filter", "INPUT", family: :inet)
```

**New API:**
```elixir
Query.list_rules(pid, "filter", "INPUT", family: :inet)
```

The Query module now handles all listing operations:
- `Query.list_tables/2`
- `Query.list_chains/3`
- `Query.list_rules/4`
- `Query.list_sets/3`

## Core Modules

### NFTables.Builder - Unified Configuration Builder

The Builder module is the primary interface for creating nftables configurations:

```elixir
alias NFTables.Builder

# Create table, chain, and set atomically
Builder.new(family: :inet)
|> Builder.add(table: "filter")
|> Builder.add(
  chain: "INPUT",
  table: "filter",
  type: :filter,
  hook: :input,
  priority: 0,
  policy: :drop
)
|> Builder.add(
  set: "blocklist",
  table: "filter",
  type: :ipv4_addr
)
|> Builder.add(
  element: ["192.168.1.100", "10.0.0.50"],
  set: "blocklist",
  table: "filter"
)
|> Builder.submit(pid)

# Query operations
{:ok, tables} = NFTables.Query.list_tables(pid, family: :inet)
{:ok, chains} = NFTables.Query.list_chains(pid, family: :inet)
```

### NFTables.Expr - Rule Expression Building

The `Rule` module now provides a fluent API for building rule expressions:

```elixir
alias NFTables.{Builder, Expr}
import NFTables.Expr

# Build complex rules using the fluent API
:ok = Builder.new(family: :inet)
  |> Builder.add(table: "filter")
  |> Builder.add(chain: "INPUT")
  |> Builder.add(rule: 
    expr()
    |> source_ip("192.168.1.100")
    |> protocol(:tcp)
    |> dport(80)
    |> drop()
  )
  |> Builder.submit(pid)

# List rules using Query module
{:ok, rules} = NFTables.Query.list_rules(pid, "filter", "INPUT", family: :inet)
```

**Note**: The old `Expr.block_ip/4`, `Expr.accept_ip/4`, `Expr.rate_limit/6`, and `Expr.delete/5` functions are deprecated. See the Migration Guide above for how to use the new Builder + Expr API.

### NFTables.Expr - Pure Expression Builder

The Match module provides a streamlined, pure functional API for building rule expressions:

```elixir
import NFTables.Expr
alias NFTables.Builder

# Build rule expressions with clean, chainable API
expr = expr()
  |> source_ip("192.168.1.100")
  |> tcp()
  |> dport(22)
  |> rate_limit(5, :minute)
  |> counter()
  |> drop()

# Execute via Builder pattern
Builder.new()
|> Builder.add(rule: expr, table: "filter", chain: "INPUT", family: :inet)
|> Builder.submit(pid: pid)

# Or use convenience aliases for more concise code
:ok = expr()
  |> source("192.168.1.100")  # alias for source_ip
  |> dport(22)                 # alias for dest_port
  |> tcp()                     # match TCP protocol
  |> limit(5, :minute)         # alias for rate_limit
  |> counter()
  |> drop()
  |> then(fn expr ->
    Builder.new()
    |> Builder.add(rule: expr, table: "filter", chain: "INPUT", family: :inet)
    |> Builder.submit(pid: pid)
  end)
```

**Key Features:**
- **Pure functional** - No side effects, expressions are data
- **Chainable API** - Build complex rules step by step
- **Convenience aliases** - `source/1`, `dest/1`, `dport/1`, `sport/1`, `tcp/1`, `udp/1`
- **Protocol helpers** - `tcp/0`, `udp/0`, `icmp/0`, `sctp/0`, `dccp/0`, `gre/0` for common protocols
- **Composable** - Build expressions in one context, execute in another
- **Advanced features** - Flowtables, meters, raw payload, TPROXY, OSF, and more

**Available Matchers:**
- **IP**: `source_ip/2`, `dest_ip/2`, `source/2`, `dest/2`
- **Ports**: `sport/2`, `dport/2`, `port/2` (supports ranges)
- **Protocol**: `protocol/2`, `tcp/1`, `udp/1`, `icmp/1`, `sctp/1`, `dccp/1`, `gre/1`
- **State**: `state/2`, `ct_state/2` - Connection tracking
- **Interface**: `iif/2`, `oif/2` - Input/output interfaces
- **Layer 2**: `source_mac/2`, `dest_mac/2`, `vlan_id/2`
- **CT**: `ct_status/2`, `ct_bytes/3`, `ct_packets/3`, `limit_connections/2`
- **Advanced**: `mark/2`, `dscp/2`, `icmp_type/2`, `tcp_flags/3`, `ttl/3`
- **Raw Payload**: `payload_raw/5`, `payload_raw_masked/6` - Deep packet inspection
- **Socket**: `socket_transparent/1` - Socket matching
- **OSF**: `osf_name/2`, `osf_version/2` - OS fingerprinting
- **SCTP/DCCP**: Use `sctp()/dccp()` for protocol, then `dport()/sport()` for ports
- **GRE**: `gre_version/2`, `gre_key/2`, `gre_flags/2` - GRE fields
- **Sets**: `in_set/3` - Match against named sets

**Available Actions:**
- **Meters**: `meter_update/5`, `meter_add/5` - Per-key rate limiting
- **Counters**: `counter/1` - Packet/byte counting
- **Logging**: `log/2` - Packet logging
- **Rate Limiting**: `limit/3`, `rate_limit/3` - Simple rate limiting
- **Marking**: `set_mark/2`, `set_connmark/2`, `save_mark/1`, `restore_mark/1`
- **Modification**: `set_dscp/2`, `set_ttl/2`, `increment_ttl/1`, `decrement_ttl/1`
- **CT**: `set_ct_label/2`, `set_ct_helper/2`, `set_ct_zone/2`

**Available Verdicts:**
- **Terminal**: `accept/1`, `drop/1`, `reject/1`, `reject/2`
- **Flow Control**: `jump/2`, `goto/2`, `return/1`, `continue/1`
- **NAT**: `snat_to/2`, `dnat_to/2`, `masquerade/1`, `redirect_to/2`
- **Advanced**: `tproxy/2` - Transparent proxy
- **Special**: `notrack/1`, `queue_to_userspace/2`, `synproxy/1`, `flow_offload/1`

See the [Match documentation](lib/nftex/match.ex) for the full API.

### NFTables.Policy - Pre-built Policies

```elixir
alias NFTables.{Policy, Builder}

# Quick firewall setup
:ok = NFTables.Policy.setup_basic_firewall(pid,
  allow_services: [:ssh, :http, :https],
  ssh_rate_limit: 10
)

# Individual policy helpers (composable - all in one transaction)
:ok =
  Builder.new()
  |> Policy.accept_loopback()
  |> Policy.accept_established()
  |> Policy.drop_invalid()
  |> Policy.allow_ssh(rate_limit: 10)
  |> Policy.allow_http()
  |> Policy.allow_https()
  |> Builder.submit(pid: pid)
```

### NFTables.Sysctl - Network Parameter Management

NFTables provides safe, whitelist-based access to kernel network parameters via `/proc/sys/net/*`. All operations use the existing CAP_NET_ADMIN capability.

```elixir
alias NFTables.{Sysctl, Sysctl.Network}

# Low-level API - direct parameter access
{:ok, "0"} = Sysctl.get(pid, "net.ipv4.ip_forward")
:ok = Sysctl.set(pid, "net.ipv4.ip_forward", "1")

# High-level helpers for common operations
:ok = Network.enable_ipv4_forwarding(pid)
:ok = Network.enable_ipv6_forwarding(pid)
:ok = Network.enable_syncookies(pid)

# Check forwarding status
{:ok, true} = Network.ipv4_forwarding_enabled?(pid)

# Connection tracking configuration
:ok = Network.set_conntrack_max(pid, 131072)
{:ok, 131072} = Network.get_conntrack_max(pid)

# ICMP configuration
:ok = Network.ignore_ping(pid)  # Stealth mode
:ok = Network.allow_ping(pid)   # Normal mode

# Composite operations
:ok = Network.configure_router(pid,
  ipv4_forwarding: true,
  ipv6_forwarding: true,
  syncookies: true,
  send_redirects: false
)

:ok = Network.harden_security(pid)
```

**Security Features:**
- Parameter whitelist (44 network-related parameters)
- Value validation per parameter type
- Limited to `/proc/sys/net/*` only
- Uses existing CAP_NET_ADMIN capability

**Supported Parameters:**
- IPv4/IPv6 forwarding and configuration
- TCP settings (syncookies, timestamps, keepalive, etc.)
- Connection tracking (nf_conntrack)
- ICMP settings
- Security parameters (rp_filter, source routing, redirects)

See `NFTables.Sysctl` and `NFTables.Sysctl.Network` documentation for the complete parameter list.

## Advanced Usage

### NAT Gateway

```elixir
alias NFTables.{Builder, NAT}

# Setup NAT with masquerading and port forwarding
Builder.new()
|> NAT.setup_masquerade("eth0", table: "nat")
|> NAT.port_forward(8080, "10.0.0.10", 80, table: "nat")
|> NAT.source_nat("10.0.0.0/24", "203.0.113.1", table: "nat")
|> Builder.submit(pid: pid)

# Static 1:1 NAT
Builder.new()
|> NAT.static_nat("203.0.113.100", "192.168.1.100", table: "nat")
|> Builder.submit(pid: pid)
```

### Connection Tracking

```elixir
import NFTables.Expr
alias NFTables.Builder

# Track connection state
expr = expr()
  |> ct_state([:established, :related])
  |> accept()

Builder.new()
|> Builder.add(rule: expr, table: "filter", chain: "INPUT", family: :inet)
|> Builder.submit(pid: pid)

# Or using Policy helpers for common patterns
:ok =
  Builder.new()
  |> NFTables.Policy.accept_established()
  |> Builder.submit(pid: pid)

# Connection limits
expr = expr()
  |> tcp()
  |> dport(80)
  |> ct_state([:new])
  |> limit_connections(100)  # Max 100 concurrent connections
  |> drop()

Builder.new()
|> Builder.add(rule: expr, table: "filter", chain: "INPUT", family: :inet)
|> Builder.submit(pid: pid)

# Track connection bytes
expr = expr()
  |> ct_bytes(:gt, 1_000_000)  # Over 1MB
  |> log("LARGE_TRANSFER: ")
  |> accept()

Builder.new()
|> Builder.add(rule: expr, table: "filter", chain: "FORWARD", family: :inet)
|> Builder.submit(pid: pid)
```

### Hardware Acceleration with Flowtables

Offload established connections to hardware for dramatic performance improvements:

```elixir
alias NFTables.Builder

# Create flowtable for hardware offloading
Builder.new(family: :inet)
|> Builder.add(table: "filter")
|> Builder.add(
  flowtable: "fastpath",
  hook: :ingress,
  priority: 0,
  devices: ["eth0", "eth1"]
)
|> Builder.submit(pid)

# Add rule to offload established connections
import NFTables.Expr

offload_rule = expr()
  |> state([:established, :related])
  |> flow_offload()

Builder.new()
|> Builder.add(rule: offload_rule, table: "filter", chain: "forward", family: :inet)
|> Builder.submit(pid)
```

### Per-Key Rate Limiting with Meters

Dynamic rate limiting per IP address or other keys:

```elixir
import NFTables.Expr
alias NFTables.Expr.Meter

# Per-IP SSH rate limiting
ssh_meter = expr()
  |> meter_update(
    Meter.payload(:ip, :saddr),  # Key: source IP
    "ssh_limits",                # Set name
    3,                           # 3 attempts
    :minute                      # per minute
  )
  |> tcp()
  |> dport(22)
  |> accept()

# Composite key (IP + port) for connection limits
conn_meter = expr()
  |> meter_add(
    Meter.composite_key([
      Meter.payload(:ip, :saddr),
      Meter.payload(:tcp, :dport)
    ]),
    "conn_limits",
    10,
    :second,
    burst: 5
  )
  |> accept()
```

### Deep Packet Inspection with Raw Payload

Match custom protocols using offset-based packet access:

```elixir
import NFTables.Expr

# Match DNS queries (port 53 via raw payload)
dns_block = expr()
  |> udp()
  |> payload_raw(:th, 16, 16, 53)  # Transport header, offset 16, length 16 bits, value 53
  |> log("DNS query blocked: ")
  |> drop()

# Check TCP SYN flag using masked match
syn_counter = expr()
  |> tcp()
  |> payload_raw_masked(:th, 104, 8, 0x02, 0x02)  # TCP flags offset, mask SYN bit
  |> counter()

# Match HTTP GET method
http_get = expr()
  |> tcp()
  |> dport(80)
  |> payload_raw(:ih, 0, 32, "GET ")  # Inner header, first 4 bytes
  |> log("HTTP GET: ")
  |> accept()
```

### Transparent Proxy with TPROXY

Intercept traffic without changing destination addresses:

```elixir
import NFTables.Expr
alias NFTables.Builder

# Setup transparent proxy for HTTP traffic
Builder.new(family: :ip)
|> Builder.add(table: "tproxy")
|> Builder.add(
  chain: "prerouting",
  type: :filter,
  hook: :prerouting,
  priority: -150,
  policy: :accept
)
|> Builder.submit(pid)

# Mark existing transparent sockets
mark_existing = expr()
  |> socket_transparent()
  |> set_mark(1)
  |> accept()

# TPROXY new HTTP connections
tproxy_http = expr()
  |> tcp()
  |> dport(80)
  |> mark(0)
  |> tproxy(to: 8080)

Builder.new()
|> Builder.add(rule: mark_existing, table: "tproxy", chain: "prerouting", family: :ip)
|> Builder.add(rule: tproxy_http, table: "tproxy", chain: "prerouting", family: :ip)
|> Builder.submit(pid)
```

### Specialized Protocols

Support for SCTP, DCCP, and GRE protocols:

```elixir
import NFTables.Expr

# SCTP (WebRTC data channels) - use generic dport/sport
sctp_rule = expr()
  |> sctp()
  |> dport(9899)
  |> accept()

# DCCP (streaming media) - use generic dport/sport
dccp_rule = expr()
  |> dccp()
  |> sport(5000)
  |> dport(6000)
  |> log("DCCP traffic: ")
  |> accept()

# GRE (VPN tunnels)
gre_rule = expr()
  |> gre()
  |> gre_version(0)
  |> gre_key(12345)
  |> source_ip("10.0.0.1")
  |> accept()

# Port ranges supported for SCTP/DCCP
sctp_range = expr()
  |> sctp()
  |> dport(9000..9999)
  |> counter()
```

### OS Fingerprinting

Passive operating system detection for security policies:

```elixir
import NFTables.Expr

# Allow SSH only from Linux systems
linux_ssh = expr()
  |> tcp()
  |> dport(22)
  |> osf_name("Linux")
  |> limit(10, :minute)
  |> accept()

# Rate limit Windows connections
windows_limit = expr()
  |> osf_name("Windows", ttl: :strict)
  |> limit(10, :second, burst: 5)
  |> accept()

# Block unknown OS
block_unknown = expr()
  |> osf_name("unknown")
  |> log("Unknown OS blocked: ")
  |> drop()

# OS-based marking for routing
mark_by_os = [
  expr() |> osf_name("Linux") |> set_mark(1),
  expr() |> osf_name("Windows") |> set_mark(2),
  expr() |> osf_name("MacOS") |> set_mark(3)
]
```

**Note:** OSF requires the pf.os database to be loaded:
```bash
nfnl_osf -f /usr/share/pf.os
```

### Raw JSON Commands

For advanced use cases, you can send raw JSON directly:

```elixir
json_cmd = Jason.encode!(%{
  "nftables" => [
    %{
      "add" => %{
        "table" => %{
          "family" => "inet",
          "name" => "custom"
        }
      }
    }
  ]
})

{:ok, response} = NFTables.Port.call(pid, json_cmd)
result = Jason.decode!(response)
```

Or use nft command syntax:

```elixir
nft_command = "add table inet custom"
{:ok, response} = NFTables.Port.call(pid, nft_command)
```

Both are processed by `libnftables.nft_run_cmd_from_buffer()`.

## Distributed Firewall Support

NFTables supports distributed firewall architectures where a central command & control node generates firewall rules and sends them to multiple firewall nodes for execution. This is achieved through separation of command building and execution.

### Architecture

```
┌──────────────────────────────┐
│  C&C Node                    │
│  (NFTables Library)             │
│                              │
│  - Builds firewall rules     │
│  - Generates JSON/nft cmds   │
│  - Sends to firewall nodes   │
└──────────┬───────────────────┘
           │
           │ JSON/nft commands over network
           │ (your transport)
           │
           ▼
┌──────────────────────────────┐
│  Firewall Node 1, 2, 3...    │
│  (Minimal Shim + Port)       │
│                              │
│  - Receives commands         │
│  - Executes via port         │
│  - Returns results           │
└──────────────────────────────┘
```

### Command Building Without Execution

Builder allows you to construct nftables configurations without executing them immediately:

```elixir
alias NFTables.{Builder, Match}
import Match

# Build configuration without executing
builder = Builder.new(family: :inet)
|> Builder.add(table: "filter")
|> Builder.add(
  chain: "INPUT",
  table: "filter",
  type: :filter,
  hook: :input,
  priority: 0,
  policy: :drop
)
|> Builder.add(
  rule: expr() |> source_ip("192.168.1.100") |> drop(),
  table: "filter",
  chain: "INPUT"
)

# Convert to JSON for inspection or remote execution
json_cmd = Builder.to_json(builder)

# Execute when ready
Builder.submit(builder, pid)
```

### Atomic Multi-Command Operations

Builder natively supports atomic batch operations - multiple commands are executed in a single transaction:

```elixir
alias NFTables.Builder

# Build multiple operations atomically
builder = Builder.new()
|> Builder.add(table: "filter", family: :inet)
|> Builder.add(
  table: "filter",
  chain: "INPUT",
  family: :inet,
  type: :filter,
  hook: :input,
  priority: 0,
  policy: :drop
)
|> Builder.add(set: "blocklist", table: "filter", type: :ipv4_addr)
|> Builder.add(element: ["1.2.3.4", "5.6.7.8"], set: "blocklist", table: "filter")

# Execute locally (all operations succeed or all fail - atomic)
Builder.submit(builder, pid)

# Or convert to JSON for remote execution
json = Builder.to_json(builder)
MyTransport.send_to_node("firewall-1", json)
```

### Execution Abstraction

The Builder pattern supports flexible execution via requestors:

```elixir
# Builder uses NFTables.Local requestor by default
builder = Builder.new()
|> Builder.add(table: "filter", family: :inet)
|> Builder.submit(pid: pid)  # Executes locally via NFTables.Local

# Or convert to JSON for inspection/remote execution
json = Builder.to_json(builder)
```

### Match for Remote Execution

The Match API generates pure expressions that can be combined with Builder for remote execution:

```elixir
import NFTables.Expr
alias NFTables.Builder

# Build complex rule as Builder command (not yet executed)
builder = Builder.new(family: :inet)
|> Builder.add(table: "filter")
|> Builder.add(chain: "INPUT")
|> Builder.add(rule: 
  expr()
  |> source_ip("192.168.1.100")
  |> tcp()
  |> dport(22)
  |> rate_limit(10, :minute)
  |> log("SSH_ATTACK: ")
  |> drop()
)

# Convert to JSON command for remote execution
json_cmd = Builder.to_json(builder)

# Send to remote nodes
MyTransport.send_to_node("firewall-1", json_cmd)
MyTransport.send_to_node("firewall-2", json_cmd)
MyTransport.send_to_node("firewall-3", json_cmd)

# On remote nodes, execute received command
NFTables.NFTables.Local.submit(Jason.decode!(json_cmd), pid: pid)
```

### Builder Operations Reference

Builder provides a unified interface for all nftables operations:

```elixir
alias NFTables.{Builder, Match}
import Match

# Table operations
Builder.new() |> Builder.add(table: "filter", family: :inet)
Builder.new() |> Builder.delete(table: "filter", family: :inet)

# Chain operations
Builder.new()
|> Builder.add(
  chain: "INPUT",
  table: "filter",
  family: :inet,
  type: :filter,
  hook: :input,
  priority: 0,
  policy: :drop
)
|> Builder.delete(chain: "INPUT", table: "filter", family: :inet)

# Set operations
Builder.new()
|> Builder.add(set: "blocklist", table: "filter", family: :inet, type: :ipv4_addr)
|> Builder.add(element: ["192.168.1.100", "10.0.0.50"], set: "blocklist", table: "filter")
|> Builder.delete(element: ["192.168.1.100"], set: "blocklist", table: "filter")
|> Builder.delete(set: "blocklist", table: "filter", family: :inet)

# Rule operations with Match expressions
block_ip_expr = expr() |> source_ip("192.168.1.100") |> drop()
accept_ip_expr = expr() |> source_ip("10.0.0.1") |> accept()
rate_limit_expr = expr() |> tcp() |> dport(22) |> limit(10, :second) |> accept()

Builder.new()
|> Builder.add(rule: block_ip_expr, table: "filter", chain: "INPUT", family: :inet)
|> Builder.submit(pid)
```

### Complete Distributed Firewall Example

```elixir
defmodule MyApp.DistributedFirewall do
  alias NFTables.{Builder, Match}
  import Match

  # On C&C node - build firewall configuration
  def build_firewall_config() do
    # Build expressions
    loopback_expr = expr() |> source_ip("127.0.0.1") |> accept()
    ssh_rate_limit_expr = expr()
      |> tcp() |> dport(22) |> state([:new])
      |> limit(10, :minute) |> accept()

    # Build complete configuration
    Builder.new(family: :inet)
    # Create table
    |> Builder.add(table: "filter")
    # Create INPUT chain
    |> Builder.add(
      chain: "INPUT",
      table: "filter",
      type: :filter,
      hook: :input,
      priority: 0,
      policy: :drop
    )
    # Create blocklist set
    |> Builder.add(
      set: "blocklist",
      table: "filter",
      type: :ipv4_addr
    )
    # Add malicious IPs to blocklist
    |> Builder.add(
      element: ["1.2.3.4", "5.6.7.8"],
      set: "blocklist",
      table: "filter"
    )
    # Allow loopback
    |> Builder.add(rule: loopback_expr, table: "filter", chain: "INPUT")
    # Rate limit SSH
    |> Builder.add(rule: ssh_rate_limit_expr, table: "filter", chain: "INPUT")
  end

  # On C&C node - deploy to multiple firewalls
  def deploy_to_firewalls(firewall_nodes) do
    config_builder = build_firewall_config()
    json_cmd = Builder.to_json(config_builder)

    # Send to all firewall nodes
    Enum.map(firewall_nodes, fn node ->
      Task.async(fn ->
        MyTransport.send_to_node(node, json_cmd)
      end)
    end)
    |> Task.await_many(timeout: 10_000)
  end

  # On firewall nodes - minimal shim
  def execute_received_command(json_cmd) do
    {:ok, pid} = NFTables.start_link()
    NFTables.Local.submit(json_cmd, pid: pid)
  end
end

# Deploy firewall rules to 3 nodes
MyApp.DistributedFirewall.deploy_to_firewalls([
  "firewall-1.local",
  "firewall-2.local",
  "firewall-3.local"
])
```

### Key Benefits

- **Incremental Updates** - Each operation generates one minimal command
- **Atomic Batches** - Multiple commands executed atomically (all-or-nothing)
- **Transport Agnostic** - Use any network transport (Phoenix PubSub, gRPC, etc.)
- **Centralized Logic** - Firewall policy managed from single C&C node
- **Minimal Remote Footprint** - Firewall nodes only need port + minimal shim
- **Fault Tolerant** - Port crashes isolated from BEAM VM

## Examples

The `examples/` directory contains complete, runnable examples:

- `01_basic_firewall.exs` - Complete firewall setup with secure defaults
- `04_rate_limiting.exs` - Rate limiting for DDoS protection
- `07_match_expressions.exs` - Advanced match expressions
- `08_nat_gateway.exs` - NAT gateway configuration
- `09_connection_tracking.exs` - Connection tracking features
- `10_packet_modification.exs` - Packet modification examples
- `firewall_rules.exs` - Dynamic rule management
- `ip_blocklist.exs` - IP blocklist with sets
- `query_tables.exs` - Query operations

Run any example:

```bash
mix run examples/01_basic_firewall.exs
```

## Testing

Run the test suite:

```bash
# Set capability on test binary
sudo setcap cap_net_admin=ep priv/port_nftables

# Run tests
mix test
```

## Security

NFTables follows security best practices:

1. **Minimal Privileges** - Port runs with only CAP_NET_ADMIN capability
2. **Permission Checks** - Port validates file permissions on startup (must not be world-readable/writable/executable)
3. **Input Validation** - All user input is validated before sending to the kernel
4. **Fault Isolation** - Port crashes don't affect the BEAM VM
5. **No Shell Commands** - All operations use libnftables API, no shell execution

See NFTables_port [Security](https://github.com/dcoai/nftables_port/dev_docs/security.md) doc.

## Contributing

Contributions are welcome! Please:

1. Fork the repository
2. Create a feature branch
3. Add tests for new functionality
4. Ensure all tests pass
5. Submit a pull request

## License

See [LICENSE](LICENSE) for details.

## Resources

- [nftables documentation](https://wiki.nftables.org/)
- [libnftables JSON API](https://wiki.nftables.org/wiki-nftables/index.php/JSON_API)
- [nft man page](https://www.netfilter.org/projects/nftables/manpage.html)
- [Netfilter project](https://www.netfilter.org/)

## Credits

Built with:
- [libnftables](https://www.netfilter.org/) - Official nftables library
- [Zig](https://ziglang.org/) - Systems programming language
- [Elixir](https://elixir-lang.org/) - Functional programming language