diff --git a/Dockerfile b/Dockerfile index 9858ad7e..b7ec166d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ LABEL org.label-schema.schema-version="1.0.0-rc1" \ org.label-schema.vcs-usage="https://github.com/qdm12/private-internet-access-docker/blob/master/README.md#setup" \ org.label-schema.docker.cmd="docker run -d -v ./auth.conf:/auth.conf:ro --cap-add=NET_ADMIN --device=/dev/net/tun qmcgaw/private-internet-access" \ org.label-schema.docker.cmd.devel="docker run -it --rm -v ./auth.conf:/auth.conf:ro --cap-add=NET_ADMIN --device=/dev/net/tun qmcgaw/private-internet-access" \ - org.label-schema.docker.params="" \ + org.label-schema.docker.params="REGION=PIA region,PROTOCOL=udp or tcp,ENCRYPTION=strong or normal,BLOCK_MALICIOUS=on or off" \ org.label-schema.version="" \ image-size="17.1MB" \ ram-usage="13MB to 80MB" \ @@ -23,7 +23,7 @@ ENV ENCRYPTION=strong \ REGION="CA Montreal" \ BLOCK_MALICIOUS=off 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 + CMD [ "$(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" ] || exit 1 RUN V_ALPINE="v$(cat /etc/alpine-release | grep -oE '[0-9]+\.[0-9]+')" && \ echo https://dl-3.alpinelinux.org/alpine/$V_ALPINE/main > /etc/apk/repositories && \ apk add -q --progress --no-cache --update openvpn wget ca-certificates iptables unbound unzip && \ diff --git a/README.md b/README.md index 005045ae..26eee136 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,6 @@ # Private Internet Access Client (OpenVPN+Iptables+DNS over TLS on Alpine Linux) -*VPN client to tunnel to private internet access servers using OpenVPN, IPtables, DNS over TLS and Alpine Linux* - -Optionally set the protocol (TCP, UDP) and the level of encryption using Docker environment variables. - -A killswitch is implemented with the *iptables* firewall, only allowing traffic with PIA servers on needed ports / protocols. +*Lightweight VPN client to tunnel to private internet access servers* [![PIA Docker OpenVPN](https://github.com/qdm12/private-internet-access-docker/raw/master/readme/title.png)](https://hub.docker.com/r/qmcgaw/private-internet-access/) @@ -30,16 +26,21 @@ It is based on: - [Alpine 3.8](https://alpinelinux.org) for a tiny image - [OpenVPN 2.4.6-r3](https://pkgs.alpinelinux.org/package/v3.8/main/x86_64/openvpn) to tunnel to PIA servers -- [IPtables 1.6.2-r0](https://pkgs.alpinelinux.org/package/v3.8/main/x86_64/iptables) enforces the container to communicate only through the VPN or with other containers in its virtual network (killswitch) +- [IPtables 1.6.2-r0](https://pkgs.alpinelinux.org/package/v3.8/main/x86_64/iptables) enforces the container to communicate only through the VPN or with other containers in its virtual network (acts as a killswitch) - [Unbound 1.7.3-r0](https://pkgs.alpinelinux.org/package/v3.8/main/x86_64/unbound) configured with Cloudflare's [1.1.1.1](https://1.1.1.1) DNS over TLS - [Malicious hostnames list](https://github.com/qdm12/malicious-hostnames-docker) used with Unbound (see `BLOCK_MALICIOUS` environment variable) - [Malicious IPs list](https://github.com/qdm12/malicious-ips-docker) used with Unbound (see `BLOCK_MALICIOUS`) ## Extra features +- With environment variables, choose: + - the PIA region + - the protocol `TCP` or `UDP` + - the level of encryption - Connect other containers to it -- Restarts OpenVPN on failure using another IP address corresponding to the PIA server domain name (usually 10 IPs per subdomain name) -- Regular Docker healthchecks using [duckduckgo.com](https://duckduckgo.com) to obtain your current public IP address and compare it with your initial non-VPN IP address +- The *iptables* firewall allows traffic only with needed PIA servers (IP addresses, port, protocol) combination +- OpenVPN restarts on failure using another PIA IP address in the same region +- Docker healthchecks using [duckduckgo.com](https://duckduckgo.com) to obtain your public IP address and compare it with your initial non-VPN IP address - Openvpn and Unbound do not run as root ## Requirements @@ -111,7 +112,7 @@ You can simply use the Docker healthcheck. The container will mark itself as **u wget -qO- https://ipinfo.io/ip ``` -1. Run the **curl** Docker container using your *pia* container with: +1. Run the same command in a Docker container using your *pia* container as network with: ```bash docker run --rm --network=container:pia alpine:3.8 wget -qO- https://ipinfo.io/ip @@ -214,10 +215,11 @@ For more containers, add more `--link pia:xxx` and modify *nginx.conf* according ## TODOs +- [ ] Test pia with port mappings and without pia_net and nginx - [ ] Iptables should change after initial ip address is obtained - [ ] More checks for environment variables provided - [ ] Add checks when launching PIA $? -- [ ] VPN server for other devices to go through the tunnel +- [ ] VPN server for other devices to go through the tunnel OR hiproxy ## License diff --git a/docker-compose.yml b/docker-compose.yml index 033cdfb4..27d97f9c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: '3' services: pia: - build: . + build: https://github.com/qdm12/private-internet-access-docker.git image: qmcgaw/private-internet-access container_name: pia cap_add: @@ -17,7 +17,7 @@ services: - ENCRYPTION=strong - REGION=CA Montreal restart: always - + networks: pianet: external: true diff --git a/entrypoint.sh b/entrypoint.sh index a56b0c11..0aafbdc3 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,49 +1,66 @@ #!/bin/sh -printf "\n =========================================" -printf "\n =========================================" -printf "\n ============= PIA CONTAINER =============" -printf "\n =========================================" -printf "\n =========================================" -printf "\n == by github.com/qdm12 - Quentin McGaw ==\n" +exitOnError(){ + # $1 should be $? + status=$1 + message=$2 + [ "$message" != "" ] || message="Error!" + if [ $status != 0 ]; then + printf "$message (status $status)\n" + exit $status + fi +} -printf "\nOpenVPN version: $(openvpn --version | head -n 1 | grep -oE "OpenVPN [0-9\.]* " | cut -d" " -f2)" -printf "\nUnbound version: $(unbound -h | grep "Version" | cut -d" " -f2)" -printf "\nIptables version: $(iptables --version | cut -d" " -f2)" +printf "\n =========================================\n" +printf "=========================================\n" +printf "============= PIA CONTAINER =============\n" +printf "=========================================\n" +printf "=========================================\n" +printf "== by github.com/qdm12 - Quentin McGaw ==\n\n" + +printf "OpenVPN version: $(openvpn --version | head -n 1 | grep -oE "OpenVPN [0-9\.]* " | cut -d" " -f2)\n" +printf "Unbound version: $(unbound -h | grep "Version" | cut -d" " -f2)\n" +printf "Iptables version: $(iptables --version | cut -d" " -f2)\n" ############################################ # CHECK PARAMETERS ############################################ cat "/openvpn-$PROTOCOL-$ENCRYPTION/$REGION.ovpn" &> /dev/null -if [[ "$?" != 0 ]]; then printf "/openvpn-$PROTOCOL-$ENCRYPTION/$REGION.ovpn is not accessible\nSleeping for 10 seconds before exit...\n"; sleep 10; exit 1; fi +exitOnError $? "/openvpn-$PROTOCOL-$ENCRYPTION/$REGION.ovpn is not accessible" # TODO more ############################################ # CHECK FOR TUN DEVICE ############################################ -while [ "$(cat /dev/net/tun 2>&1 /dev/null)" != "cat: read error: File descriptor in bad state" ]; -do printf "\nTUN device is not opened, sleeping for 30 seconds..."; sleep 30; done -printf "\nTUN device is opened" +while [ "$(cat /dev/net/tun 2>&1 /dev/null)" != "cat: read error: File descriptor in bad state" ]; do + printf "TUN device is not available, sleeping for 30 seconds...\n" + sleep 30 +done +printf "TUN device OK\n" ############################################ # BLOCKING MALICIOUS HOSTNAMES AND IPs WITH UNBOUND ############################################ touch /etc/unbound/blocks-malicious.conf -printf "\nUnbound malicious hostnames blocking is $BLOCK_MALICIOUS" +printf "Unbound malicious hostnames blocking is $BLOCK_MALICIOUS\n" if [ "$BLOCK_MALICIOUS" = "on" ] && [ ! -f /etc/unbound/blocks-malicious.conf ]; then printf "Extracting malicious hostnames archive..." tar -xjf /etc/unbound/malicious-hostnames.bz2 -C /etc/unbound/ + exitOnError $? printf "DONE\n" printf "Extracting malicious IPs archive..." tar -xjf /etc/unbound/malicious-ips.bz2 -C /etc/unbound/ + exitOnError $? printf "DONE\n" printf "Building blocks-malicious.conf for Unbound..." while read hostname; do echo "local-zone: \""$hostname"\" static" >> /etc/unbound/blocks-malicious.conf done < /etc/unbound/malicious-hostnames + exitOnError $? while read ip; do echo "private-address: $ip" >> /etc/unbound/blocks-malicious.conf done < /etc/unbound/malicious-ips + exitOnError $? printf "$(cat /etc/unbound/malicious-hostnames | wc -l ) malicious hostnames and $(cat /etc/unbound/malicious-ips | wc -l) malicious IP addresses added\n" rm -f /etc/unbound/malicious-hostnames* /etc/unbound/malicious-ips* else @@ -53,129 +70,122 @@ 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..." +printf "Launching Unbound daemon to connect to Cloudflare DNS 1.1.1.1 at its TLS endpoint...\n" 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..." +exitOnError $? +printf "DONE\n" +printf "Changing 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 +exitOnError $? echo "options ndots:0" >> /etc/resolv.conf -printf "DONE" +exitOnError $? +printf "DONE\n" ############################################ # ORIGINAL IP FOR HEALTHCHECK ############################################ -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" +printf "Getting non VPN public IP address..." +export INITIAL_IP=$(wget -qO- 'https://duckduckgo.com/?q=what+is+my+ip' | grep -o 'Your IP address is [0-9.]*[0-9]' | grep -o '[0-9][0-9.]*') +exitOnError $? +printf "$INITIAL_IP\n" ############################################ # FIREWALL ############################################ -printf "\nSetting firewall for killswitch purposes..." -printf "\n * Detecting local subnet..." +printf "Setting firewall for killswitch purposes...\n" +printf " * 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..." +exitOnError $? +printf "$SUBNET\n" +printf " * 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 +exitOnError $? PORT=$(echo $CONNECTIONSTRING | cut -d' ' -f3) -if [[ "$PORT" == "" ]]; then printf "Port could not be extracted from configuration file\n"; exit 1; fi +if [ "$PORT" = "" ]; then + printf "Port not found in /openvpn-$PROTOCOL-$ENCRYPTION/$REGION.ovpn\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 -sed -i '/^remote $PIADOMAIN $PORT/d' "/openvpn-$PROTOCOL-$ENCRYPTION/$REGION.ovpn" && \ -printf "\n * Port: $PORT" -printf "\n * Domain: $PIADOMAIN" -printf "\n * Detecting IP addresses corresponding to $PIADOMAIN..." +if [ "$PIADOMAIN" = "" ]; then + printf "Domain not found in /openvpn-$PROTOCOL-$ENCRYPTION/$REGION.ovpn\n" + exit 1 +fi +sed -i '/^remote $PIADOMAIN $PORT/d' "/openvpn-$PROTOCOL-$ENCRYPTION/$REGION.ovpn" +exitOnError $? "Can't delete remote connection string in /openvpn-$PROTOCOL-$ENCRYPTION/$REGION.ovpn" +printf "DONE\n" +printf " * Port: $PORT\n" +printf " * Domain: $PIADOMAIN\n" +printf " * 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 -for ip in $VPNIPS -do - printf "\n $ip" +exitOnError $? +printf "DONE\n" +for ip in $VPNIPS; do printf " $ip\n"; done +printf " * Adding IP addresses of $PIADOMAIN to /openvpn-$PROTOCOL-$ENCRYPTION/$REGION.ovpn...\n" +for ip in $VPNIPS; do + if [ $(grep "remote $ip $PORT" "/openvpn-$PROTOCOL-$ENCRYPTION/$REGION.ovpn") != "" ]; then + printf " remote $ip $PORT (already present)\n" + else + printf " remote $ip $PORT\n" + echo "remote $ip $PORT" >> "/openvpn-$PROTOCOL-$ENCRYPTION/$REGION.ovpn" + fi done -printf "\n * Adding IP addresses of $PIADOMAIN to /openvpn-$PROTOCOL-$ENCRYPTION/$REGION.ovpn..." -for ip in $VPNIPS -do - printf "\n remote $ip $PORT" - grep "remote $ip $PORT" "/openvpn-$PROTOCOL-$ENCRYPTION/$REGION.ovpn" || echo "remote $ip $PORT" >> "/openvpn-$PROTOCOL-$ENCRYPTION/$REGION.ovpn" -done -printf "\n * Deleting all iptables rules..." +printf " * 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 +exitOnError $? 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 +exitOnError $? 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 +exitOnError $? iptables -t nat --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" -printf "\n * Blocking all output traffic..." +exitOnError $? +printf "DONE\n" +printf " * Blocking all output traffic..." iptables -F OUTPUT -status=$? -if [[ "$status" != 0 ]]; then printf "ERROR with status code $status\nSleeping for 10 seconds before exit...\n"; sleep 10; exit $status; fi +exitOnError $? iptables -P OUTPUT DROP -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 "\n * Adding rules to accept local loopback traffic..." +exitOnError $? +printf "DONE\n" +printf " * Adding rules to accept local loopback traffic..." iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT -status=$? -if [[ "$status" != 0 ]]; then printf "ERROR with status code $status\nSleeping for 10 seconds before exit...\n"; sleep 10; exit $status; fi +exitOnError $? iptables -A OUTPUT -o lo -j ACCEPT -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 "\n * Adding rules to accept traffic of subnet $SUBNET..." +exitOnError $? +printf "DONE\n" +printf " * Adding rules to accept traffic of subnet $SUBNET..." iptables -A OUTPUT -d $SUBNET -j ACCEPT -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..." +exitOnError $? +printf "DONE\n" +for ip in $VPNIPS; do + printf " * 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 - 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" + exitOnError $? + printf "DONE\n" done -printf "\n * Adding rules to accept traffic going through the tun device..." +printf " * Adding rules to accept traffic going through the tun device..." iptables -A OUTPUT -o tun0 -j ACCEPT -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" +exitOnError $? +printf "DONE\n" ############################################ # USER SECURITY ############################################ -printf "\nChanging /auth.conf ownership to nonrootuser with read only access..." +printf "Changing /auth.conf ownership to nonrootuser with read only access..." chown nonrootuser /auth.conf +exitOnError $? chmod 400 /auth.conf -printf "DONE" +exitOnError $? +printf "DONE\n" ############################################ # OPENVPN LAUNCH ############################################ -printf "\nStarting OpenVPN using the following parameters:" -printf "\n * Region: $REGION" -printf "\n * Encryption: $ENCRYPTION" -printf "\n * Protocol: $PROTOCOL" -printf "\n * Port: $PORT" -printf "\n * Initial IP address: $(echo "$VPNIPS" | head -n 1)" -printf "\n\n" +printf "Starting OpenVPN using the following parameters:\n" +printf " * Region: $REGION\n" +printf " * Encryption: $ENCRYPTION\n" +printf " * Protocol: $PROTOCOL\n" +printf " * Port: $PORT\n" +printf " * Initial IP address: $(echo "$VPNIPS" | head -n 1)\n\n" cd "/openvpn-$PROTOCOL-$ENCRYPTION" +exitOnError $? "Can't access /openvpn-$PROTOCOL-$ENCRYPTION" openvpn --config "$REGION.ovpn" --user nonrootuser --persist-tun --auth-retry nointeract --auth-user-pass /auth.conf --auth-nocache -status=$? -printf "\nOpenVPN exited with status $status\n" +printf "\nOpenVPN exited with status $?\n"