From 87281df9037fc10f244c4f31e9731cc1332f8dad Mon Sep 17 00:00:00 2001
From: jow <jow@3c298f89-4303-0410-b956-a3cf2f4a3e73>
Date: Thu, 30 Jun 2011 01:31:23 +0000
Subject: [PATCH] [package] firewall: 	- allow multiple ports, protocols,
 macs, icmp types per rule 	- implement "limit" and "limit_burst" options
 for rules 	- implement "extra" option to rules and redirects for passing
 arbritary flags to iptables 	- implement negations for "src_port",
 "dest_port", "src_dport", "src_mac", "proto" and "icmp_type" options 	-
 allow wildcard (*) "src" and "dest" options in rules to allow specifying
 "any" source or destination 	- validate symbolic icmp-type names against
 the selected iptables binary 	- properly handle forwarded ICMPv6 traffic in
 the default configuration

git-svn-id: svn://svn.openwrt.org/openwrt/trunk@27317 3c298f89-4303-0410-b956-a3cf2f4a3e73
---
 package/firewall/Makefile                   |   2 +-
 package/firewall/files/firewall.config      |  61 ++++++++---
 package/firewall/files/lib/core_redirect.sh |  56 +++++-----
 package/firewall/files/lib/core_rule.sh     |  63 +++++++----
 package/firewall/files/lib/fw.sh            | 111 +++++++++++++++++---
 package/firewall/files/reflection.hotplug   |   1 +
 6 files changed, 220 insertions(+), 74 deletions(-)

diff --git a/package/firewall/Makefile b/package/firewall/Makefile
index f8510f182..ff62309d3 100644
--- a/package/firewall/Makefile
+++ b/package/firewall/Makefile
@@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
 PKG_NAME:=firewall
 
 PKG_VERSION:=2
-PKG_RELEASE:=26
+PKG_RELEASE:=27
 
 include $(INCLUDE_DIR)/package.mk
 
diff --git a/package/firewall/files/firewall.config b/package/firewall/files/firewall.config
index c852f4b00..c7bc79825 100644
--- a/package/firewall/files/firewall.config
+++ b/package/firewall/files/firewall.config
@@ -8,23 +8,23 @@ config defaults
 
 config zone
 	option name		lan
-	option network	'lan'
-	option input	ACCEPT 
-	option output	ACCEPT 
-	option forward	REJECT
+	option network		'lan'
+	option input		ACCEPT 
+	option output		ACCEPT 
+	option forward		REJECT
 
 config zone
 	option name		wan
-	option network	'wan'
-	option input	REJECT
-	option output	ACCEPT 
-	option forward	REJECT
+	option network		'wan'
+	option input		REJECT
+	option output		ACCEPT 
+	option forward		REJECT
 	option masq		1 
-	option mtu_fix	1
+	option mtu_fix		1
 
 config forwarding 
-	option src      lan
-	option dest     wan
+	option src      	lan
+	option dest     	wan
 
 # We need to accept udp packets on port 68,
 # see https://dev.openwrt.org/ticket/4108
@@ -33,14 +33,41 @@ config rule
 	option proto		udp
 	option dest_port	68
 	option target		ACCEPT
-	option family	ipv4
+	option family		ipv4
+
+# Allow IPv4 ping
+config rule
+	option src		wan
+	option proto		icmp
+	option icmp_type	echo-request
+	option family		ipv4
+	option target		ACCEPT
+
+# Allow essential incoming IPv6 ICMP traffic
+config rule                                   
+	option src		wan
+	option dest		*
+	option proto		icmp
+	list icmp_type		router-solicitation
+	list icmp_type		router-advertisement
+	list icmp_type		neighbour-solicitation
+	list icmp_type		neighbour-advertisement
+	list icmp_type		echo-request
+	list icmp_type		destination-unreachable
+	list icmp_type		packet-too-big
+	list icmp_type		time-exceeded
+	option limit		1000/sec
+	option family		ipv6
+	option target		ACCEPT
 
-#Allow ping
+# Drop leaking router advertisements on WAN
 config rule
-	option src wan
-	option proto icmp
-	option icmp_type echo-request
-	option target ACCEPT
+	option src		*
+	option dest		wan
+	option proto		icmp
+	option icmp_type	router-advertisement
+	option family		ipv6
+	option target		DROP
 
 # include a file with users custom iptables rules
 config include
diff --git a/package/firewall/files/lib/core_redirect.sh b/package/firewall/files/lib/core_redirect.sh
index 64c619e43..f511d2915 100644
--- a/package/firewall/files/lib/core_redirect.sh
+++ b/package/firewall/files/lib/core_redirect.sh
@@ -13,11 +13,11 @@ fw_config_get_redirect() {
 		string src_dport "" \
 		string dest "" \
 		ipaddr dest_ip "" \
-		string dest_mac "" \
 		string dest_port "" \
 		string proto "tcpudp" \
 		string family "" \
 		string target "DNAT" \
+		string extra "" \
 	} || return
 	[ -n "$redirect_name" ] || redirect_name=$redirect__name
 }
@@ -29,26 +29,27 @@ fw_load_redirect() {
 
 	local fwdchain natchain natopt nataddr natports srcdaddr srcdports
 	if [ "$redirect_target" == "DNAT" ]; then
-		[ -n "$redirect_src" -a -n "$redirect_dest_ip$redirect_dest_port" ] || {
+		[ -n "${redirect_src#*}" -a -n "$redirect_dest_ip$redirect_dest_port" ] || {
 			fw_log error "DNAT redirect ${redirect_name}: needs src and dest_ip or dest_port, skipping"
 			return 0
 		}
 
-		fwdchain="zone_${redirect_src}${redirect_dest_ip:+_forward}"
+		fwdchain="zone_${redirect_src}_forward"
 
 		natopt="--to-destination"
 		natchain="zone_${redirect_src}_prerouting"
 		nataddr="$redirect_dest_ip"
-		fw_get_port_range natports "$redirect_dest_port" "-"
+		fw_get_port_range natports "${redirect_dest_port#!}" "-"
 
 		fw_get_negation srcdaddr '-d' "${redirect_src_dip:+$redirect_src_dip/$redirect_src_dip_prefixlen}"
 		fw_get_port_range srcdports "$redirect_src_dport" ":"
+		fw_get_negation srcdports '--dport' "$srcdports"
 
 		list_contains FW_CONNTRACK_ZONES $redirect_src || \
 			append FW_CONNTRACK_ZONES $redirect_src
 
 	elif [ "$redirect_target" == "SNAT" ]; then
-		[ -n "$redirect_dest" -a -n "$redirect_src_dip" ] || {
+		[ -n "${redirect_dest#*}" -a -n "$redirect_src_dip" ] || {
 			fw_log error "SNAT redirect ${redirect_name}: needs dest and src_dip, skipping"
 			return 0
 		}
@@ -58,10 +59,11 @@ fw_load_redirect() {
 		natopt="--to-source"
 		natchain="zone_${redirect_dest}_nat"
 		nataddr="$redirect_src_dip"
-		fw_get_port_range natports "$redirect_src_dport" "-"
+		fw_get_port_range natports "${redirect_src_dport#!}" "-"
 
 		fw_get_negation srcdaddr '-d' "${redirect_dest_ip:+$redirect_dest_ip/$redirect_dest_ip_prefixlen}"
 		fw_get_port_range srcdports "$redirect_dest_port" ":"
+		fw_get_negation srcdports '--dport' "$srcdports"
 
 		list_contains FW_CONNTRACK_ZONES $redirect_dest || \
 			append FW_CONNTRACK_ZONES $redirect_dest
@@ -79,34 +81,38 @@ fw_load_redirect() {
 
 	local srcports
 	fw_get_port_range srcports "$redirect_src_port" ":"
+	fw_get_negation srcports '--sport' "$srcports"
 
 	local destaddr
 	fw_get_negation destaddr '-d' "${redirect_dest_ip:+$redirect_dest_ip/$redirect_dest_ip_prefixlen}"
 
 	local destports
 	fw_get_port_range destports "${redirect_dest_port:-$redirect_src_dport}" ":"
+	fw_get_negation destports '--dport' "$destports"
 
 	[ "$redirect_proto" == "tcpudp" ] && redirect_proto="tcp udp"
 	for redirect_proto in $redirect_proto; do
-		local pos
-		eval 'pos=$((++FW__REDIR_COUNT_'${mode#G}'_'$natchain'))'
-
-		fw add $mode n $natchain $redirect_target $pos { $redirect_src_ip $redirect_dest_ip } { \
-			$srcaddr $srcdaddr \
-			${redirect_proto:+-p $redirect_proto} \
-			${srcports:+--sport $srcports} \
-			${srcdports:+--dport $srcdports} \
-			${redirect_src_mac:+-m mac --mac-source $redirect_src_mac} \
-			$natopt $nataddr${natports:+:$natports} \
-		}
-
-		fw add $mode f ${fwdchain:-forward} ACCEPT ^ { $redirect_src_ip $redirect_dest_ip } { \
-			$srcaddr ${destaddr:--m conntrack --ctstate DNAT} \
-			${redirect_proto:+-p $redirect_proto} \
-			${srcports:+--sport $srcports} \
-			${destports:+--dport $destports} \
-			${redirect_src_mac:+-m mac --mac-source $redirect_src_mac} \
-		}
+		fw_get_negation redirect_proto '-p' "$redirect_proto"
+		for redirect_src_mac in ${redirect_src_mac:-""}; do
+			fw_get_negation redirect_src_mac '--mac-source' "$redirect_src_mac"
+			fw add $mode n $natchain $redirect_target + \
+				{ $redirect_src_ip $redirect_dest_ip } { \
+				$srcaddr $srcdaddr $redirect_proto \
+				$srcports $srcdports \
+				${redirect_src_mac:+-m mac $redirect_src_mac} \
+				$natopt $nataddr${natports:+:$natports} \
+				$redirect_options \
+			}
+
+			[ -n "$destaddr" ] && \
+			fw add $mode f ${fwdchain:-forward} ACCEPT + \
+				{ $redirect_src_ip $redirect_dest_ip } { \
+				$srcaddr $destaddr $redirect_proto \
+				$srcports $destports \
+				$redirect_src_mac \
+				$redirect_extra \
+			}
+		done
 	done
 
 	fw_callback post redirect
diff --git a/package/firewall/files/lib/core_rule.sh b/package/firewall/files/lib/core_rule.sh
index 8c234a33a..64a510df9 100644
--- a/package/firewall/files/lib/core_rule.sh
+++ b/package/firewall/files/lib/core_rule.sh
@@ -16,24 +16,23 @@ fw_config_get_rule() {
 		string proto "tcpudp" \
 		string target "" \
 		string family "" \
+		string limit "" \
+		string limit_burst "" \
+		string extra "" \
 	} || return
 	[ -n "$rule_name" ] || rule_name=$rule__name
-	[ "$rule_proto" == "icmp" ] || rule_icmp_type=
 }
 
 fw_load_rule() {
 	fw_config_get_rule "$1"
 
-	[ "$rule_target" != "NOTRACK" ] || [ -n "$rule_src" ] || {
+	[ "$rule_target" != "NOTRACK" ] || [ -n "$rule_src" ] || [ "$rule_src" != "*" ] || {
 		fw_log error "NOTRACK rule ${rule_name}: needs src, skipping"
 		return 0
 	}
 
 	fw_callback pre rule
 
-	fw_get_port_range rule_src_port $rule_src_port
-	fw_get_port_range rule_dest_port $rule_dest_port
-
 	local table=f
 	local chain=input
 	local target="${rule_target:-REJECT}"
@@ -41,8 +40,22 @@ fw_load_rule() {
 		table=r
 		chain="zone_${rule_src}_notrack"
 	else
-		[ -n "$rule_src" ] && chain="zone_${rule_src}${rule_dest:+_forward}"
-		[ -n "$rule_dest" ] && target="zone_${rule_dest}_${target}"
+		if [ -n "$rule_src" ]; then
+			if [ "$rule_src" != "*" ]; then
+				chain="zone_${rule_src}${rule_dest:+_forward}"
+			else
+				chain="${rule_dest:+forward}"
+				chain="${chain:-input}"
+			fi
+		fi
+
+		if [ -n "$rule_dest" ]; then
+			if [ "$rule_dest" != "*" ]; then
+				target="zone_${rule_dest}_${target}"
+			elif [ "$target" = REJECT ]; then
+				target=reject
+			fi
+		fi
 	fi
 
 	local mode
@@ -54,17 +67,31 @@ fw_load_rule() {
 
 	[ "$rule_proto" == "tcpudp" ] && rule_proto="tcp udp"
 	for rule_proto in $rule_proto; do
-		local rule_pos
-		eval 'rule_pos=$((++FW__RULE_COUNT_'${mode#G}'_'$chain'))'
-
-		fw add $mode $table $chain $target $rule_pos { $rule_src_ip $rule_dest_ip } { \
-			$src_spec $dest_spec \
-			${rule_proto:+-p $rule_proto} \
-			${rule_src_port:+--sport $rule_src_port} \
-			${rule_src_mac:+-m mac --mac-source $rule_src_mac} \
-			${rule_dest_port:+--dport $rule_dest_port} \
-			${rule_icmp_type:+--icmp-type $rule_icmp_type} \
-		}
+		fw_get_negation rule_proto '-p' "$rule_proto"
+		for rule_src_port in ${rule_src_port:-""}; do
+			fw_get_port_range rule_src_port $rule_src_port
+			fw_get_negation rule_src_port '--sport' "$rule_src_port"
+			for rule_dest_port in ${rule_dest_port:-""}; do
+				fw_get_port_range rule_dest_port $rule_dest_port
+				fw_get_negation rule_dest_port '--dport' "$rule_dest_port"
+				for rule_src_mac in ${rule_src_mac:-""}; do
+					fw_get_negation rule_src_mac '--mac-source' "$rule_src_mac"
+					for rule_icmp_type in ${rule_icmp_type:-""}; do
+						[ "$rule_proto" = "-p icmp" ] || rule_icmp_type=""
+						fw add $mode $table $chain $target + \
+							{ $rule_src_ip $rule_dest_ip } { \
+							$src_spec $dest_spec $rule_proto \
+							$rule_src_port $rule_dest_port \
+							${rule_src_mac:+-m mac $rule_src_mac} \
+							${rule_icmp_type:+--icmp-type $rule_icmp_type} \
+							${rule_limit:+-m limit --limit $rule_limit \
+								${rule_limit_burst:+--limit-burst $rule_limit_burst}} \
+							$rule_extra \
+						}
+					done
+				done
+			done
+		done
 	done
 
 	fw_callback post rule
diff --git a/package/firewall/files/lib/fw.sh b/package/firewall/files/lib/fw.sh
index 896947241..647bcd6a5 100644
--- a/package/firewall/files/lib/fw.sh
+++ b/package/firewall/files/lib/fw.sh
@@ -137,10 +137,13 @@ fw__exec() { # <action> <family> <table> <chain> <target> <position> { <rules> }
 	case "$tgt" in
 		-) tgt= ;;
 	esac
+
+	local rule_offset
 	case "$pos" in
 		^) pos=1 ;;
 		$) pos= ;;
 		-) pos= ;;
+		+) eval "rule_offset=\${FW__RULE_OFS_${app}_${tab}_${chn}:-1}" ;;
 	esac
 
 	if ! fw__has - family || ! fw__has $tab ; then
@@ -159,13 +162,29 @@ fw__exec() { # <action> <family> <table> <chain> <target> <position> { <rules> }
 		fi
 	fi
 
-	local cmdline="$app --table ${tab} --${cmd} ${chn} ${pol} ${pos} ${tgt:+--jump "$tgt"}"
+	local cmdline="$app --table ${tab} --${cmd} ${chn} ${pol} ${rule_offset:-${pos}} ${tgt:+--jump "$tgt"}"
 	while [ $# -gt 1 ]; do
-		case "$app:$1" in
-			ip6tables:--icmp-type) cmdline="$cmdline --icmpv6-type" ;;
-			ip6tables:icmp|ip6tables:ICMP) cmdline="$cmdline icmpv6" ;;
-			iptables:--icmpv6-type) cmdline="$cmdline --icmp-type" ;;
-			iptables:icmpv6) cmdline="$cmdline icmp" ;;
+		# special parameter handling
+		case "$1:$2" in
+			-p:icmp*|--protocol:icmp*)
+				[ "$app" = ip6tables ] && \
+					cmdline="$cmdline -p icmpv6" || \
+					cmdline="$cmdline -p icmp"
+				shift
+			;;
+			--icmp-type:*|--icmpv6-type:*)
+				local icmp_type
+				if [ "$app" = ip6tables ] && fw_check_icmptype6 icmp_type "$2"; then
+					cmdline="$cmdline $icmp_type"
+				elif [ "$app" = iptables ] && fw_check_icmptype4 icmp_type "$2"; then
+					cmdline="$cmdline $icmp_type"
+				else
+					local fam=IPv4; [ "$app" = ip6tables ] && fam=IPv6
+					fw_log info "ICMP type '$2' is not valid for $fam address family, skipping rule"
+					return 1
+				fi
+				shift	
+			;;
 			*) cmdline="$cmdline $1" ;;
 		esac
 		shift
@@ -175,7 +194,10 @@ fw__exec() { # <action> <family> <table> <chain> <target> <position> { <rules> }
 
 	$cmdline
 
-	fw__rc $?
+	local rv=$?
+	[ $rv -eq 0 ] && [ -n "$rule_offset" ] && \
+		export -- "FW__RULE_OFS_${app}_${tab}_${chn}=$(($rule_offset + 1))"
+	fw__rc $rv
 }
 
 fw_get_port_range() {
@@ -189,8 +211,8 @@ fw_get_port_range() {
 
 	local _first=${_ports%-*}
 	local _last=${_ports#*-}
-	if [ "$_first" != "$_last" ]; then
-		export -- "$_var=$_first$_delim$_last"
+	if [ "${_first#!}" != "${_last#!}" ]; then
+		export -- "$_var=$_first$_delim${_last#!}"
 	else
 		export -- "$_var=$_first"
 	fi
@@ -221,11 +243,11 @@ fw_get_family_mode() {
 fw_get_negation() {
 	local _var="$1"
 	local _flag="$2"
-	local _ipaddr="$3"
+	local _value="$3"
 
-	[ "${_ipaddr#!}" != "$_ipaddr" ] && \
-		export -n -- "$_var=! $_flag ${_ipaddr#!}" || \
-		export -n -- "$_var=${_ipaddr:+$_flag $_ipaddr}"
+	[ "${_value#!}" != "$_value" ] && \
+		export -n -- "$_var=! $_flag ${_value#!}" || \
+		export -n -- "$_var=${_value:+$_flag $_value}"
 }
 
 fw_get_subnet4() {
@@ -245,3 +267,66 @@ fw_get_subnet4() {
 		*) export -n -- "$_var=" ;;
 	esac
 }
+
+fw_check_icmptype4() {
+	local _var="$1"
+	local _type="$2"
+	case "$_type" in
+		![0-9]*) export -n -- "$_var=! --icmp-type ${_type#!}"; return 0 ;;
+		[0-9]*)  export -n -- "$_var=--icmp-type $_type";       return 0 ;;
+	esac
+
+	[ -z "$FW_ICMP4_TYPES" ] && \
+		export FW_ICMP4_TYPES=$(
+			iptables -p icmp -h 2>/dev/null | \
+			sed -n -e '/^Valid ICMP Types:/ {
+				n; :r;
+				/router-advertisement/d;
+				/router-solicitation/d;
+				s/[()]/ /g; s/[[:space:]]\+/\n/g; p; n; b r
+			}' | sort -u
+		)
+
+	local _check
+	for _check in $FW_ICMP4_TYPES; do
+		if [ "$_check" = "${_type#!}" ]; then
+			[ "${_type#!}" != "$_type" ] && \
+				export -n -- "$_var=! --icmp-type ${_type#!}" || \
+				export -n -- "$_var=--icmp-type $_type"
+			return 0
+		fi
+	done
+
+	export -n -- "$_var="
+	return 1
+}
+
+fw_check_icmptype6() {
+	local _var="$1"
+	local _type="$2"
+	case "$_type" in
+		![0-9]*) export -n -- "$_var=! --icmpv6-type ${_type#!}"; return 0 ;;
+		[0-9]*)  export -n -- "$_var=--icmpv6-type $_type";       return 0 ;;
+	esac
+
+	[ -z "$FW_ICMP6_TYPES" ] && \
+	 	export FW_ICMP6_TYPES=$(
+	 		ip6tables -p icmpv6 -h 2>/dev/null | \
+	 		sed -n -e '/^Valid ICMPv6 Types:/ {
+	 			n; :r; s/[()]/ /g; s/[[:space:]]\+/\n/g; p; n; b r
+	 		}' | sort -u
+	 	)
+
+	local _check
+	for _check in $FW_ICMP6_TYPES; do
+		if [ "$_check" = "${_type#!}" ]; then
+			[ "${_type#!}" != "$_type" ] && \
+				export -n -- "$_var=! --icmpv6-type ${_type#!}" || \
+				export -n -- "$_var=--icmpv6-type $_type"
+			return 0
+		fi
+	done
+
+	export -n -- "$_var="
+	return 1
+}
diff --git a/package/firewall/files/reflection.hotplug b/package/firewall/files/reflection.hotplug
index 33d121cec..4fd8f296d 100644
--- a/package/firewall/files/reflection.hotplug
+++ b/package/firewall/files/reflection.hotplug
@@ -56,6 +56,7 @@ if [ "$ACTION" = "add" ] && [ "$INTERFACE" = "wan" ]; then
 		[ "$src" = wan ] && [ "$target" = DNAT ] && {
 			local dest
 			config_get dest "$cfg" dest "lan"
+			[ "$dest" != "*" ] || return
 
 			local net
 			for net in $(find_networks "$dest"); do
-- 
2.20.1