Skip to content

Instantly share code, notes, and snippets.

@fbouynot
Last active January 23, 2025 09:23
Show Gist options
  • Save fbouynot/00f89f4bcaa2b1fa4b9f70b24ebc8cc6 to your computer and use it in GitHub Desktop.
Save fbouynot/00f89f4bcaa2b1fa4b9f70b24ebc8cc6 to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation, either version 3 of the License, or (at your
# option) any later version. Please see LICENSE.txt at the top level of
# the source code distribution for details.
#
# @package nftables-apply
# @author <[email protected]>
# @link https://github.com/fbouynot/scripts/blob/main/nftables-apply
# @copyright <[email protected]>
#
# Free adaptation for nftables of iptables-apply (https://github.com/wertarbyte/iptables/blob/master/iptables-apply)
#
# -e: When a command fails, bash exits instead of continuing with the rest of the script
# -u: This will make the script fail, when accessing an unset variable
# -o pipefail: This will ensure that a pipeline command is treated as failed, even if one command in the pipeline fails
set -euo pipefail
# Replace the Internal Field Separator ' \n\t' by '\n\t' so you can loop through names with spaces
IFS=$'\n\t'
# Enable debug mode by running your script as TRACE=1 ./script.sh instead of ./script.sh
if [[ "${TRACE-0}" == "1" ]]
then
set -o xtrace
fi
# Define constants
PROGNAME="${0##*/}"
VERSION='1.2.4'
RED="$(tput setaf 1)"
NC="$(tput sgr0)" # No Color
DEFAULT_TIMEOUT=15
DEFAULT_DESTINATION_FILE='/etc/nftables.conf'
DEFAULT_SOURCE_FILE='/etc/nftables-candidate.conf'
readonly PROGNAME VERSION RED NC DEFAULT_TIMEOUT DEFAULT_DESTINATION_FILE DEFAULT_SOURCE_FILE
help() {
cat << EOF
Usage: ${PROGNAME} [-Vh] [ { -s | --source-file } <source-file> ] [ { -d | --destination-file } <destination-file> ] [ { -t | --timeout } <timeout> ]
-h --help Print this message.
-V --version Print the version.
-s --source-file STRING The source file for candidate config. (default: ${DEFAULT_SOURCE_FILE})
-d --destination-file STRING The destination file where to write the config. (default: ${DEFAULT_DESTINATION_FILE})
-t --timeout INT The time to wait before rolling back. (default: ${DEFAULT_TIMEOUT})
EOF
exit 2
}
version() {
cat << EOF
${PROGNAME} version ${VERSION} under GPLv3 licence.
EOF
exit 2
}
# Deal with arguments
while [[ $# -gt 1 ]]
do
key="${1}"
case $key in
-h|--help)
help
;;
-s|--source-file)
export source_file="${2}"
shift # consume -s
;;
-d|--destination-file)
export destination_file="${2}"
shift # consume -d
;;
-t|--timeout)
export timeout="${2}"
shift # consume -t
;;
-V|--version)
version
;;
*)
;;
esac
shift # consume $1
done
# Set defaults if no options specified
source_file="${source_file:-$DEFAULT_SOURCE_FILE}"
destination_file="${destination_file:-$DEFAULT_DESTINATION_FILE}"
timeout="${timeout:-$DEFAULT_TIMEOUT}"
# Change directory to base script directory
cd "$(dirname "${0}")"
# Check root permissions
check_root() {
# Check the command is run as root
if [ "${EUID}" -ne 0 ]
then
echo -e "${RED}E:${NC} please run as root" >&2
exit 3
fi
return 0
}
restore() {
nft flush ruleset
nft -f /tmp/nftables.conf.bak
rm -f /tmp/nftables.conf.bak
# Start fail2ban
if systemctl is-enabled fail2ban > /dev/null 2>&1
then
systemctl start fail2ban 2>/dev/null
fi
return 0
}
save() {
cp "${source_file}" "${destination_file}"
echo -e "\nConfiguration changed"
return 0
}
# Main function
main() {
# Check the command is run as root
check_root
# Check if we can read the destination file
if [[ ! -r "${destination_file}" ]]
then
echo -e "${RED}E:${NC} cannot read ${destination_file}" >&2
exit 4
fi
# Backup current ruleset
nft list ruleset > /tmp/nftables.conf.bak
# Check if we can read the source file
if [[ ! -r "${source_file}" ]]
then
echo -e "${RED}E:${NC} cannot read ${source_file}" >&2
exit 5
fi
# Dry run new ruleset, exit if failures
nft -f "${source_file}" || (echo -e "${RED}E:${NC} Invalid rules, exiting" >&2 && exit 6)
# Check the candidate configuration starts by flushing ruleset
if [[ $(head -n 1 /etc/nftables-candidate.conf) != "flush ruleset" ]]
then
sed -i '1s/^/flush ruleset\n/' "${source_file}"
fi
# Stop fail2ban
if systemctl is-active fail2ban > /dev/null 2>&1
then
systemctl stop fail2ban 2>/dev/null
fi
# Apply new ruleset, rollback if timeout
timeout "${timeout}"s nft -f "${source_file}" || (echo -e "${RED}E:${NC} timeout while applying new configuration, rolling back to the previous ruleset" >&2 && restore && exit 7)
# Ask the user if they can open a new connection
# If they can't, rollback
# If they can, save
echo -n "Can you establish NEW connections to the machine? (y/N) "
read -r -n1 -t "${timeout}" answer 2>&1 || :
if [[ "${answer}" == "y" ]]
then
save
else
echo -e "\n${RED}E:${NC} rolling back to the previous ruleset" >&2
restore
exit 8
fi
rm -f /tmp/nftables.conf.bak
# Start fail2ban
if systemctl is-enabled fail2ban > /dev/null 2>&1
then
systemctl start fail2ban 2>/dev/null
fi
exit 0
}
main "$@"
@fbouynot
Copy link
Author

fbouynot commented Mar 30, 2023 via email

@fbouynot
Copy link
Author

OK, I think I got it fixed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment