aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--LICENSE21
-rw-r--r--README.md24
-rwxr-xr-xfirewall.sh176
3 files changed, 221 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..853b46d
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c6f2292
--- /dev/null
+++ b/README.md
@@ -0,0 +1,24 @@
+# stateful-iptables-firewall
+Dual stack capable iptables firewall script
+
+```
+public and private are both public facing interfaces
+but have different ports allowed. e.g. private (1.1.1.2)
+IP allows SSH and VPN access while public (1.1.1.1) allows
+HTTP.
+
++----------+ +-------------+
+| | "public" | 1.1.1.1 |
+| <-----------------> 2001::1 |
+| internet | | eth0 |
+| <-----------------> 1.1.1.2 |
+| | "private" | 2001::2 |
++----------+ +-------------+
+
+
++-----------+ +-------------+
+| | 172.16.23.0/24 | |
+| VPN <----------------> tun0 |
+| | | 172.16.23.1 |
++-----------+ +-------------+
+```
diff --git a/firewall.sh b/firewall.sh
new file mode 100755
index 0000000..b57fa22
--- /dev/null
+++ b/firewall.sh
@@ -0,0 +1,176 @@
+#!/bin/bash
+# Created by Nicholas Hall <ngharo@gmail.com>
+declare -A public_services_tcp
+declare -A public_services_udp
+declare -A private_services_tcp
+declare -A private_services_udp
+
+public_ipv4="1.1.1.1/32"
+public_ipv6="2001::1/128"
+public_services_tcp=([HTTP]=80 [HTTPS]=443)
+public_services_udp=()
+
+private_ipv4="1.1.1.2/32"
+private_ipv6="2001::2/128"
+ # port range
+private_services_tcp=([SSH]=22022 [rtorrent]="61000:61100")
+private_services_udp=([OpenVPN]=35221)
+
+vpn_interface="tun0"
+vpn_subnet="172.16.23.0/24"
+vpn_gateway="172.16.23.1/32"
+
+# reply to pings?
+allow_icmp_ping=true
+
+##########################################################
+# helper functions
+#
+# dual firewall (ipv4+ipv6) wrapper
+run() {
+ # by default run command for ipv4 and ipv6 firewall
+ local commands="/sbin/iptables /sbin/ip6tables";
+
+ # override to specify which firewall
+ if [[ "-4" == "${1}" ]]; then
+ commands="/sbin/iptables"
+ shift
+ elif [[ "-6" == "${1}" ]]; then
+ commands="/sbin/ip6tables"
+ shift
+ fi
+
+ local iptables_args="$1"
+ for cmd in $commands; do
+ [[ "$cmd" == "/sbin/ip6tables" ]] && iptables_args=$(echo "$iptables_args" | to6)
+ echo "${cmd} ${iptables_args}"
+ $($cmd $iptables_args)
+ done
+}
+
+# replace IPv4 specific bits with IPv6 bits
+to6() {
+ declare -A mapping
+ # add any additional ipv4 -> ipv6 replacements to the following array
+ mapping=(
+ ["icmp-port-unreachable"]="icmp6-port-unreachable"
+ ["icmp-proto-unreachable"]="icmp6-adm-prohibited"
+ )
+
+ # build a list of sed expressions
+ local expressions=()
+ for k in "${!mapping[@]}"; do
+ expressions+=("-e s/${k}/${mapping[$k]}/g")
+ done
+
+ while read -r line_in; do
+ sed "${expressions[@]}" <<< $line_in
+ done
+}
+
+# helper to open an array of ports
+# example:
+#
+# ports=(80 443)
+# openports -4 TCP 127.0.0.2 ports
+openports() {
+ local ipv="${1}"
+ local proto="${2}"
+ local address="${3}"
+ declare -n ports=$4
+
+ local _protochain="-A TCP -p tcp"
+ [[ "UDP" == "${proto}" ]] && _protochain="-A UDP -p udp"
+
+ for port in "${ports[@]}"; do
+ run "${ipv}" "${_protochain} -d ${address} --dport ${port} -j ACCEPT"
+ done
+}
+
+# Flush and allow all
+run "-F"
+run "-X"
+run "-t nat -F"
+run "-t nat -X"
+run "-t mangle -F"
+run "-t mangle -X"
+run "-P INPUT ACCEPT"
+run "-P FORWARD ACCEPT"
+run "-P OUTPUT ACCEPT"
+
+# Chains we'll be using
+run "-N TCP"
+run "-N UDP"
+run -4 "-N natchain"
+run -4 "-N natforwarding"
+
+# Establish default policies
+run "-P FORWARD DROP"
+run "-P OUTPUT ACCEPT"
+run "-P INPUT DROP"
+
+# allow already established (passed firewall inspection) traffic right away
+run "-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT"
+
+# Trusted interfaces
+run "-A INPUT -i lo -j ACCEPT"
+run "-A INPUT -i tun0 -j ACCEPT"
+
+# allow ICMPv6 neighbor discovery
+# We do this because ICMPv6 remain untracked and get classified as invalid
+# which would be dropped by the rule following
+run "-A INPUT -p 41 -j ACCEPT"
+
+# Drop "invalid" packets right away
+run "-A INPUT -m conntrack --ctstate INVALID -j DROP"
+
+if $allow_icmp_ping; then
+ run -4 "-A INPUT -p icmp --icmp-type 8 -m conntrack --ctstate NEW -j ACCEPT"
+ run -6 "-A INPUT -p ipv6-icmp -j ACCEPT"
+fi
+
+# append any new traffic onto our UDP/TCP chains for further analysis
+run "-A INPUT -p udp -m conntrack --ctstate NEW -j UDP"
+run "-A INPUT -p tcp --syn -m conntrack --ctstate NEW -j TCP"
+
+# Traffic does not match UDP/TCP chains, reject with icmp-port-unreachable / tcp-rst packets
+run "-A INPUT -p udp -j REJECT --reject-with icmp-port-unreachable"
+run "-A INPUT -p tcp -j REJECT --reject-with tcp-reset"
+run "-A INPUT -m limit --limit 10/min -j LOG --log-level 7"
+run "-A INPUT -j REJECT --reject-with icmp-proto-unreachable"
+
+###########################################################################
+# Open defined ports
+openports -4 TCP $public_ipv4 public_services_tcp
+openports -4 UDP $public_ipv4 public_services_udp
+openports -6 TCP $public_ipv6 public_services_tcp
+openports -6 UDP $public_ipv6 public_services_udp
+
+openports -4 TCP $private_ipv4 private_services_tcp
+openports -4 UDP $private_ipv4 private_services_udp
+openports -6 TCP $private_ipv6 private_services_tcp
+openports -6 UDP $private_ipv6 private_services_udp
+
+# port scan trickery
+# https://wiki.archlinux.org/index.php/Simple_Stateful_Firewall#Tricking_port_scanners
+run "-I TCP -p tcp -m recent --update --seconds 60 --name TCP-PORTSCAN -j REJECT --reject-with tcp-reset"
+run "-D INPUT -p tcp -j REJECT --reject-with tcp-reset"
+run "-A INPUT -p tcp -m recent --set --name TCP-PORTSCAN -j REJECT --reject-with tcp-reset"
+run "-I UDP -p udp -m recent --update --seconds 60 --name UDP-PORTSCAN -j REJECT --reject-with icmp-port-unreachable"
+run "-D INPUT -p udp -j REJECT --reject-with icmp-port-unreachable"
+run "-A INPUT -p udp -m recent --set --name UDP-PORTSCAN -j REJECT --reject-with icmp-port-unreachable"
+
+# Anything past here is not UDP or TCP, block it.
+# delete and readd to ensure it's last on the chain
+run "-D INPUT -j REJECT --reject-with icmp-proto-unreachable"
+run "-A INPUT -j REJECT --reject-with icmp-proto-unreachable"
+
+# IPv4 NAT for VPN clients
+run -4 "-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT"
+run -4 "-A FORWARD -j natchain"
+run -4 "-A FORWARD -j natforwarding"
+run -4 "-A FORWARD -j REJECT --reject-with icmp-host-unreach"
+run -4 "-P FORWARD DROP"
+run -4 "-A natchain -i ${vpn_interface} -o ${vpn_interface} -j DROP" # drop client-to-client traffic
+run -4 "-A natchain -i ${vpn_interface} -j ACCEPT"
+run -4 "-t nat -A POSTROUTING -s ${vpn_subnet} -o eth0 -j MASQUERADE"