Learn how to configure nftables for NAT and port forwarding in home lab environments. This tutorial covers basic NAT masquerading, port forwarding rules, and advanced networking scenarios for virtual machines and containers.
Prerequisites
- Root or sudo access
- Basic understanding of networking concepts
- Multiple network interfaces or VLANs configured
What this solves
Home labs often need Network Address Translation (NAT) and port forwarding to allow virtual machines, containers, and services to communicate with external networks while maintaining security. This tutorial shows you how to configure nftables to provide internet access to your lab infrastructure and expose specific services through port forwarding rules.
Step-by-step installation
Update system and install nftables
Start by updating your system packages and installing nftables along with its documentation.
sudo apt update && sudo apt upgrade -y
sudo apt install -y nftables
Stop and disable conflicting firewalls
Disable other firewall services that might conflict with nftables configuration. This ensures nftables has full control over packet filtering and NAT rules.
sudo systemctl stop ufw
sudo systemctl disable ufw
sudo systemctl stop iptables
sudo systemctl disable iptables
Enable IP forwarding
Configure the kernel to allow packet forwarding between network interfaces, which is essential for NAT functionality.
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.conf
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
Create basic nftables configuration
Create a comprehensive nftables configuration file that includes NAT, port forwarding, and basic security rules for home lab environments.
#!/usr/sbin/nft -f
Clear existing rules
flush ruleset
Define variables for common interfaces and networks
define WAN_IF = "eth0" # Replace with your WAN interface
define LAN_IF = "eth1" # Replace with your LAN interface
define LAN_NET = "192.168.1.0/24" # Replace with your LAN network
define HOMELAB_NET = "10.0.0.0/24" # Replace with your lab network
Main filter table for firewall rules
table inet filter {
chain input {
type filter hook input priority filter
policy drop
# Allow loopback
iif lo accept
# Allow established and related connections
ct state established,related accept
# Allow ICMP/ICMPv6
ip protocol icmp accept
ip6 nexthdr icmpv6 accept
# Allow SSH from LAN and homelab networks
iifname { $LAN_IF } ip saddr { $LAN_NET, $HOMELAB_NET } tcp dport 22 accept
# Allow DNS from local networks
iifname { $LAN_IF } ip saddr { $LAN_NET, $HOMELAB_NET } udp dport 53 accept
iifname { $LAN_IF } ip saddr { $LAN_NET, $HOMELAB_NET } tcp dport 53 accept
# Drop everything else
counter drop
}
chain forward {
type filter hook forward priority filter
policy drop
# Allow established and related connections
ct state established,related accept
# Allow forwarding from LAN to WAN
iifname $LAN_IF oifname $WAN_IF ip saddr $LAN_NET accept
# Allow forwarding from homelab to WAN
iifname $LAN_IF oifname $WAN_IF ip saddr $HOMELAB_NET accept
# Allow forwarding between lab networks
iifname $LAN_IF oifname $LAN_IF ip saddr { $LAN_NET, $HOMELAB_NET } ip daddr { $LAN_NET, $HOMELAB_NET } accept
# Drop everything else
counter drop
}
chain output {
type filter hook output priority filter
policy accept
}
}
NAT table for address translation
table inet nat {
chain prerouting {
type nat hook prerouting priority dstnat
# Port forwarding rules for home lab services
# Web server on 10.0.0.10:80 -> external port 8080
iifname $WAN_IF tcp dport 8080 dnat to 10.0.0.10:80
# SSH to lab server on 10.0.0.20:22 -> external port 2222
iifname $WAN_IF tcp dport 2222 dnat to 10.0.0.20:22
# HTTPS service on 10.0.0.10:443 -> external port 8443
iifname $WAN_IF tcp dport 8443 dnat to 10.0.0.10:443
}
chain postrouting {
type nat hook postrouting priority srcnat
# Masquerade traffic from LAN networks to WAN
oifname $WAN_IF ip saddr { $LAN_NET, $HOMELAB_NET } masquerade
}
}
Identify your network interfaces
Determine the correct interface names for your system to update the configuration variables.
ip link show
ip route show default
Update interface variables
Edit the nftables configuration to match your actual network interface names and IP ranges.
sudo nano /etc/nftables.conf
Load and test the configuration
Load the nftables rules and verify they are working correctly before making them permanent.
sudo nft -f /etc/nftables.conf
sudo nft list ruleset
Enable nftables service
Enable the nftables service to automatically load rules on boot and start the service.
sudo systemctl enable nftables
sudo systemctl start nftables
sudo systemctl status nftables
Add additional port forwarding rules
Add more port forwarding rules for common home lab services like databases, monitoring tools, and development servers.
sudo nft add rule inet nat prerouting iifname "eth0" tcp dport 3306 dnat to 10.0.0.30:3306 # MySQL
sudo nft add rule inet nat prerouting iifname "eth0" tcp dport 5432 dnat to 10.0.0.30:5432 # PostgreSQL
sudo nft add rule inet nat prerouting iifname "eth0" tcp dport 9090 dnat to 10.0.0.40:9090 # Prometheus
sudo nft add rule inet nat prerouting iifname "eth0" tcp dport 3000 dnat to 10.0.0.40:3000 # Grafana
Save the updated configuration
Export the current ruleset to the configuration file to make the new rules persistent across reboots.
sudo nft list ruleset > /tmp/nftables_backup.conf
sudo cp /tmp/nftables_backup.conf /etc/nftables.conf
Configure advanced home lab scenarios
Create isolated lab network segment
Configure rules for an isolated lab segment that has limited internet access and strict inter-VLAN communication controls.
sudo nft add table inet lab_isolation
sudo nft add chain inet lab_isolation forward { type filter hook forward priority 0 \; }
sudo nft add rule inet lab_isolation forward iifname "lab0" oifname "lab1" drop
sudo nft add rule inet lab_isolation forward iifname "lab0" oifname "eth0" ip daddr { 8.8.8.8, 1.1.1.1 } accept
sudo nft add rule inet lab_isolation forward iifname "lab0" oifname "eth0" tcp dport { 80, 443 } accept
Configure load balancing for multiple lab servers
Set up simple round-robin load balancing for web services across multiple lab servers.
sudo nft add map inet nat lb_web { type mark : ipv4_addr \; }
sudo nft add element inet nat lb_web { 1 : 10.0.0.10, 2 : 10.0.0.11, 3 : 10.0.0.12 }
sudo nft add rule inet nat prerouting iifname "eth0" tcp dport 80 mark set numgen random mod 3 map { 0 : 1, 1 : 2, 2 : 3 }
sudo nft add rule inet nat prerouting iifname "eth0" tcp dport 80 dnat to mark map @lb_web
Set up bandwidth limiting
Configure basic traffic shaping to limit bandwidth for specific lab networks or services.
sudo nft add table inet qos
sudo nft add chain inet qos postrouting { type filter hook postrouting priority 0 \; }
sudo nft add rule inet qos postrouting oifname "eth0" ip saddr 10.0.0.0/24 limit rate 50 mbytes/second accept
Verify your setup
Test that NAT, port forwarding, and security rules are working correctly in your home lab environment.
# Check if rules are loaded
sudo nft list ruleset
Test NAT functionality from a lab machine
ping 8.8.8.8
curl -I http://example.com
Check if port forwarding is working
sudo ss -tlnp | grep :8080
nmap -p 8080 YOUR_WAN_IP
Verify IP forwarding is enabled
sysctl net.ipv4.ip_forward
sysctl net.ipv6.conf.all.forwarding
Monitor packet flow
sudo nft monitor
Check connection tracking
cat /proc/net/nf_conntrack | head -10
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Lab VMs can't reach internet | IP forwarding disabled or NAT masquerade not working | Check sysctl net.ipv4.ip_forward and verify masquerade rule in postrouting chain |
| Port forwarding not working | Incorrect interface names or missing DNAT rules | Verify interface names with ip link and check prerouting DNAT rules |
| Connection timeouts from external clients | Firewall blocking forwarded connections | Add accept rules in forward chain for destination ports and IPs |
| nftables rules not persistent after reboot | Service not enabled or configuration not saved | Run sudo systemctl enable nftables and save ruleset to /etc/nftables.conf |
| Inter-VLAN communication blocked | Forward chain dropping packets between lab networks | Add specific forward rules for allowed inter-network communication |
| DNS resolution fails in lab | DNS packets blocked by firewall rules | Allow UDP/TCP port 53 in input and forward chains for DNS servers |
Next steps
- Configure Linux system firewall with nftables and security hardening
- Configure NGINX load balancing with health checks and automatic failover
- Configure network interface monitoring with ICMP ping and connectivity testing
- Configure advanced nftables logging and monitoring for network security
- Set up nftables IPv6 NAT and dual-stack networking for modern networks
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Global variables
SCRIPT_NAME="nftables-nat-setup"
BACKUP_DIR="/tmp/${SCRIPT_NAME}-backup-$(date +%s)"
# Usage message
usage() {
cat << EOF
Usage: $0 [OPTIONS]
Configure nftables NAT and port forwarding for home lab environments
OPTIONS:
-w, --wan-if INTERFACE WAN interface name (default: auto-detect)
-l, --lan-if INTERFACE LAN interface name (default: auto-detect)
-n, --lan-net NETWORK LAN network CIDR (default: 192.168.1.0/24)
-L, --lab-net NETWORK Lab network CIDR (default: 10.0.0.0/24)
-h, --help Show this help message
Example:
$0 --wan-if enp0s3 --lan-if enp0s8 --lan-net 192.168.100.0/24
EOF
exit 1
}
# Logging functions
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Cleanup function
cleanup() {
if [[ -d "$BACKUP_DIR" ]]; then
log_warn "Script failed. Restoring from backup..."
if [[ -f "$BACKUP_DIR/nftables.conf" ]]; then
cp "$BACKUP_DIR/nftables.conf" /etc/nftables.conf 2>/dev/null || true
fi
if [[ -f "$BACKUP_DIR/sysctl.conf" ]]; then
cp "$BACKUP_DIR/sysctl.conf" /etc/sysctl.conf 2>/dev/null || true
fi
systemctl reload nftables 2>/dev/null || true
sysctl -p 2>/dev/null || true
fi
}
trap cleanup ERR
# Check prerequisites
check_prerequisites() {
echo "[1/8] Checking prerequisites..."
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root"
exit 1
fi
if ! command -v systemctl &> /dev/null; then
log_error "systemd is required"
exit 1
fi
}
# Detect distribution and package manager
detect_distro() {
echo "[2/8] Detecting distribution..."
if [[ ! -f /etc/os-release ]]; then
log_error "Cannot detect distribution - /etc/os-release not found"
exit 1
fi
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update"
PKG_INSTALL="apt install -y"
PKG_UPGRADE="apt upgrade -y"
FIREWALL_SERVICE="ufw"
;;
almalinux|rocky|centos|rhel|ol)
PKG_MGR="dnf"
PKG_UPDATE="dnf check-update || true"
PKG_INSTALL="dnf install -y"
PKG_UPGRADE="dnf upgrade -y"
FIREWALL_SERVICE="firewalld"
;;
fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf check-update || true"
PKG_INSTALL="dnf install -y"
PKG_UPGRADE="dnf upgrade -y"
FIREWALL_SERVICE="firewalld"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum check-update || true"
PKG_INSTALL="yum install -y"
PKG_UPGRADE="yum upgrade -y"
FIREWALL_SERVICE="iptables"
;;
*)
log_error "Unsupported distribution: $ID"
exit 1
;;
esac
log_info "Detected: $PRETTY_NAME using $PKG_MGR"
}
# Parse command line arguments
parse_args() {
WAN_IF=""
LAN_IF=""
LAN_NET="192.168.1.0/24"
LAB_NET="10.0.0.0/24"
while [[ $# -gt 0 ]]; do
case $1 in
-w|--wan-if) WAN_IF="$2"; shift 2 ;;
-l|--lan-if) LAN_IF="$2"; shift 2 ;;
-n|--lan-net) LAN_NET="$2"; shift 2 ;;
-L|--lab-net) LAB_NET="$2"; shift 2 ;;
-h|--help) usage ;;
*) log_error "Unknown option: $1"; usage ;;
esac
done
}
# Auto-detect network interfaces
detect_interfaces() {
if [[ -z "$WAN_IF" ]]; then
WAN_IF=$(ip route | grep default | head -1 | awk '{print $5}')
log_info "Auto-detected WAN interface: $WAN_IF"
fi
if [[ -z "$LAN_IF" ]]; then
LAN_IF=$(ip link show | grep -E '^[0-9]+:' | grep -v lo | grep -v "$WAN_IF" | head -1 | cut -d: -f2 | tr -d ' ')
log_info "Auto-detected LAN interface: $LAN_IF"
fi
if [[ -z "$WAN_IF" || -z "$LAN_IF" ]]; then
log_error "Could not detect network interfaces. Please specify them manually."
exit 1
fi
}
# Install and update packages
install_packages() {
echo "[3/8] Installing nftables..."
mkdir -p "$BACKUP_DIR"
$PKG_UPDATE
$PKG_INSTALL nftables
log_info "nftables installed successfully"
}
# Stop conflicting services
stop_conflicting_services() {
echo "[4/8] Stopping conflicting firewall services..."
local services=("$FIREWALL_SERVICE" "iptables")
for service in "${services[@]}"; do
if systemctl is-active --quiet "$service" 2>/dev/null; then
log_info "Stopping $service"
systemctl stop "$service"
fi
if systemctl is-enabled --quiet "$service" 2>/dev/null; then
log_info "Disabling $service"
systemctl disable "$service"
fi
done
}
# Enable IP forwarding
enable_ip_forwarding() {
echo "[5/8] Enabling IP forwarding..."
[[ -f /etc/sysctl.conf ]] && cp /etc/sysctl.conf "$BACKUP_DIR/"
if ! grep -q "net.ipv4.ip_forward = 1" /etc/sysctl.conf; then
echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.conf
fi
if ! grep -q "net.ipv6.conf.all.forwarding = 1" /etc/sysctl.conf; then
echo 'net.ipv6.conf.all.forwarding = 1' >> /etc/sysctl.conf
fi
sysctl -p
log_info "IP forwarding enabled"
}
# Create nftables configuration
create_nftables_config() {
echo "[6/8] Creating nftables configuration..."
[[ -f /etc/nftables.conf ]] && cp /etc/nftables.conf "$BACKUP_DIR/"
cat > /etc/nftables.conf << EOF
#!/usr/sbin/nft -f
# Clear existing rules
flush ruleset
# Define variables for interfaces and networks
define WAN_IF = "$WAN_IF"
define LAN_IF = "$LAN_IF"
define LAN_NET = "$LAN_NET"
define HOMELAB_NET = "$LAB_NET"
# Main filter table
table inet filter {
chain input {
type filter hook input priority filter
policy drop
iif lo accept
ct state established,related accept
ip protocol icmp accept
ip6 nexthdr icmpv6 accept
iifname { \$LAN_IF } ip saddr { \$LAN_NET, \$HOMELAB_NET } tcp dport 22 accept
iifname { \$LAN_IF } ip saddr { \$LAN_NET, \$HOMELAB_NET } udp dport 53 accept
iifname { \$LAN_IF } ip saddr { \$LAN_NET, \$HOMELAB_NET } tcp dport 53 accept
counter drop
}
chain forward {
type filter hook forward priority filter
policy drop
ct state established,related accept
iifname \$LAN_IF oifname \$WAN_IF ip saddr \$LAN_NET accept
iifname \$LAN_IF oifname \$WAN_IF ip saddr \$HOMELAB_NET accept
iifname \$LAN_IF oifname \$LAN_IF ip saddr { \$LAN_NET, \$HOMELAB_NET } ip daddr { \$LAN_NET, \$HOMELAB_NET } accept
counter drop
}
chain output {
type filter hook output priority filter
policy accept
}
}
# NAT table
table inet nat {
chain prerouting {
type nat hook prerouting priority dstnat
}
chain postrouting {
type nat hook postrouting priority srcnat
oifname \$WAN_IF ip saddr { \$LAN_NET, \$HOMELAB_NET } masquerade
}
}
EOF
chmod 644 /etc/nftables.conf
log_info "nftables configuration created"
}
# Enable and start services
enable_services() {
echo "[7/8] Enabling nftables service..."
systemctl enable nftables
systemctl restart nftables
log_info "nftables service enabled and started"
}
# Verify configuration
verify_setup() {
echo "[8/8] Verifying setup..."
if ! systemctl is-active --quiet nftables; then
log_error "nftables service is not running"
exit 1
fi
if ! sysctl net.ipv4.ip_forward | grep -q "= 1"; then
log_error "IP forwarding is not enabled"
exit 1
fi
if ! nft list tables | grep -q "inet filter"; then
log_error "nftables filter table not found"
exit 1
fi
if ! nft list tables | grep -q "inet nat"; then
log_error "nftables NAT table not found"
exit 1
fi
log_info "Setup verification completed successfully"
log_info "NAT and port forwarding configured for:"
log_info " WAN Interface: $WAN_IF"
log_info " LAN Interface: $LAN_IF"
log_info " LAN Network: $LAN_NET"
log_info " Lab Network: $LAB_NET"
log_info ""
log_info "To add port forwarding rules, edit /etc/nftables.conf"
log_info "Example: iifname \$WAN_IF tcp dport 8080 dnat to 10.0.0.10:80"
}
# Main execution
main() {
parse_args "$@"
check_prerequisites
detect_distro
detect_interfaces
install_packages
stop_conflicting_services
enable_ip_forwarding
create_nftables_config
enable_services
verify_setup
}
main "$@"
Review the script before running. Execute with: bash install.sh