Skip to content

Instantly share code, notes, and snippets.

@deas
Created February 13, 2026 12:34
Show Gist options
  • Select an option

  • Save deas/b28b194f347fc668d3af26b6a8993827 to your computer and use it in GitHub Desktop.

Select an option

Save deas/b28b194f347fc668d3af26b6a8993827 to your computer and use it in GitHub Desktop.
#!/bin/bash
set -euo pipefail
# Check for root privileges
if [[ $EUID -ne 0 ]]; then
echo "Error: This script must be run as root" >&2
exit 1
fi
show_usage() {
echo "Usage:"
echo " $0 <name> <destination_ip>[:port] <source_ip1> [source_ip2] ... - Create DNAT rules"
echo " $0 <name> remove - Remove DNAT rules"
echo " $0 list - List active multi-redirect tables"
echo ""
echo "Examples:"
echo " $0 myapp 192.168.1.10:8080 10.0.0.1 10.0.0.2 - Forward port 8080 from sources"
echo " $0 myapp 192.168.1.10 10.0.0.1 10.0.0.2 - Forward all ports from sources"
exit 1
}
# Show usage if no arguments or help requested
if [[ $# -eq 0 ]] || [[ "${1:-}" == "-h" ]] || [[ "${1:-}" == "--help" ]]; then
show_usage
fi
# Handle list command (no name required)
if [[ "$1" == "list" ]]; then
echo "Active DNAT multi-redirect tables:"
nft list tables 2>/dev/null | grep "table ip" | awk '{print $3}' | while read -r table; do
if nft list table ip "$table" 2>/dev/null | grep -q "dnat to"; then
echo " - $table"
fi
done
exit 0
fi
# Now we need at least a name
if [[ $# -lt 1 ]]; then
show_usage
fi
TABLE_NAME="$1"
# Check if second argument is "remove"
if [[ "${2:-}" == "remove" ]]; then
if [[ $# -ne 2 ]]; then
echo "Error: remove takes no additional arguments" >&2
show_usage
fi
if ! nft list table ip "$TABLE_NAME" >/dev/null 2>&1; then
echo "Table '$TABLE_NAME' does not exist" >&2
exit 1
fi
nft delete table ip "$TABLE_NAME"
echo "Removed DNAT rules for table: $TABLE_NAME"
exit 0
fi
# Create mode: name destination_ip[:port] source_ip1 [source_ip2 ...]
if [[ $# -lt 3 ]]; then
echo "Error: Invalid number of arguments. Need at least: name, destination_ip[:port], and one source_ip" >&2
show_usage
fi
DEST="$2"
shift 2
SOURCE_IPS=("$@")
# Parse destination - can be IP:port or just IP (for all ports)
DST_IP=""
PORT=""
if [[ "$DEST" =~ ^([^:]+):([0-9]+)$ ]]; then
# IP:port format
DST_IP="${BASH_REMATCH[1]}"
PORT="${BASH_REMATCH[2]}"
elif [[ "$DEST" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
# IP only - forward all ports
DST_IP="$DEST"
PORT=""
else
echo "Error: Destination must be IP:port (e.g., 192.168.1.10:8080) or IP address (for all ports)" >&2
exit 1
fi
# Enable IP forwarding
sysctl -w net.ipv4.ip_forward=1 >/dev/null
# Build nftables rules for source IPs
PREROUTING_RULES=""
OUTPUT_RULES=""
for src_ip in "${SOURCE_IPS[@]}"; do
if [[ -n "$PORT" ]]; then
# Specific port forwarding
PREROUTING_RULES+=" tcp dport $PORT ip daddr $src_ip dnat to $DST_IP:$PORT
"
OUTPUT_RULES+=" tcp dport $PORT ip daddr $src_ip dnat to $DST_IP:$PORT
"
else
# All ports forwarding
PREROUTING_RULES+=" ip daddr $src_ip dnat to $DST_IP
"
OUTPUT_RULES+=" ip daddr $src_ip dnat to $DST_IP
"
fi
done
# Create or replace the nftables table
nft "
add table ip $TABLE_NAME
flush table ip $TABLE_NAME
table ip $TABLE_NAME {
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
$PREROUTING_RULES }
chain output {
type nat hook output priority dstnat; policy accept;
$OUTPUT_RULES }
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
ip daddr $DST_IP masquerade
}
}
"
SOURCE_IPS_STR=$(IFS=, ; echo "${SOURCE_IPS[*]}")
if [[ -n "$PORT" ]]; then
echo "Created DNAT rules: $SOURCE_IPS_STR:$PORT -> $DST_IP:$PORT (table: $TABLE_NAME)"
else
echo "Created DNAT rules: $SOURCE_IPS_STR -> $DST_IP (all ports, table: $TABLE_NAME)"
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment