diff --git a/Dockerfile b/Dockerfile index 4ddcf06f..aeb112c6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,60 @@ +FROM alpine:3.8 AS rootanchors +RUN apk add -q --progress wget perl-xml-xpath && \ + wget -q https://www.internic.net/domain/named.root -O named.root && \ + echo "602f28581292bf5e50c8137c955173e6 named.root" > hashes.md5 && \ + md5sum -c hashes.md5 && \ + wget -q https://data.iana.org/root-anchors/root-anchors.xml -O root-anchors.xml && \ + echo "1b2a628d1ff22d4dc7645cfc89f21b6a575526439c6706ecf853e6fff7099dc8 root-anchors.xml" > hashes.sha256 && \ + sha256sum -c hashes.sha256 && \ + KEYTAGS=$(xpath -q -e '/TrustAnchor/KeyDigest/KeyTag/node()' root-anchors.xml) && \ + ALGORITHMS=$(xpath -q -e '/TrustAnchor/KeyDigest/Algorithm/node()' root-anchors.xml) && \ + DIGESTTYPES=$(xpath -q -e '/TrustAnchor/KeyDigest/DigestType/node()' root-anchors.xml) && \ + DIGESTS=$(xpath -q -e '/TrustAnchor/KeyDigest/Digest/node()' root-anchors.xml) && \ + i=1 && \ + while [ 1 ]; do \ + KEYTAG=$(echo $KEYTAGS | cut -d" " -f$i); \ + [ "$KEYTAG" != "" ] || break; \ + ALGORITHM=$(echo $ALGORITHMS | cut -d" " -f$i); \ + DIGESTTYPE=$(echo $DIGESTTYPES | cut -d" " -f$i); \ + DIGEST=$(echo $DIGESTS | cut -d" " -f$i); \ + echo ". IN DS $KEYTAG $ALGORITHM $DIGESTTYPE $DIGEST" >> /root.key; \ + i=`expr $i + 1`; \ + done; + +FROM alpine:3.8 AS blocks +RUN apk add -q --progress wget && \ + wget -q https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts -O temp && \ + sed -i '/\(^[ \|\t]*#\)\|\(^[ ]\+\)\|\(^$\)\|\(^[\n\|\r\|\r\n][ \|\t]*$\)\|\(^127.0.0.1\)\|\(^255.255.255.255\)\|\(^::1\)\|\(^fe80\)\|\(^ff00\)\|\(^ff02\)\|\(^0.0.0.0 0.0.0.0\)/d' temp && \ + sed -i 's/\([ \|\t]*#.*$\)\|\(\r\)\|\(0.0.0.0 \)//g' temp && \ + cat temp >> allHostnames && \ + wget -q https://raw.githubusercontent.com/CHEF-KOCH/NSABlocklist/master/HOSTS -O temp && \ + sed -i '/\(^[ \|\t]*#\)\|\(^[ ]\+\)\|\(^$\)\|\(^[\n\|\r\|\r\n][ \|\t]*$\)\|\(^127.0.0.1\)/d' temp && \ + sed -i 's/\([ \|\t]*#.*$\)\|\(\r\)\|\(0.0.0.0 \)//g' temp && \ + cat temp >> allHostnames && \ + wget -q https://raw.githubusercontent.com/k0nsl/unbound-blocklist/master/blocks.conf -O temp && \ + sed -i '/\(^[ \|\t]*#\)\|\(^[ ]\+\)\|\(^$\)\|\(^[\n\|\r\|\r\n][ \|\t]*$\)\|\(^local-data\)/d' temp && \ + sed -i 's/\([ \|\t]*#.*$\)\|\(\r\)\|\(local-zone: \"\)\|\(\" redirect\)//g' temp && \ + cat temp >> allHostnames && \ + wget -q https://raw.githubusercontent.com/notracking/hosts-blocklists/master/domains.txt -O temp && \ + sed -i '/\(^[ \|\t]*#\)\|\(^[ ]\+\)\|\(^$\)\|\(^[\n\|\r\|\r\n][ \|\t]*$\)\|\(::$\)/d' temp && \ + sed -i 's/\([ \|\t]*#.*$\)\|\(\r\)\|\(address=\/\)\|\(\/0.0.0.0$\)//g' temp && \ + cat temp >> allHostnames && \ + wget -q https://raw.githubusercontent.com/notracking/hosts-blocklists/master/hostnames.txt -O temp && \ + sed -i '/\(^[ \|\t]*#\)\|\(^[ ]\+\)\|\(^$\)\|\(^[\n\|\r\|\r\n][ \|\t]*$\)\|\(^::\)/d' temp && \ + sed -i 's/\([ \|\t]*#.*$\)\|\(\r\)\|\(^0.0.0.0 \)//g' temp && \ + cat temp >> allHostnames && \ + sort -o allHostnames allHostnames && \ + cat allHostnames | uniq -i -u > uniqueHostnames && \ + cat allHostnames | uniq -i -d > duplicateHostnames && \ + duplicates=$(($(wc -l < allHostnames)-$(wc -l < uniqueHostnames)-$(wc -l < duplicateHostnames))) && \ + echo "Removed $duplicates duplicates in a total of $(wc -l < allHostnames) hostnames" && \ + mv uniqueHostnames allHostnames && \ + cat duplicateHostnames >> allHostnames && \ + sort -o allHostnames allHostnames && \ + sed -i '/\(psma01.com.\)\|\(psma02.com.\)\|\(psma03.com.\)\|\(MEZIAMUSSUCEMAQUEUE.SU\)/d' allHostnames && \ + while read line; do printf "local-zone: \"$line\" static\n" >> blocks-malicious.conf; done < allHostnames && \ + tar -cjf blocks-malicious.conf.bz2 blocks-malicious.conf + FROM alpine:3.8 LABEL maintainer="quentin.mcgaw@gmail.com" \ description="VPN client to private internet access servers using OpenVPN, IPtables firewall, DNS over TLS with Unbound and Alpine Linux" \ @@ -6,14 +63,16 @@ LABEL maintainer="quentin.mcgaw@gmail.com" \ ram="13MB" \ cpu_usage="Low" \ github="https://github.com/qdm12/private-internet-access-docker" -HEALTHCHECK --interval=1m --timeout=10s --start-period=10s --retries=1 \ - CMD export OLD_VPN_IP="$NEW_VPN_IP" && \ - export NEW_VPN_IP=$(wget -qqO- 'https://duckduckgo.com/?q=what+is+my+ip' | grep -ow 'Your IP address is [0-9.]*[0-9]' | grep -ow '[0-9][0-9.]*') && \ - [ "$NEW_VPN_IP" != "$INITIAL_IP" ] && [ "$NEW_VPN_IP" != "$OLD_VPN_IP" ] || exit 1 +COPY --from=rootanchors /named.root /etc/unbound/root.hints +COPY --from=rootanchors /root.key /etc/unbound/root.key +COPY --from=blocks /blocks-malicious.conf.bz2 /etc/unbound/blocks-malicious.conf.bz2 +HEALTHCHECK --interval=5m --timeout=15s --start-period=10s --retries=2 \ + CMD if [[ "$(wget -qqO- 'https://duckduckgo.com/?q=what+is+my+ip' | grep -ow 'Your IP address is [0-9.]*[0-9]' | grep -ow '[0-9][0-9.]*')" == "$INITIAL_IP" ]]; then echo "IP address is the same as the non VPN IP address"; exit 1; fi ENV ENCRYPTION=strong \ PROTOCOL=tcp \ - REGION=Germany -RUN apk add -q --progress --no-cache --update openvpn ca-certificates iptables ip6tables unbound && \ + REGION=Germany \ + BLOCK_MALICIOUS=off +RUN apk add -q --progress --no-cache --update openvpn ca-certificates iptables unbound && \ apk add -q --progress --no-cache --update --virtual=build-dependencies unzip && \ mkdir /openvpn-udp-normal /openvpn-udp-strong /openvpn-tcp-normal /openvpn-tcp-strong && \ wget -q https://www.privateinternetaccess.com/openvpn/openvpn.zip \ @@ -26,7 +85,8 @@ RUN apk add -q --progress --no-cache --update openvpn ca-certificates iptables i unzip -q openvpn-strong-tcp.zip -d /openvpn-tcp-strong && \ apk del -q --progress --purge build-dependencies && \ rm -rf /*.zip /var/cache/apk/* /etc/unbound/unbound.conf && \ - addgroup -S nonrootusers && adduser -S nonrootuser -G nonrootusers + chown unbound /etc/unbound/root.key COPY unbound.conf /etc/unbound/unbound.conf COPY entrypoint.sh / -ENTRYPOINT /entrypoint.sh \ No newline at end of file +RUN chmod +x /entrypoint.sh +ENTRYPOINT /entrypoint.sh diff --git a/entrypoint.sh b/entrypoint.sh index 63056516..1757aea6 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -19,14 +19,30 @@ do done printf "\nTUN device is opened" +############################################ +# BLOCKING MALICIOUS HOSTS WITH UNBOUND +############################################ +touch /etc/unbound/blocks-malicious.conf +printf "\nUnbound malicious hosts blocking is $BLOCK_MALICIOUS" +if [[ "$BLOCK_MALICIOUS" == "on" ]]; then + printf "\nExtracting blocks-malicious.conf.bz2..." + tar -xjf /etc/unbound/blocks-malicious.conf.bz2 -C /etc/unbound/ + rm /etc/unbound/blocks-malicious.conf.bz2 + printf "DONE" +fi + ############################################ # SETTING DNS OVER TLS TO 1.1.1.1 / 1.0.0.1 ############################################ printf "\nLaunching Unbound daemon to connect to Cloudflare DNS 1.1.1.1 at its TLS endpoint..." unbound +status=$? +if [[ "$status" != 0 ]]; then printf "ERROR with status code $status\nSleeping for 10 seconds before exit...\n"; sleep 10; exit $status; fi printf "DONE" printf "\nChanging DNS to localhost..." echo "nameserver 127.0.0.1" > /etc/resolv.conf +status=$? +if [[ "$status" != 0 ]]; then printf "ERROR with status code $status\nSleeping for 10 seconds before exit...\n"; sleep 10; exit $status; fi echo "options ndots:0" >> /etc/resolv.conf printf "DONE" @@ -35,6 +51,8 @@ printf "DONE" ############################################ printf "\nGetting non VPN public IP address..." export INITIAL_IP=$(wget -qqO- 'https://duckduckgo.com/?q=what+is+my+ip' | grep -ow 'Your IP address is [0-9.]*[0-9]' | grep -ow '[0-9][0-9.]*') +status=$? +if [[ "$status" != 0 ]]; then printf "ERROR with status code $status\nSleeping for 10 seconds before exit...\n"; sleep 10; exit $status; fi printf "$INITIAL_IP" ############################################ @@ -43,15 +61,23 @@ printf "$INITIAL_IP" printf "\nSetting firewall for killswitch purposes..." printf "\n * Detecting local subnet..." SUBNET=$(ip route show default | tail -n 1 | awk '// {print $1}') +status=$? +if [[ "$status" != 0 ]]; then printf "ERROR with status code $status\nSleeping for 10 seconds before exit...\n"; sleep 10; exit $status; fi printf "$SUBNET" printf "\n * Reading parameters to be used for region $REGION, protocol $PROTOCOL and encryption $ENCRYPTION..." CONNECTIONSTRING=$(grep -i "/openvpn-$PROTOCOL-$ENCRYPTION/$REGION.ovpn" -e 'privateinternetaccess.com') +status=$? +if [[ "$status" != 0 ]]; then printf "ERROR with status code $status\nSleeping for 10 seconds before exit...\n"; sleep 10; exit $status; fi PORT=$(echo $CONNECTIONSTRING | cut -d' ' -f3) +if [[ "$PORT" == "" ]]; then printf "Port could not be extracted from configuration file\n"; exit 1; fi PIADOMAIN=$(echo $CONNECTIONSTRING | cut -d' ' -f2) +if [[ "$PIADOMAIN" == "" ]]; then printf "Port could not be extracted from configuration file\n"; exit 1; fi printf "\n * Port: $PORT" printf "\n * Domain: $PIADOMAIN" printf "\n * Detecting IP addresses corresponding to $PIADOMAIN..." VPNIPS=$(nslookup $PIADOMAIN localhost | tail -n +5 | grep -o '[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}') +status=$? +if [[ "$status" != 0 ]]; then printf "ERROR with status code $status\nSleeping for 10 seconds before exit...\n"; sleep 10; exit $status; fi VPNIPSLENGTH=0 for ip in $VPNIPS do @@ -60,47 +86,39 @@ do done printf "\n * Deleting all iptables rules..." iptables --flush +status=$? +if [[ "$status" != 0 ]]; then printf "ERROR with status code $status\nSleeping for 10 seconds before exit...\n"; sleep 10; exit $status; fi iptables --delete-chain +status=$? +if [[ "$status" != 0 ]]; then printf "ERROR with status code $status\nSleeping for 10 seconds before exit...\n"; sleep 10; exit $status; fi iptables -t nat --flush +status=$? +if [[ "$status" != 0 ]]; then printf "ERROR with status code $status\nSleeping for 10 seconds before exit...\n"; sleep 10; exit $status; fi iptables -t nat --delete-chain -ip6tables --flush -ip6tables --delete-chain +status=$? +if [[ "$status" != 0 ]]; then printf "ERROR with status code $status\nSleeping for 10 seconds before exit...\n"; sleep 10; exit $status; fi printf "DONE" iptables -F OUTPUT iptables -P OUTPUT DROP -ip6tables -F OUTPUT 2>/dev/null -ip6tables -P OUTPUT DROP 2>/dev/null printf "\n * Adding rules to accept local loopback traffic..." iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT iptables -A OUTPUT -o lo -j ACCEPT -ip6tables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT 2>/dev/null -ip6tables -A OUTPUT -o lo -j ACCEPT 2>/dev/null printf "DONE" printf "\n * Adding rules to accept traffic of subnet $SUBNET..." iptables -A OUTPUT -d $SUBNET -j ACCEPT -ip6tables -A OUTPUT -d $SUBNET -j ACCEPT 2>/dev/null +status=$? +if [[ "$status" != 0 ]]; then printf "ERROR with status code $status\nSleeping for 10 seconds before exit...\n"; sleep 10; exit $status; fi printf "DONE" for ip in $VPNIPS do printf "\n * Adding rules to accept traffic with $ip on port $PROTOCOL $PORT..." iptables -A OUTPUT -j ACCEPT -d $ip -o eth0 -p $PROTOCOL -m $PROTOCOL --dport $PORT - ip6tables -A OUTPUT -j ACCEPT -d $ip -o eth0 -p $PROTOCOL -m $PROTOCOL --dport $PORT 2>/dev/null + status=$? + if [[ "$status" != 0 ]]; then printf "ERROR with status code $status\nSleeping for 10 seconds before exit...\n"; sleep 10; exit $status; fi printf "DONE" done printf "\n * Adding rules to accept traffic going through the tun device..." iptables -A OUTPUT -o tun0 -j ACCEPT -ip6tables -A OUTPUT -o tun0 -j ACCEPT 2>/dev/null -printf "DONE" -#printf "\n * Allowing outgoing DNS queries on port 53 UDP..." -#iptables -A OUTPUT -p udp -m udp --dport 53 -j ACCEPT -#ip6tables -A OUTPUT -p udp -m udp --dport 53 -j ACCEPT 2>/dev/null -#printf "DONE" - -############################################ -# USE NON-ROOT USER -############################################ -printf "\nSwitching from root to nonrootuser..." -su -l nonrootuser printf "DONE" ############################################ @@ -112,7 +130,10 @@ PREVIOUSIP=$PIADOMAIN while [ $failed != 0 ] do VPNIP=$(echo $VPNIPS | cut -d' ' -f$i) + printf "\nChanging server VPN address $PREVIOUSIP to $VPNIP..." sed -i "s/$PREVIOUSIP/$VPNIP/g" $REGION.ovpn + status=$? + if [[ "$status" != 0 ]]; then printf "ERROR with status code $status\nSleeping for 10 seconds before exit...\n"; sleep 10; exit $status; fi PREVIOUSIP=$VPNIP printf "\nStarting OpenVPN using the following parameters:" printf "\n * Region: $REGION" @@ -122,12 +143,12 @@ do openvpn --config "$REGION.ovpn" --auth-user-pass /auth.conf failed=$? if [[ $failed != 0 ]]; then - echo "==> Openvpn failed with error code: $failed" + printf "\n==> Openvpn failed with status code: $failed" i=$((i+1)) if [[ $i -gt $VPNIPSLENGTH ]]; then i=0 fi else - echo "==> Openvpn stopped cleanly" + printf "\n==> Openvpn stopped gracefully" fi done diff --git a/unbound.conf b/unbound.conf index 8be3479f..924660cf 100644 --- a/unbound.conf +++ b/unbound.conf @@ -1,18 +1,45 @@ server: + # See https://www.nlnetlabs.nl/documentation/unbound/unbound.conf/ + # logging verbosity: 0 - use-syslog: yes - qname-minimisation: yes - do-tcp: yes + val-log-level: 2 + use-syslog: no + + # performance + num-threads: 1 prefetch: yes + prefetch-key: yes + key-cache-size: 16m + key-cache-slabs: 4 + msg-cache-size: 4m + msg-cache-slabs: 4 + rrset-cache-size: 4m + rrset-cache-slabs: 4 + cache-min-ttl: 3600 + cache-max-ttl: 9000 + + # privacy rrset-roundrobin: yes - use-caps-for-id: yes + hide-identity: yes + hide-version: yes + + # security + root-hints: "/etc/unbound/root.hints" + trust-anchor-file: "/etc/unbound/root.key" + harden-below-nxdomain: yes + harden-referral-path: yes + harden-algo-downgrade: yes + # set above to no if there is any problem + + # network do-ip4: yes do-ip6: no interface: 127.0.0.1 - hide-identity: yes - hide-version: yes + + # other files + include: "/etc/unbound/blocks-malicious.conf" forward-zone: name: "." forward-addr: 1.1.1.1@853#cloudflare-dns.com forward-addr: 1.0.0.1@853#cloudflare-dns.com - forward-ssl-upstream: yes + forward-tls-upstream: yes