347 lines
9.9 KiB
Bash
Executable file
347 lines
9.9 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
#
|
|
# Rebuild dynamic IPtables chain with DNS lookups of named hosts
|
|
# Required utils: getent, grep, cut, iptables, logger, stat
|
|
#
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
_VERSION="0.0.4"
|
|
_NAME=$(basename "${0}")
|
|
|
|
# Server prep and usage:
|
|
# 1. Place script in /usr/local/sbin/dyniptables.sh (root:root, 0744)
|
|
#
|
|
# 2. Add to system on-boot iptables rules a new filter chain and (j)ump:
|
|
# :DYNAMIC - [0:0]
|
|
# -A INPUT -j DYNAMIC
|
|
# ...where DYNAMIC is the name of the $DCHAIN below
|
|
#
|
|
# 3. Add to root's crontab a refresh every 6 hours:
|
|
# 5 */6 * * * /usr/local/sbin/dyniptables.sh
|
|
#
|
|
# 4. Add an override.conf to systemd iptables startup:
|
|
# DEB clones: `systemctl edit netfilter-persistent.service`
|
|
# or
|
|
# RPM clones: `systemctl edit iptables.service` (IPv4)
|
|
# `systemctl edit ip6tables.service` (IPv6)
|
|
#
|
|
# [Service]
|
|
# ExecStartPost=/usr/local/sbin/dyniptables.sh # (DEB, all rules)
|
|
# or
|
|
# ExecStartPost=/usr/local/sbin/dyniptables.sh -4 # (RPM, IPv4 only)
|
|
# ExecStartPost=/usr/local/sbin/dyniptables.sh -6 # (RPM, IPv6 only)
|
|
|
|
# List of hosts, declare emtpy array (do not edit)
|
|
declare -a DHOSTS
|
|
|
|
# Name of chain, declare empty variable
|
|
declare DCHAIN=""
|
|
|
|
# Our config for DHOSTS and DCHAIN
|
|
_CONFIG="/etc/dyniptables.conf"
|
|
|
|
##############################################################################
|
|
## Regular Expressions to match IPv4/IPv6
|
|
## https://stackoverflow.com/a/17871737/150649
|
|
## https://gist.github.com/syzdek/6086792
|
|
|
|
RE_IPV4="((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.)"
|
|
RE_IPV4="${RE_IPV4}{3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])"
|
|
|
|
# [1:2:3:4:5:6:7:8]
|
|
RE_IPV6="([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|"
|
|
# [1::] 1:2:3:4:5:6:7::
|
|
RE_IPV6="${RE_IPV6}([0-9a-fA-F]{1,4}:){1,7}:|"
|
|
# [1::8] 1:2:3:4:5:6::8 1:2:3:4:5:6::8
|
|
RE_IPV6="${RE_IPV6}([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|"
|
|
# [1::7:8] 1:2:3:4:5::7:8 1:2:3:4:5::8
|
|
RE_IPV6="${RE_IPV6}([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|"
|
|
# [1::6:7:8] 1:2:3:4::6:7:8 1:2:3:4::8
|
|
RE_IPV6="${RE_IPV6}([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|"
|
|
# [1::5:6:7:8] 1:2:3::5:6:7:8 1:2:3::8
|
|
RE_IPV6="${RE_IPV6}([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|"
|
|
# [1::4:5:6:7:8] 1:2::4:5:6:7:8 1:2::8
|
|
RE_IPV6="${RE_IPV6}([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|"
|
|
# [1::3:4:5:6:7:8] 1::3:4:5:6:7:8 1::8
|
|
RE_IPV6="${RE_IPV6}[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|"
|
|
# [::2:3:4:5:6:7:8] ::2:3:4:5:6:7:8 ::8 ::
|
|
RE_IPV6="${RE_IPV6}:((:[0-9a-fA-F]{1,4}){1,7}|:)|"
|
|
# [fe08::7:8%eth0] fe08::7:8%1
|
|
RE_IPV6="${RE_IPV6}fe08:(:[0-9a-fA-F]{1,4}){2,2}%[0-9a-zA-Z]{1,}|"
|
|
# [::255.255.255.255] ::ffff:255.255.255.255 ::ffff:0:255.255.255.255
|
|
RE_IPV6="${RE_IPV6}::(ffff(:0{1,4}){0,1}:){0,1}${RE_IPV4}|"
|
|
# [2001:db8:3:4::192.0.2.33] 64:ff9b::192.0.2.33
|
|
RE_IPV6="${RE_IPV6}([0-9a-fA-F]{1,4}:){1,4}:${RE_IPV4}"
|
|
|
|
##
|
|
##############################################################################
|
|
|
|
# Internal variables
|
|
declare _QUIET=0 _RULESV4=1 _RULESV6=1 _VALID4=1 _VALID6=1
|
|
|
|
# Allow Ctrl+C, etc.
|
|
function error_exit() {
|
|
echo "Trapped a kill signal, exiting."
|
|
exit 99
|
|
}
|
|
trap error_exit SIGHUP SIGINT SIGTERM
|
|
|
|
# Log to SYSLOG
|
|
function logme() {
|
|
if [[ $_QUIET -eq 0 ]]; then
|
|
echo "$1" | logger -t dyniptables
|
|
fi
|
|
}
|
|
|
|
# Converting input to int for port check
|
|
function to_int() {
|
|
local -i _num="10#${1}"
|
|
echo "${_num}"
|
|
}
|
|
|
|
# Config
|
|
function create_config() {
|
|
if [[ -f "${_CONFIG}" ]]; then
|
|
echo "Config file ${_CONFIG} already exists, not creating."
|
|
return 1
|
|
fi
|
|
echo "Creating config ${_CONFIG}"
|
|
cat << 'EOCONF' >> "${_CONFIG}"
|
|
# dyniptables config file
|
|
# permissions on this file must be root:root, 0644
|
|
|
|
# The DHOSTS array is already declared; uncomment and add a line for
|
|
# each host to allow, notice '+=' to ADD to the array each line.
|
|
#
|
|
# Format: 'TYPE,HOST,PORT' (comma seperated, no spaces!)
|
|
# add as many lines as needed using += as shown
|
|
#
|
|
# TYPE is one of: ipv4, ipv6
|
|
# HOST is the remote host to look up in DNS
|
|
# PORT is the local incoming port to open
|
|
#
|
|
# Examples:
|
|
#DHOSTS+=('ipv4,host1.domain.com,10051')
|
|
#DHOSTS+=('ipv6,host1.domain.com,10051')
|
|
#DHOSTS+=('ipv4,host2.domain.com,10051')
|
|
#DHOSTS+=('ipv6,host3.domain.com,10051')
|
|
|
|
# Name of dyname chain for both IPv4 and IPv6 - IT WILL BE FLUSHED
|
|
#
|
|
# The named chain must be created in your iptables chains FIRST manually
|
|
# - this script does not create/delete the basic (empty) chain!
|
|
# - example:
|
|
#
|
|
# :DYNAMIC - [0:0]
|
|
# -A INPUT -j DYNAMIC
|
|
#
|
|
# - you must use a unique name (not INPUT, etc.)
|
|
# - iptables has a 30char limit on chain names
|
|
#
|
|
DCHAIN="DYNAMIC"
|
|
|
|
EOCONF
|
|
|
|
# check our work
|
|
if [[ -f "${_CONFIG}" ]]; then
|
|
chown root:root "${_CONFIG}"
|
|
chmod 0644 "${_CONFIG}"
|
|
if [[ $(stat -c "%u:%g:%a" "${_CONFIG}") != "0:0:644" ]]; then
|
|
echo "ERROR: incorrect permissions on ${_CONFIG}"
|
|
return 1
|
|
fi
|
|
else
|
|
echo "ERROR: failed to create ${_CONFIG}"
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# Usage
|
|
function usage() {
|
|
echo "$_NAME version $_VERSION"
|
|
echo
|
|
echo "Rebuild dynamic IP chain with DNS lookups of named hosts"
|
|
echo
|
|
echo " Usage: $_NAME [-q] [-4|-6] || [-C|-V|-h]"
|
|
echo " Default behavior: both IPv4 and IPv6 rules processed."
|
|
echo
|
|
echo " -C Create default config and exit"
|
|
echo " -4 Process IPv4 rules only"
|
|
echo " -6 Process IPv6 rules only"
|
|
echo " -q Suppress logging (quiet mode)"
|
|
echo " -V Version of program"
|
|
echo " -h This help text"
|
|
echo
|
|
echo " Note: -4 and -6 together results in no rules processed."
|
|
}
|
|
|
|
# Commandline options
|
|
while getopts ":C46qVh" opt; do
|
|
case "$opt" in
|
|
C)
|
|
create_config
|
|
exit $? ;;
|
|
4)
|
|
_RULESV6=0 ;;
|
|
6)
|
|
_RULESV4=0 ;;
|
|
q)
|
|
_QUIET=1 ;;
|
|
V)
|
|
echo "$_NAME version $_VERSION"
|
|
exit 0 ;;
|
|
h)
|
|
usage
|
|
exit 0 ;;
|
|
*)
|
|
echo "Unrecognized option: $OPTARG (Run '$_NAME -h' for help)"
|
|
exit 1 ;;
|
|
esac
|
|
done
|
|
shift $((OPTIND-1))
|
|
|
|
# Attempt to read in our config
|
|
if [[ -r "${_CONFIG}" ]]; then
|
|
if [[ $(stat -c "%u:%g:%a" "${_CONFIG}") != "0:0:644" ]]; then
|
|
echo "ERROR: Config file security - ${_CONFIG}"
|
|
echo "ERROR: Config is not 'chown root:root', 'chmod 644' - exiting."
|
|
exit 1
|
|
else
|
|
# shellcheck source=/dev/null
|
|
source "${_CONFIG}"
|
|
if [[ ${#DHOSTS[@]} -eq 0 ]]; then
|
|
echo "ERROR: no hosts configred in DHOSTS, exiting."
|
|
exit 1
|
|
fi
|
|
if [[ -z "${DCHAIN}" ]]; then
|
|
echo "ERROR: no chain configured in DCHAIN, exiting."
|
|
exit 1
|
|
fi
|
|
fi
|
|
else
|
|
echo "ERROR: Missing config ${_CONFIG}"
|
|
echo "ERROR: Run '$_NAME -C' to create default, then edit."
|
|
exit 1
|
|
fi
|
|
|
|
# Networking may not be up; running a bad iptable rule can cause the
|
|
# service to fail the ExecStartPost in systemd and leave the system
|
|
# open. A lot of error checking is required for safety.
|
|
|
|
# Flush the existing chains; if the IP changed, we don't know what the
|
|
# old host->IP was without maintaining our own database
|
|
if [[ $_RULESV4 -eq 1 ]]; then
|
|
logme "Flushing $DCHAIN IPv4 chain"
|
|
/sbin/iptables -F "${DCHAIN}"
|
|
# shellcheck disable=SC2181
|
|
if [[ $? -ne 0 ]]; then
|
|
logme "IPv4 chain ${DCHAIN} flush error, skipping IPv4 rules"
|
|
_VALID4=0
|
|
fi
|
|
fi
|
|
if [[ $_RULESV6 -eq 1 ]]; then
|
|
logme "Flushing $DCHAIN IPv6 chain"
|
|
/sbin/ip6tables -F "${DCHAIN}"
|
|
# shellcheck disable=SC2181
|
|
if [[ $? -ne 0 ]]; then
|
|
logme "IPv6 chain ${DCHAIN} flush error, skipping IPv6 rules"
|
|
_VALID6=0
|
|
fi
|
|
fi
|
|
|
|
# Loop through the entries, take action
|
|
# for each element:
|
|
# convert comma to space to make new child array
|
|
# _tmparray[0] = TYPE
|
|
# _tmparray[1] = HOST
|
|
# _tmparray[2] = PORT
|
|
|
|
# shellcheck disable=SC2068
|
|
for iprule in ${DHOSTS[@]}; do
|
|
# shellcheck disable=SC2206
|
|
_tmparray=(${iprule//,/ })
|
|
|
|
# ensure 3 elements in the array
|
|
if [[ ${#_tmparray[@]} -ne 3 ]]; then
|
|
logme "Malformed entry, skipping: $iprule"
|
|
continue
|
|
fi
|
|
|
|
# port check
|
|
# https://docwhat.org/bash-checking-a-port-number
|
|
_portnum=$(to_int "${_tmparray[2]}" 2>/dev/null)
|
|
# shellcheck disable=SC2004
|
|
if (( $_portnum < 1 || $_portnum > 65535 )) ; then
|
|
logme "Invalid port, skipping: $iprule"
|
|
continue
|
|
fi
|
|
|
|
# IPv4 host
|
|
if [[ "${_tmparray[0]}" == "ipv4" ]]; then
|
|
|
|
# Silently skip if IPv4 ruls are disabled
|
|
if [[ $_RULESV4 -ne 1 ]]; then
|
|
continue
|
|
fi
|
|
|
|
# If we couldn't flush the chain, skip the rule
|
|
if [[ $_VALID4 -ne 1 ]]; then
|
|
logme "IPv4 chain error, skipping: $iprule"
|
|
continue
|
|
fi
|
|
|
|
# networking may not be up
|
|
_hostip=$(getent ahostsv4 "${_tmparray[1]}" | \
|
|
grep STREAM | \
|
|
cut -d' ' -f1 | \
|
|
grep -iE "${RE_IPV4}")
|
|
if [[ -z "$_hostip" ]]; then
|
|
logme "Unknown host, skipping: $iprule"
|
|
continue
|
|
fi
|
|
|
|
# add the rule if all checks pass
|
|
logme "Adding $_hostip to ${_tmparray[0]} chain, port ${_tmparray[2]}"
|
|
/sbin/iptables -I "$DCHAIN" -p tcp -m tcp \
|
|
-s i"${_hostip}" --dport "${_tmparray[2]}" -j ACCEPT
|
|
|
|
# IPv6 host
|
|
elif [[ "${_tmparray[0]}" == "ipv6" ]]; then
|
|
|
|
# Silently skip if IPv6 ruls are disabled
|
|
if [[ $_RULESV6 -ne 1 ]]; then
|
|
continue
|
|
fi
|
|
|
|
# If we couldn't flush the chain, skip the rule
|
|
if [[ $_VALID6 -ne 1 ]]; then
|
|
logme "IPv6 chain error, skipping: $iprule"
|
|
continue
|
|
fi
|
|
|
|
# networking may not be up
|
|
_hostip=$(getent ahostsv6 "${_tmparray[1]}" | \
|
|
grep STREAM | \
|
|
cut -d' ' -f1 | \
|
|
grep -iE "${RE_IPV6}")
|
|
if [[ -z "$_hostip" ]]; then
|
|
logme "Unknown host, skipping: $iprule"
|
|
continue
|
|
fi
|
|
|
|
# add the rule if all checks pass
|
|
logme "Adding $_hostip to ${_tmparray[0]} chain, port ${_tmparray[2]}"
|
|
/sbin/ip6tables -I "$DCHAIN" -p tcp -m tcp \
|
|
-s "${_hostip}" --dport "${_tmparray[2]}" -j ACCEPT
|
|
|
|
# Oops
|
|
else
|
|
logme "Invalid type, skipping: $iprule"
|
|
continue
|
|
fi
|
|
|
|
done;
|
|
|
|
# Clean exit
|
|
exit 0
|