Skip to content

Instantly share code, notes, and snippets.

@haruue
Last active July 21, 2025 17:18
Show Gist options
  • Save haruue/08cd9ba401f27d50c8761131191db248 to your computer and use it in GitHub Desktop.
Save haruue/08cd9ba401f27d50c8761131191db248 to your computer and use it in GitHub Desktop.
iptables-apply, but for nftables
#!/bin/bash
# nftables-apply -- a safer way to update nftables remotely
#
# Usage:
# nftables-apply [-hV] [-t timeout] [-w savefile] {[rulesfile]|-c [runcmd]}
#
# Versions:
# * 1.0 Copyright 2006 Martin F. Krafft <[email protected]>
# Original version
# * 1.1 Copyright 2010 GW <[email protected] or http://gw.tnode.com/>
# Added parameter -c (run command)
# Added parameter -w (save successfully applied rules to file)
# Major code cleanup
# * 2.0 Copyright 2025 Haruue <[email protected]>
# Renamed from iptables-apply to nftables-apply
#
# Released under the terms of the Artistic Licence 2.0
#
set -eu
PROGNAME="${0##*/}"
VERSION=2.0
_nftables-save() {
echo '#!/usr/bin/nft -f'
echo 'flush ruleset'
nft list ruleset
}
_nftables-restore() {
nft -f -
}
### Default settings
DEF_TIMEOUT=10
MODE=0 # apply rulesfile mode
# MODE=1 # run command mode
SAVE="_nftables-save"
RESTORE="_nftables-restore"
DEF_RULESFILE="/etc/nftables.conf"
DEF_SAVEFILE="$DEF_RULESFILE"
DEF_RUNCMD="/etc/nftables.run"
### Functions
function blurb() {
cat <<-__EOF__
$PROGNAME $VERSION -- a safer way to update nftables remotely
__EOF__
}
function copyright() {
cat <<-__EOF__
$PROGNAME has been published under the terms of the Artistic Licence 2.0.
Original version - Copyright 2006 Martin F. Krafft <[email protected]>.
Version 1.1 - Copyright 2010 GW <[email protected] or http://gw.tnode.com/>.
Version 2.0 - Copyright 2025 Haruue <[email protected]>
__EOF__
}
function about() {
blurb
echo
copyright
}
function usage() {
blurb
echo
cat <<-__EOF__
Usage:
$PROGNAME [-hV] [-t timeout] [-w savefile] {[rulesfile]|-c [runcmd]}
The script will try to apply a new rulesfile (as output by 'nft list ruleset',
read by 'nft -f') or run a command to configure nftables and then
prompt the user whether the changes are okay. If the new nftables rules cut
the existing connection, the user will not be able to answer affirmatively.
In this case, the script rolls back to the previous working nftables rules
after the timeout expires.
Successfully applied rules can also be written to savefile and later used
to roll back to this state. This can be used to implement a store last good
configuration mechanism when experimenting with an nftables setup script:
$PROGNAME -w $DEF_SAVEFILE -c $DEF_RUNCMD
Options:
-t seconds, --timeout seconds
Specify the timeout in seconds (default: $DEF_TIMEOUT).
-w savefile, --write savefile
Specify the savefile where successfully applied rules will be written to
(default if empty string is given: $DEF_SAVEFILE).
-c runcmd, --command runcmd
Run command runcmd to configure nftables instead of applying a rulesfile
(default: $DEF_RUNCMD).
-h, --help
Display this help text.
-V, --version
Display version information.
__EOF__
}
function checkcommands() {
for cmd in "${COMMANDS[@]}"; do
if ! command -v "$cmd" >/dev/null; then
echo "Error: needed command not found: $cmd" >&2
exit 127
fi
done
}
function revertrules() {
echo -n "Reverting to old nftables rules... "
"$RESTORE" <"$TMPFILE"
echo "done."
}
### Parsing and checking parameters
TIMEOUT="$DEF_TIMEOUT"
SAVEFILE=""
SHORTOPTS="t:w:chV";
LONGOPTS="timeout:,write:,command,help,version";
OPTS=$(getopt -s bash -o "$SHORTOPTS" -l "$LONGOPTS" -n "$PROGNAME" -- "$@") || exit $?
for opt in $OPTS; do
case "$opt" in
(-*)
unset OPT_STATE
;;
(*)
case "${OPT_STATE:-}" in
(SET_TIMEOUT) eval TIMEOUT="$opt";;
(SET_SAVEFILE)
eval SAVEFILE="$opt"
[ -z "$SAVEFILE" ] && SAVEFILE="$DEF_SAVEFILE"
;;
esac
;;
esac
case "$opt" in
(-t|--timeout) OPT_STATE="SET_TIMEOUT";;
(-w|--write) OPT_STATE="SET_SAVEFILE";;
(-c|--command) MODE=1;;
(-h|--help) usage >&2; exit 0;;
(-V|--version) about >&2; exit 0;;
(--) break;;
esac
shift
done
# Validate parameters
if [ "$TIMEOUT" -ge 0 ] 2>/dev/null; then
TIMEOUT=$((TIMEOUT))
else
echo "Error: timeout must be a positive number" >&2
exit 1
fi
if [ -n "$SAVEFILE" ] && [ -e "$SAVEFILE" ] && [ ! -w "$SAVEFILE" ]; then
echo "Error: savefile not writable: $SAVEFILE" >&2
exit 8
fi
case "$MODE" in
(1)
# Treat parameter as runcmd (run command mode)
RUNCMD="${1:-$DEF_RUNCMD}"
if [ ! -x "$RUNCMD" ]; then
echo "Error: runcmd not executable: $RUNCMD" >&2
exit 6
fi
# Needed commands
COMMANDS=(mktemp nft "$RUNCMD")
checkcommands
;;
(*)
# Treat parameter as rulesfile (apply rulesfile mode)
RULESFILE="${1:-$DEF_RULESFILE}";
if [ ! -r "$RULESFILE" ]; then
echo "Error: rulesfile not readable: $RULESFILE" >&2
exit 2
fi
# Needed commands
COMMANDS=(mktemp nft)
checkcommands
;;
esac
### Begin work
# Store old nftables rules to temporary file
TMPFILE=$(mktemp "/tmp/$PROGNAME-XXXXXXXX")
trap 'rm -f $TMPFILE' EXIT HUP INT QUIT ILL TRAP ABRT BUS \
FPE USR1 SEGV USR2 PIPE ALRM TERM
if ! "$SAVE" >"$TMPFILE"; then
# An error occured
if ! grep -q nf_tables /proc/modules 2>/dev/null; then
echo "Error: nftables support lacking from the kernel" >&2
exit 3
else
echo "Error: unknown error saving old nftables rules: $TMPFILE" >&2
exit 4
fi
fi
# Legacy to stop the fail2ban daemon if present
[ -x /etc/init.d/fail2ban ] && /etc/init.d/fail2ban stop
# Configure nftables
case "$MODE" in
(1)
# Run command in background and kill it if it times out
echo -n "Running command '$RUNCMD'... "
"$RUNCMD" &
CMD_PID=$!
( sleep "$TIMEOUT"; kill "$CMD_PID" 2>/dev/null; exit 0 ) &
if ! wait "$CMD_PID"; then
echo "failed."
echo "Error: unknown error running command: $RUNCMD" >&2
revertrules
exit 7
else
echo "done."
fi
;;
(*)
# Apply nftables rulesfile
echo -n "Applying new nftables rules from '$RULESFILE'... "
if ! "$RESTORE" <"$RULESFILE"; then
echo "failed."
echo "Error: unknown error applying new nftables rules: $RULESFILE" >&2
revertrules
exit 5
else
echo "done."
fi
;;
esac
# Prompt user for confirmation
echo -n "Can you establish NEW connections to the machine? (y/N) "
read -r -n1 -t "$TIMEOUT" ret 2>&1 || :
case "${ret:-}" in
(y*|Y*)
# Success
echo
if [ -n "$SAVEFILE" ]; then
# Write successfully applied rules to the savefile
echo "Writing successfully applied rules to '$SAVEFILE'..."
if ! "$SAVE" >"$SAVEFILE"; then
echo "Error: unknown error writing successfully applied rules: $SAVEFILE" >&2
exit 9
fi
fi
echo "... then my job is done. See you next time."
;;
(*)
# Failed
echo
if [ -z "${ret:-}" ]; then
echo "Timeout! Something happened (or did not). Better play it safe..."
else
echo "No affirmative response! Better play it safe..."
fi
revertrules
exit 255
;;
esac
# Legacy to start the fail2ban daemon again
[ -x /etc/init.d/fail2ban ] && /etc/init.d/fail2ban start
exit 0
# vim:noet:sw=8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment