diff options
-rw-r--r-- | LICENSE | 21 | ||||
-rw-r--r-- | README.md | 24 | ||||
-rwxr-xr-x | firewall.sh | 176 |
3 files changed, 221 insertions, 0 deletions
@@ -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" |