From 9e832a0fe3b5ae9220272f63de0f5de3680d9137 Mon Sep 17 00:00:00 2001 From: tengel Date: Wed, 20 Mar 2024 11:20:58 -0500 Subject: [PATCH] importing new rebuild --- README.md | 28 ++++ dyniptables.conf | 32 +++++ dyniptables.ipt | 4 + dyniptables.sh | 347 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 411 insertions(+) create mode 100644 dyniptables.conf create mode 100644 dyniptables.ipt create mode 100755 dyniptables.sh diff --git a/README.md b/README.md index 173d952..cae2bf6 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,32 @@ rebuild dynamic IPtables chain with DNS lookups of named hosts +### 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 in dyniptables.conf + 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) + ``` + SPDX-License-Identifier: MIT + diff --git a/dyniptables.conf b/dyniptables.conf new file mode 100644 index 0000000..92e7531 --- /dev/null +++ b/dyniptables.conf @@ -0,0 +1,32 @@ +# 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" diff --git a/dyniptables.ipt b/dyniptables.ipt new file mode 100644 index 0000000..af6f7d1 --- /dev/null +++ b/dyniptables.ipt @@ -0,0 +1,4 @@ +# example chain for iptables files + +:DYNAMIC - [0:0] +-A INPUT -j DYNAMIC diff --git a/dyniptables.sh b/dyniptables.sh new file mode 100755 index 0000000..f2bbda9 --- /dev/null +++ b/dyniptables.sh @@ -0,0 +1,347 @@ +#!/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