Last active
July 21, 2025 17:18
-
-
Save haruue/08cd9ba401f27d50c8761131191db248 to your computer and use it in GitHub Desktop.
iptables-apply, but for nftables
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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