aboutsummaryrefslogtreecommitdiff
path: root/firewall.sh
blob: 763e87c500585cb70cd80437e1f2a179389f2a91 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
#!/bin/bash
declare -A public_services_tcp
declare -A public_services_udp
declare -A private_services_tcp
declare -A private_services_udp

allow_icmp_ping=true

public_ipv4="104.236.18.1/32"
public_ipv6="2604:a880::1/128"
public_services_tcp=(
    # single port: [desc]=port_number
    #  for ranges: [desc]=start:end
    [HTTP]=80
    [HTTPS]=443
)
public_services_udp=(
    [MOSH]=23563
)

# private_ipv4="1.2.3.4/32"
# private_ipv6="2001:dead:beef::/128"
# private_services_tcp=([rtorrent]="45400:45600")
# private_services_udp=([rtorrent]="45400:45600")

# vpn_interface="tun0"
# vpn_subnet="172.16.23.0/24"
# vpn_gateway="172.16.23.1/32"

###################################################################
# run [-4|-6] IPTABLES_COMMAND
# dual firewall (ipv4+ipv6) wrapper
run() {
    local commands="/sbin/iptables /sbin/ip6tables";

    # overrides to run on single 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
        if [[ "$cmd" == "/sbin/ip6tables" ]]; then
            # refuckers the iptables command to run on ip6tables
            iptables_args=$(echo "$iptables_args" | to6)
        fi

        echo "[exec] ${cmd} ${iptables_args}" # debug/logging
        $($cmd $iptables_args)
    done
}

# replace IPv4 specific bits with IPv6 bits
to6() {
    declare -A mapping
    # add any additional ipv4 -> ipv6 pattern 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"
if [[ -n $vpn_interface ]]; then
    run "-t nat -F"
    run "-t nat -X"
fi
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"
if [[ -n $vpn_interface ]]; then
    run -4 "-N natchain"
    run -4 "-N natforwarding"
fi

# 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"
[[ -n $vpn_interface ]] && run "-A INPUT -i ${vpn_interface} -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

# Disallow DHT packets now to prevent being added to the timeout box
run "-A INPUT -p udp --dport 23223 -j REJECT --reject-with icmp-port-unreachable"
run "-A INPUT -p tcp --dport 23224 -j REJECT --reject-with tcp-reset"

# 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

if [[ -n $private_ipv4 ]]; then
    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
fi

# 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"

# NAT for VPN clients
if [[ -n $vpn_interface ]]; then
    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 "-A natchain -i docker0 -j ACCEPT"
    run -4 "-t nat -A POSTROUTING -s ${vpn_subnet} -o eth0 -j MASQUERADE"
fi