Packages

Elixir library for working with Linux nftables rules. Provides high-level APIs for building tables, chains, rules, maps and sets. Works with NFTables.Port for communicating with the kernel firewall.

Current section

Files

Jump to
nftables examples advanced_features.exs
Raw

examples/advanced_features.exs

#!/usr/bin/env elixir
# Advanced NFTables Features Demo
#
# This example demonstrates the new match expressions, NAT operations,
# and connection tracking features added to NFTables.
#
# Prerequisites:
# - CAP_NET_ADMIN capability set on the port binary
# - Run as: mix run examples/advanced_features.exs
defmodule AdvancedFeaturesDemo do
@moduledoc """
Demonstrates advanced NFTables features including:
- Extended match expressions (TCP flags, packet length, TTL, MAC, marks, DSCP)
- NAT operations (masquerade, port forwarding, static NAT)
- Advanced connection tracking (direction, status, marks)
- Packet modification (set marks)
"""
alias NFTables.{Table, Chain, RuleBuilder, NAT}
def run do
IO.puts("\n=== Advanced NFTables Features Demo ===\n")
# Start NFTables with Port (JSON-based communication)
{:ok, pid} = NFTables.Port.start_link(check_capabilities: false)
IO.puts("✓ NFTables started\n")
# Clean up any existing test tables
cleanup(pid)
# Demo each feature area
demo_match_expressions(pid)
demo_nat_operations(pid)
demo_connection_tracking(pid)
demo_packet_modification(pid)
IO.puts("\n=== Demo Complete ===")
IO.puts("Tables created: filter, nat")
IO.puts("Use 'nft list ruleset' to view rules")
# Keep process alive for inspection
IO.puts("\nPress Ctrl+C to exit and clean up...")
:timer.sleep(:infinity)
rescue
error ->
IO.puts("Error: #{inspect(error)}")
:ok
end
defp demo_match_expressions(pid) do
IO.puts("## Extended Match Expressions\n")
# Create filter table and chain
:ok = Table.add(pid, %{name: "filter", family: :inet})
:ok =
Chain.add(pid, %{
table: "filter",
name: "INPUT",
family: :inet
})
IO.puts("1. TCP Flags - Block SYN flood")
Match.new(pid, "filter", "INPUT")
|> Match.tcp_flags([:syn], [:syn, :ack, :rst, :fin])
|> Match.rate_limit(100, :second, burst: 20)
|> Match.drop()
|> Match.commit()
IO.puts(" ✓ Rule: Drop excessive SYN packets (>100/sec)")
IO.puts("\n2. Packet Length - Block jumbo frames")
Match.new(pid, "filter", "INPUT")
|> Match.length(:gt, 9000)
|> Match.log("JUMBO: ")
|> Match.drop()
|> Match.commit()
IO.puts(" ✓ Rule: Drop packets > 9000 bytes")
IO.puts("\n3. TTL - Block TTL=1 (traceroute)")
Match.new(pid, "filter", "INPUT")
|> Match.ttl(:eq, 1)
|> Match.log("TTL1: ")
|> Match.drop()
|> Match.commit()
IO.puts(" ✓ Rule: Drop packets with TTL=1")
IO.puts("\n4. MAC Address - Allow specific MAC")
Match.new(pid, "filter", "INPUT")
|> Match.source_mac("aa:bb:cc:dd:ee:ff")
|> Match.accept()
|> Match.commit()
IO.puts(" ✓ Rule: Accept from MAC aa:bb:cc:dd:ee:ff")
IO.puts("\n5. DSCP - Prioritize VoIP traffic")
Match.new(pid, "filter", "INPUT")
|> Match.dscp(46)
# Expedited Forwarding
|> Match.accept()
|> Match.commit()
IO.puts(" ✓ Rule: Accept DSCP 46 (EF - VoIP)")
IO.puts("\n6. UDP Ports - DNS traffic")
Match.new(pid, "filter", "INPUT")
|> Match.udp_dport(53)
|> Match.rate_limit(1000, :second)
|> Match.accept()
|> Match.commit()
IO.puts(" ✓ Rule: Rate-limit DNS queries (1000/sec)")
IO.puts("\n7. Fragmentation - Block fragments")
Match.new(pid, "filter", "INPUT")
|> Match.fragmented(true)
|> Match.log("FRAG: ")
|> Match.drop()
|> Match.commit()
IO.puts(" ✓ Rule: Drop fragmented packets")
IO.puts("")
end
defp demo_nat_operations(pid) do
IO.puts("## NAT Operations\n")
# Create NAT table and chains
:ok = Table.add(pid, %{name: "nat", family: :inet})
:ok =
Chain.add(pid, %{
table: "nat",
name: "prerouting",
family: :inet
})
:ok =
Chain.add(pid, %{
table: "nat",
name: "postrouting",
family: :inet
})
IO.puts("1. Internet Sharing (Masquerade)")
:ok =
NFTables.add(table: "filter")
|> NAT.setup_masquerade("eth0")
|> NFTables.submit(pid: pid)
IO.puts(" ✓ Masquerade on eth0 (internet sharing)")
IO.puts("\n2. Port Forwarding (DNAT)")
:ok =
NFTables.add(table: "filter")
|> NAT.port_forward(80, "192.168.1.100", 8080)
|> NAT.port_forward(443, "192.168.1.100", 8443)
|> NAT.port_forward(53, "192.168.1.1", 53, protocol: :udp)
|> NFTables.submit(pid: pid)
IO.puts(" ✓ Forward port 80 → 192.168.1.100:8080")
IO.puts(" ✓ Forward port 443 → 192.168.1.100:8443")
IO.puts(" ✓ Forward UDP port 53 → 192.168.1.1:53")
IO.puts("\n3. Static 1:1 NAT")
:ok =
NFTables.add(table: "filter")
|> NAT.static_nat("203.0.113.100", "192.168.1.100")
|> NFTables.submit(pid: pid)
IO.puts(" ✓ 1:1 NAT: 203.0.113.100 ↔ 192.168.1.100")
IO.puts("\n4. Source NAT for subnet")
:ok =
NFTables.add(table: "filter")
|> NAT.source_nat("10.0.0.0/24", "203.0.113.1")
|> NFTables.submit(pid: pid)
IO.puts(" ✓ SNAT: 10.0.0.0/24 → 203.0.113.1")
IO.puts("\n5. Transparent Proxy (Redirect)")
:ok =
NFTables.add(table: "filter")
|> NAT.redirect_port(80, 3128)
|> NFTables.submit(pid: pid)
IO.puts(" ✓ Redirect port 80 → 3128 (transparent proxy)")
IO.puts("")
end
defp demo_connection_tracking(pid) do
IO.puts("## Advanced Connection Tracking\n")
IO.puts("1. CT Direction - Match original direction")
Match.new(pid, "filter", "INPUT")
|> Match.ct_direction(:original)
|> Match.counter()
|> Match.accept()
|> Match.commit()
IO.puts(" ✓ Rule: Accept original direction")
IO.puts("\n2. CT Status - Match assured connections")
Match.new(pid, "filter", "INPUT")
|> Match.ct_status([:assured, :seen_reply])
|> Match.accept()
|> Match.commit()
IO.puts(" ✓ Rule: Accept assured connections")
IO.puts("\n3. CT Status - Detect NATed traffic")
Match.new(pid, "filter", "INPUT")
|> Match.ct_status([:snat])
|> Match.log("SNAT-CONN: ")
|> Match.accept()
|> Match.commit()
IO.puts(" ✓ Rule: Log SNAT connections")
IO.puts("\n4. Connection Mark - Match marked connections")
Match.new(pid, "filter", "INPUT")
|> Match.connmark(100)
|> Match.accept()
|> Match.commit()
IO.puts(" ✓ Rule: Accept connections with mark 100")
IO.puts("")
end
defp demo_packet_modification(pid) do
IO.puts("## Packet Modification\n")
IO.puts("1. Set Packet Mark - Policy routing")
Match.new(pid, "filter", "INPUT")
|> Match.source_ip("192.168.1.0/24")
|> Match.set_mark(100)
|> Match.accept()
|> Match.commit()
IO.puts(" ✓ Rule: Mark packets from 192.168.1.0/24 with 100")
IO.puts("\n2. Set Connection Mark - Persist across packets")
Match.new(pid, "filter", "INPUT")
|> Match.dest_port(80)
|> Match.ct_state([:new])
|> Match.set_connmark(200)
|> Match.accept()
|> Match.commit()
IO.puts(" ✓ Rule: Mark new HTTP connections with 200")
IO.puts("\n3. Packet Mark - QoS classification")
Match.new(pid, "filter", "INPUT")
|> Match.dscp(46)
# VoIP
|> Match.set_mark(1)
# High priority
|> Match.accept()
|> Match.commit()
IO.puts(" ✓ Rule: Mark VoIP packets with priority 1")
IO.puts("")
end
defp cleanup(pid) do
# Best effort cleanup
try do
Table.delete(pid, "filter", :inet)
Table.delete(pid, "nat", :inet)
rescue
_ -> :ok
catch
:exit, _ -> :ok
end
end
end
# Run the demo
AdvancedFeaturesDemo.run()