#!/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