From afb7d704aae5dc7e0c3bf10551038cca21cd2007 Mon Sep 17 00:00:00 2001 From: Michael Tremer Date: Sun, 29 Jul 2012 14:30:48 +0000 Subject: [PATCH] firewall: Add basic IPv6 ruleset generation and macros. --- Makefile | 5 + firewall | 37 ++++- functions.constants | 9 +- functions.firewall | 73 +++++++--- functions.ip | 8 + functions.iptables | 346 +++++++++++++++++++++++++++++++++++++------- functions.macros | 90 ++++++++++++ functions.policy | 54 +++++-- macros/DHCP | 5 + macros/HTTP | 4 + macros/HTTPS | 4 + macros/WWW | 5 + 12 files changed, 547 insertions(+), 93 deletions(-) create mode 100644 functions.macros create mode 100644 macros/DHCP create mode 100644 macros/HTTP create mode 100644 macros/HTTPS create mode 100644 macros/WWW diff --git a/Makefile b/Makefile index 5cf67f24..0e7dc2e4 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ prefix=/usr bindir=$(prefix)/bin sbindir=$(prefix)/sbin libdir=$(prefix)/lib +datadir=$(prefix)/share sysconfdir=/etc localstatedir=/var systemdunitdir=$(prefix)/lib/systemd/system @@ -27,6 +28,7 @@ install: -mkdir -pv $(DESTDIR)$(sbindir) -mkdir -pv $(DESTDIR)$(systemdunitdir) -mkdir -pv $(DESTDIR)$(tmpfilesdir) + -mkdir -pv $(DESTDIR)$(datadir)/firewall install -m 755 -v firewall $(DESTDIR)$(sbindir) install -m 755 -v network $(DESTDIR)$(sbindir) @@ -60,6 +62,9 @@ install: # Install pppoe-server wrapper. install -m 755 ppp/pppoe-server $(DESTDIR)$(libdir)/network/ + # Install the firewall macros. + cp -av macros $(DESTDIR)$(datadir)/firewall/ + # Create the version file. : > $(VERSION_FILE) echo "# This file is automatically generated." >> $(VERSION_FILE) diff --git a/firewall b/firewall index 5b49ed14..2fa3db38 100755 --- a/firewall +++ b/firewall @@ -22,13 +22,40 @@ . /usr/lib/network/functions function cli_start() { - firewall_start + firewall_start $@ } function cli_stop() { firewall_stop } +function cli_show() { + firewall_show $@ +} + +function cli_panic() { + if cli_help_requested $@; then + cli_show_man firewall-panic + exit ${EXIT_OK} + fi + + local admin_hosts + while [ $# -gt 0 ]; do + case "${1}" in + *) + if ip_is_valid ${1}; then + admin_hosts="${admin_hosts} ${1}" + else + warning "Invalid IP address: ${1}" + fi + ;; + esac + shift + done + + firewall_panic ${admin_hosts} +} + function cli_config() { if cli_help_requested $@; then cli_usage root-config @@ -68,6 +95,14 @@ case "${action}" in cli_stop $@ ;; + show) + cli_show $@ + ;; + + panic) + cli_panic $@ + ;; + config) cli_config $@ ;; diff --git a/functions.constants b/functions.constants index 84bdbd46..fa192b31 100644 --- a/functions.constants +++ b/functions.constants @@ -93,8 +93,11 @@ IPTABLES_TMPDIR= FIREWALL_CONFIG_DIR="/etc/firewall" FIREWALL_ZONES_DIR="${FIREWALL_CONFIG_DIR}/zones" FIREWALL_CONFIG_FILE="${FIREWALL_CONFIG_DIR}/config" -FIREWALL_CONFIG_PORTFW="${FIREWALL_CONFIG_DIR}/portfw" +FIREWALL_CONFIG_RULES="${FIREWALL_CONFIG_DIR}/rules" -FIREWALL_CONFIG_PARAMS="" +FIREWALL_MACROS_DIRS="${FIREWALL_CONFIG_DIR}/macros /usr/share/firewall/macros" -FIREWALL_LOG_FACILITY="syslog" +FIREWALL_CONFIG_PARAMS="FIREWALL_LOG_METHOD FIREWALL_NFLOG_THRESHOLD" + +FIREWALL_LOG_METHOD="nflog" +FIREWALL_NFLOG_THRESHOLD=30 diff --git a/functions.firewall b/functions.firewall index 56a840f1..2a5cbedb 100644 --- a/functions.firewall +++ b/functions.firewall @@ -22,6 +22,23 @@ # High-level function which will create a ruleset for the current firewall # configuration and load it into the kernel. function firewall_start() { + # Test mode. + local test="false" + + while [ $# -gt 0 ]; do + case "${1}" in + --test) + test="true" + ;; + esac + shift + done + + if enabled test; then + log INFO "Test mode enabled." + log INFO "The firewall ruleset will not be loaded." + fi + firewall_lock_acquire # Initialize an empty iptables ruleset. @@ -39,8 +56,8 @@ function firewall_start() { policy_add_zone ${zone} done - # Commit the new ruleset. - iptables_commit + # Load the new ruleset. + iptables_load ${test} firewall_lock_release } @@ -52,8 +69,37 @@ function firewall_stop() { # with default policy ACCEPT. iptables_init ACCEPT - # Commit it. - iptables_commit + # Load it. + iptables_load + + firewall_lock_release +} + +function firewall_show() { + # Shows the ruleset that is currently loaded. + iptables_status + + return ${EXIT_OK} +} + +function firewall_panic() { + local admin_hosts="$@" + + firewall_lock_acquire + + # Drop all communications. + iptables_init DROP + + # If an admin host is provided, some administrative + # things will be allowed from there. + local admin_host + for admin_host in ${admin_hosts}; do + iptables -A INPUT -s ${admin_host} -j ACCEPT + iptables -A OUTPUT -d ${admin_host} -j ACCEPT + done + + # Load it. + iptables_load firewall_lock_release } @@ -117,17 +163,17 @@ function firewall_connection_tracking() { iptables -A FORWARD -j CONNTRACK } -function firewall_import_portfw() { +function firewall_import_rules() { local zone=${1} shift local protocol="ipv6" - local chain="filter" + local table="filter" while [ $# -gt 0 ]; do case "${1}" in - --chain=*) - chain=$(cli_get_val ${1}) + --table=*) + table=$(cli_get_val ${1}) ;; --protocol=*) protocol=$(cli_get_val ${1}) @@ -136,12 +182,7 @@ function firewall_import_portfw() { done assert isoneof protocol ipv4 ipv6 - - local allowed_chains="filter" - if [ "${protocol}" = "ipv4" ]; then - allowed_chains="${allowed_chains} nat" - fi - assert isoneof chain ${allowed_chains} + assert isoneof table $(iptables_table ${protocol}) # XXX TODO @@ -153,7 +194,5 @@ function firewall_import_portfw() { nat) ;; esac - done < ${FIREWALL_CONFIG_PORTFW} + done < ${FIREWALL_CONFIG_RULES} } - - diff --git a/functions.ip b/functions.ip index 54ed7783..2fa2d762 100644 --- a/functions.ip +++ b/functions.ip @@ -65,6 +65,14 @@ function ip_protocol_is_supported() { listmatch ${proto} ${IP_SUPPORTED_PROTOCOLS} } +function ip_is_valid() { + local address=${1} + assert isset address + + local proto=$(ip_detect_protocol ${address}) + isset proto && return ${EXIT_TRUE} || return ${EXIT_FALSE} +} + function ip_address_add() { local device=${1} local address=${2} diff --git a/functions.iptables b/functions.iptables index cf1e4769..10514051 100644 --- a/functions.iptables +++ b/functions.iptables @@ -22,18 +22,30 @@ function iptables() { local arg local args - local table + local table="filter" + local src dst + + # Default is both protocols. + local proto="6 4" # Check if the directory where we put our rules in is set and # exists. assert isset IPTABLES_TMPDIR assert [ -d "${IPTABLES_TMPDIR}" ] - table=filter - # Parsing arguments while [ $# -gt 0 ]; do case "${1}" in + # Select IPv4 protocol. + -4) + proto="4" + shift + ;; + # Select IPv6 protocol. + -6) + proto="6" + shift + ;; -t) table=${2} shift 2 @@ -44,94 +56,316 @@ function iptables() { ;; *) args="${args} ${1}" + + # Save some values for further processing. + case "${1}" in + -s) + src="${2}" + ;; + -d) + dst="${2}" + ;; + esac shift ;; esac done - echo "${args:1:${#args}}" >> ${IPTABLES_TMPDIR}/${table} + # Check that the nat table is not used for IPv6. + if isoneof 6 ${proto}; then + assert [ "${table}" != "nat" ] + fi + + # Detect the version of the IP protocol. + local src_proto + isset src && src_proto=$(ip_detect_protocol ${src}) + + local dst_proto + isset dst && dst_proto=$(ip_detect_protocol ${dst}) + + # Check that the source and destinations are not + # using different versions of the IP protocol. + if isset src_proto && isset dst_proto; then + assert [ "${src_proto}" = "${dst_proto}" ] + fi + + local rulesfile + local p + for p in ${proto}; do + case "${p}" in + 6) + listmatch ipv4 ${src_proto} ${dst_proto} \ + && continue + ;; + 4) + listmatch ipv6 ${src_proto} ${dst_proto} \ + && continue + ;; + esac + + rulesfile=$(iptables_rulesfile ipv${p} ${table}) + print "${args:1:${#args}}" >> ${rulesfile} + done +} + +# Calls the binary iptables command. +function _iptables() { + local iptables_cmd=$(which iptables) + assert isset iptables_cmd + + ${iptables_cmd} $@ +} + +function iptables_status() { + _iptables -L -n -v +} + +# Returns which tables exist for the given protocol. +function iptables_tables() { + local proto=${1} + local file + + case "${proto}" in + ipv6) + file="/proc/net/ip6_tables_names" + ;; + ipv4) + file="/proc/net/ip_tables_names" + ;; + *) + return ${EXIT_ERROR} + ;; + esac + + assert [ -r "${file}" ] + + print "$(<${file})" + return ${EXIT_OK} +} + +function iptables_rulesfile() { + local proto=${1} + proto=${proto/ipv/} + + local chain=${2} + [ -z "${chain}" ] && chain="ruleset" + + print "${IPTABLES_TMPDIR}/${chain}${proto}" } function iptables_init() { local policy=${1} - assert isoneof policy ACCEPT DROP + assert isset policy + # Create filter table and initialize chains. iptables "* filter" - iptables_chain_create -t filter INPUT ${policy} - iptables_chain_create -t filter OUTPUT ${policy} - iptables_chain_create -t filter FORWARD ${policy} + iptables_chain_create -t filter INPUT --policy=${policy} + iptables_chain_create -t filter OUTPUT --policy=${policy} + iptables_chain_create -t filter FORWARD --policy=${policy} + # Create mangle table initialize chains. iptables -t mangle "* mangle" - iptables_chain_create -t mangle PREROUTING ACCEPT - iptables_chain_create -t mangle INPUT ACCEPT - iptables_chain_create -t mangle OUTPUT ACCEPT - iptables_chain_create -t mangle FORWARD ACCEPT - iptables_chain_create -t mangle POSTROUTING ACCEPT + iptables_chain_create -t mangle PREROUTING --policy=ACCEPT + iptables_chain_create -t mangle INPUT --policy=ACCEPT + iptables_chain_create -t mangle OUTPUT --policy=ACCEPT + iptables_chain_create -t mangle FORWARD --policy=ACCEPT + iptables_chain_create -t mangle POSTROUTING --policy=ACCEPT - iptables -t nat "* nat" - iptables_chain_create -t nat PREROUTING ACCEPT - iptables_chain_create -t nat OUTPUT ACCEPT - iptables_chain_create -t nat POSTROUTING ACCEPT + # Add NAT table for IPv4. + iptables -4 -t nat "* nat" + iptables_chain_create -4 -t nat PREROUTING --policy=ACCEPT + iptables_chain_create -4 -t nat OUTPUT --policy=ACCEPT + iptables_chain_create -4 -t nat POSTROUTING --policy=ACCEPT } -function iptables_commit() { - local chain +# Load the created ruleset into the kernel. +function iptables_load() { + # If first argument is present and true, we + # run in test mode. + local test="${1}" - # Check if the directory where we put our rules in is set and - # exists. - assert isset IPTABLES_TMPDIR - assert [ -d "${IPTABLES_TMPDIR}" ] + local rulesfile - log INFO "Committing firewall configuration..." + # First, commit all tables. + _iptables_commit + + # Concat the table rulesets into one big file. + local proto + for proto in 6 4; do + rulesfile=$(iptables_rulesfile ipv${proto}) + + local table + local tablefile + for table in $(iptables_tables ipv${proto}); do + tablefile=$(iptables_rulesfile ipv${proto} ${table}) + print "$(<${tablefile})" + done > ${rulesfile} + done + + local error="false" + local ret + + # First check if everything is correctly formatted. + for proto in 6 4; do + rulesfile=$(iptables_rulesfile ipv${proto}) + + _iptables_load ipv${proto} ${rulesfile} true + if [ $? -ne ${EXIT_OK} ]; then + log CRITICAL "Ruleset load check failed for IPv${proto}" + error="true" + fi + done + + # Check if there has been an error in the load check. + if enabled error; then + iptables_dump CRITICAL + + log CRITICAL "New firewall rules could not be loaded." + return ${EXIT_ERROR} + fi + + # Dump the data, we are going to load. + iptables_dump + + # If we are running in test mode, we are done here. + enabled test && return ${EXIT_OK} + + # If we got until here, everything is fine to load the ruleset. + for proto in 6 4; do + rulesfile=$(iptables_rulesfile ipv${proto}) + + _iptables_load ipv${proto} ${rulesfile} + done + return ${EXIT_OK} +} + +# Commit all tables. +function _iptables_commit() { iptables -t filter "COMMIT" iptables -t mangle "COMMIT" - iptables -t nat "COMMIT" - local iptables_ruleset="${IPTABLES_TMPDIR}/commit" - : > ${iptables_ruleset} + # Commit NAT chain for IPv4. + iptables -4 -t nat "COMMIT" +} + +function _iptables_load() { + local proto=${1} + local file=${2} + local testmode=${3} - # Concat the rules for every chain into one file. - local table - for table in filter mangle nat; do - cat ${IPTABLES_TMPDIR}/${table} \ - >> ${iptables_ruleset} 2>/dev/null - done + local command + case "${proto}" in + ipv6) + command="ip6tables-restore" + ;; + ipv4) + command="iptables-restore" + ;; + esac + assert isset command + + if enabled testmode; then + command="${command} --test" + fi + + local time_started=$(date -u "+%s") + + cmd ${command} < ${file} + local ret=$? + + case "${ret}" in + ${EXIT_OK}) + local time_finished=$(date -u "+%s") + time_finished=$(( ${time_finished} - ${time_started} )) + log INFO \ + "Successfully loaded new firewall ruleset for IPv${proto/ipv/} in ${time_finished}s!" + ;; + *) + if ! enabled testmode; then + log CRITICAL "Error loading firewall ruleset for IPv${proto/ipv/}!" + fi + ;; + esac + + return ${ret} +} + +function iptables_dump() { + local log_facility=${1-DEBUG} + + # Here is nothing to do, if we are not running in + # debug mode. + enabled DEBUG || return ${EXIT_OK} - log DEBUG "Dumping iptables ruleset" - local counter=1 + local rulesfile + local counter local line - while read line; do - line=$(printf "%4d | %s\n" "${counter}" "${line}") - log DEBUG "${line}" - counter=$(( $counter + 1 )) - done < ${iptables_ruleset} - - iptables-restore < ${iptables_ruleset} + local proto + for proto in 6 4; do + rulesfile=$(iptables_rulesfile ipv${proto}) + [ -e "${rulesfile}" ] || continue + + log ${log_facility} "Firewall ruleset for IPv${proto}:" + + counter=1 + while read line; do + line=$(print "%4d | %s" "${counter}" "${line}") + log ${log_facility} "${line}" + + counter=$(( $counter + 1 )) + done < ${rulesfile} + done } function iptables_chain_create() { + local chain + local table="filter" + local policy="-" + local proto local args - if [ "${1}" = "-t" ]; then - args="${1} ${2}" - shift 2 - fi + while [ $# -gt 0 ]; do + case "${1}" in + -6|-4) + proto=${1} + ;; + -t) + table=${2} + shift + ;; + --policy=*) + policy=$(cli_get_val ${1}) + ;; + *) + chain=${1} + ;; + esac + shift + done + + assert isset chain + assert isset table + assert isoneof policy ACCEPT DROP "-" - iptables ${args} ":$1 ${2--} [0:0]" + iptables ${proto} -t ${table} ":${chain} ${policy} [0:0]" } function iptables_LOG() { local prefix=${1} + local ret - if [ "${FIREWALL_LOG_FACILITY}" = "syslog" ]; then - echo -n "LOG" - [ -n "$prefix" ] && echo -n " --log-prefix \"$prefix\"" - else - echo -n "NFLOG" - [ -n "$prefix" ] && echo -n " --nflog-prefix \"$prefix\"" - echo -n " --nflog-threshold 30" - fi - echo + case "${FIREWALL_LOG_METHOD}" in + nflog) + ret="NFLOG --nflog-threshold ${FIREWALL_NFLOG_THRESHOLD}" + isset prefix && ret="${ret} --nflog-prefix \"$prefix\"" + ;; + syslog) + ret="LOG" + isset prefix && ret="${ret} --log-prefix \"$prefix\"" + ;; + esac + + print "${ret}" } function iptables_protocol() { diff --git a/functions.macros b/functions.macros new file mode 100644 index 00000000..785e5719 --- /dev/null +++ b/functions.macros @@ -0,0 +1,90 @@ +#!/bin/bash +############################################################################### +# # +# IPFire.org - A linux based firewall # +# Copyright (C) 2012 IPFire Network Development Team # +# # +# 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. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +############################################################################### + +function macro_file() { + local macro=${1} + assert isset macro + + # Make the name uppercase. + macro=${macro^^} + + local file + local dir + for dir in ${FIREWALL_MACROS_DIRS}; do + file="${dir}/${macro}" + + if [ -r "${file}" ]; then + print "${file}" + return ${EXIT_OK} + fi + done + + return ${EXIT_ERROR} +} + +function macro_exists() { + local macro=${1} + assert isset macro + + macro_file ${macro} &>/dev/null \ + && return ${EXIT_TRUE} || return ${EXIT_FALSE} +} + +function macro_read() { + local macro=${1} + assert isset macro + + # Make the name uppercase. + macro=${macro^^} + + local file=$(macro_file ${macro}) + assert isset file + + log DEBUG "Parsing macro '${macro}' (${file})." + + local src dst + local sport dport + local proto + local var + local line + + while read src dst proto sport dport; do + # Skip lines that start with a "#". + [ "${src:0:1}" = "#" ] && continue + + if [ "${src}" = "INCLUDE" ]; then + macro_read ${dst} + continue + fi + + line="" + + # Remove all the dashes. + for var in src dst proto sport dport; do + [ "${!var}" = "-" ] && continue + + line="${line} ${var}=\"${!var}\"" + done + + line="$(echo ${line})" + print "${line}" + done < ${file} +} diff --git a/functions.policy b/functions.policy index 3871720b..5584d699 100644 --- a/functions.policy +++ b/functions.policy @@ -44,13 +44,13 @@ function policy_add_zone() { iptables_chain_create ${chain}_IPS iptables -A ${chain} -i ${zone} -j ${chain}_IPS - # Port forwarding chain. - iptables_chain_create ${chain}_PORTFW - iptables -A ${chain} -i ${zone} -j ${chain}_PORTFW + # Rules for incoming packets. + iptables_chain_create ${chain}_RULES_INC + iptables -A ${chain} -i ${zone} -j ${chain}_RULES_INC - # Outgoing firewall - iptables_chain_create ${chain}_OUTFW - iptables -A ${chain} -o ${zone} -j ${chain}_OUTFW + # Rules for outgoing packets. + iptables_chain_create ${chain}_RULES_OUT + iptables -A ${chain} -o ${zone} -j ${chain}_RULES_OUT # Policy rules iptables_chain_create ${chain}_POLICY @@ -68,21 +68,21 @@ function policy_add_zone() { iptables -t mangle -A ${chain} -o ${zone} -j ${chain}_QOS_OUT # Create NAT chain. - iptables_chain_create -t nat ${chain} - iptables -t nat -A PREROUTING -i ${zone} -j ${chain} - iptables -t nat -A POSTROUTING -o ${zone} -j ${chain} + iptables_chain_create -4 -t nat ${chain} + iptables -4 -t nat -A PREROUTING -i ${zone} -j ${chain} + iptables -4 -t nat -A POSTROUTING -o ${zone} -j ${chain} # Network Address Translation - iptables_chain_create -t nat ${chain}_NAT - iptables -t nat -A ${chain} -i ${zone} -j ${chain}_NAT + iptables_chain_create -4 -t nat ${chain}_NAT + iptables -4 -t nat -A ${chain} -i ${zone} -j ${chain}_NAT - # Portforwarding - iptables_chain_create -t nat ${chain}_PORTFW - iptables -t nat -A ${chain} -i ${zone} -j ${chain}_PORTFW + # Port forwarding + iptables_chain_create -4 -t nat ${chain}_PORTFW + iptables -4 -t nat -A ${chain} -i ${zone} -j ${chain}_PORTFW # UPNP - iptables_chain_create -t nat ${chain}_UPNP - iptables -t nat -A ${chain} -j ${chain}_UPNP + iptables_chain_create -4 -t nat ${chain}_UPNP + iptables -4 -t nat -A ${chain} -j ${chain}_UPNP # After the chains that are always available have been # created, we will add a custom policy to every single @@ -96,6 +96,9 @@ function policy_add_zone() { else : # XXX TODO fi + + # Import all configured rules and those things. + policy_import_all_rules ${zone} ${chain} } function policy_add_localhost() { @@ -116,3 +119,22 @@ function policy_allow_all() { # Just accept everything. iptables -A ${chain}_POLICY -j ACCEPT } + +function policy_drop_all() { + # Nothing to do here, because that is the + # default policy of the INPUT/OUTPUT/FORWARD chain. + : +} + +function policy_import_all_rules() { + # This will populate all chains with the rules + # for the given zone. + + local zone=${1} + assert isset zone + + local chain=${2} + assert isset chain + + # XXX TODO +} diff --git a/macros/DHCP b/macros/DHCP new file mode 100644 index 00000000..42c9d78a --- /dev/null +++ b/macros/DHCP @@ -0,0 +1,5 @@ +# IPFire Macro +# This macro handles the dynamic host configuration protocol. +# SRC DST PROTO LOCAL_PORT REMOTE_PORT +- - tcp 68 67 +- - udp 68 67 diff --git a/macros/HTTP b/macros/HTTP new file mode 100644 index 00000000..7cf58883 --- /dev/null +++ b/macros/HTTP @@ -0,0 +1,4 @@ +# IPFire Macro +# This macro handles plaintext HTTP (WWW) traffic. +# SRC DST PROTO LOCAL_PORT REMOTE_PORT +- - tcp - 80 diff --git a/macros/HTTPS b/macros/HTTPS new file mode 100644 index 00000000..f53ecf51 --- /dev/null +++ b/macros/HTTPS @@ -0,0 +1,4 @@ +# IPFire Macro +# This macro handles secure HTTP (WWW) traffic. +# SRC DST PROTO LOCAL_PORT REMOTE_PORT +- - tcp - 443 diff --git a/macros/WWW b/macros/WWW new file mode 100644 index 00000000..09a47180 --- /dev/null +++ b/macros/WWW @@ -0,0 +1,5 @@ +# IPFire Macro +# This macro handles WWW traffic. +# SRC DST PROTO SRC_PORT DST_PORT +INCLUDE HTTP +INCLUDE HTTPS -- 2.47.3