diff --git a/README.md b/README.md index 9fde996..d0bc4ca 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,202 @@ -# pia-pivpn -Automation of port forwarding with PIA along with PiVPN to create a PiVPN server behind PIA +# PIA-PiVPN +Automation of port forwarding with PIA along with PiVPN to create a PiVPN server behind PIA. +In order to have both VPNs running at once without them interfering with one another, they each run using a separate protocol: +* PIA runs with OpenVPN +* PiVPN runs with WireGuard (default for PiVPN) + +# Setup + +## Prerequisites + +* `openvpn` must be installed. + +## Configure credentials + +Ensure you have your username and password stored in a text file in `/etc/openvpn/auth.txt` and format it like below: +```text +p01234567 +password123! +``` + +## First Time Setup +1. Install PiVPN +```shell +curl -L https://install.pivpn.io | bash +``` + +2. Copy `/etc/pivpn/wireguard/setupVars.conf` to your installation directory (`/home/pi/pia-pivpn/` by default) +3. Start PIA, port forwarding, and the PiVPN by running `./startup_vpn.sh` + +# Usage + +To start the PIA VPN (OpenVPN), create the port forwarding for PiVPN, and to start PiVPN (WireGuard), run `./startup_vpn.sh` + +### Note: that you will have to restart the VPN every 3 months as that's how long the port forwarding lasts!! You'll have to re-attach any devices with new creds as well! + +# Manual PIA VPN Connections + +This repository contains documentation on how to create native WireGuard and OpenVPN connections, and also on how to enable Port Forwarding in case you require this feature. You will find a lot of information below. However if you prefer quick test, here is the __TL/DR__: + +``` +git clone https://github.com/pia-foss/manual-connections.git +cd manual-connections +sudo ./run_setup.sh +``` + +The scripts were written so that they are easy to read and to modify. The code also has a lot of comments, so that you find all the information you might need. We hope you will enjoy forking the repo and customizing the scripts for your setup! + +## Table of Contents + +- [Dependencies](#dependencies) +- [Disclaimers](#disclaimers) +- [Confirmed distributions](#confirmed-distributions) +- [3rd Party Repositories](#3rd-party-repositories) +- [PIA Port Forwarding](#pia-port-forwarding) +- [Automated setup](#automated-setup) +- [Manual PF testing](#manual-pf-testing) + +## Dependencies + +In order for the scripts to work (probably even if you do a manual setup), you will need the following packages: +* `curl` +* `jq` +* (only for WireGuard) `wireguard-tools` (`wg-quick` and `wireguard` kernel module) +* (only for OpenVPN) `openvpn` + +## Disclaimers + +* Port Forwarding is disabled on server-side in the United States. +* These scripts do not enforce IPv6 or DNS settings, so that you have the freedom to configure your setup the way you desire it to work. This means you should have good understanding of VPN and cybersecurity in order to properly configure your setup. +* For battle-tested security, please use the official PIA App, as it was designed to protect you in all scenarios. +* This repo is really fresh at this moment, so please take into consideration the fact that you will probably be one of the first users that use the scripts. +* Though we support research of open source technologies, we can not provide official support for all FOSS platforms, as there are simply too many platforms (which is a good thing). That is why we link 3rd Party repos in this README. We can not guarantee the quality of the code in the 3rd Party Repos, so use them only if you understand the risks. + +## Confirmed Distributions + +The functionality of the scripts within this repository has been tested and confirmed on the following operating systems and GNU/Linux distributions: +* Arch +* Artix +* Fedora 32, 33 +* FreeBSD 12.1 (tweaks are required) +* Manjaro +* PureOS amber +* Raspberry Pi OS 2020-08-20 +* Ubuntu 18.04, 20.04 + +## 3rd Party Repositories + +Some users have created their own repositories for manual connections, based on the information they found within this repository. We can not guarantee the quality of the code found within these 3rd party repos, but we can create a centralized list so it's easy for you to find repos contain scripts to enable PIA services for your system. + +| System | Fork | Language | Scope | Repository | +|:-:|:-:|:-:|:-:|-| +| FreeBSD | Yes | Bash | Compatibility | [glorious1/manual-connections](https://github.com/glorious1/manual-connections) | +| Linux | No | Bash | NetworkManager
GUI integration | [ThePowerTool/PIA-NetworkManager-GUI-Support](https://github.com/ThePowerTool/PIA-NetworkManager-GUI-Support) | +| Linux | No | Python | WireGuard, PF | [milahu/python-piavpn](https://github.com/milahu/python-piavpn) | +| Linux | No | Bash | WireGuard, PF,
router and android config | [triffid/pia-wg](https://github.com/triffid/pia-wg) | +| Linux/FreeBSD/Win | No | Go | WireGuard,
config generation | [ddb_db/piawgcli](https://gitlab.com/ddb_db/piawgcli) | +| OPNsense | No | Python | WireGuard, PF, DIP | [FingerlessGlov3s/OPNsensePIAWireguard](https://github.com/FingerlessGlov3s/OPNsensePIAWireguard) | +| pfSense | No | Sh | OpenVPN, PF | [fm407/PIA-NextGen-PortForwarding](https://github.com/fm407/PIA-NextGen-PortForwarding) | +| pfSense | No | Java/PHP | WireGuard, PF | [ddb_db/pfpiamgr](https://gitlab.com/ddb_db/pfpiamgr) | +| Synology | Yes | Bash | Compatibility | [steff2632/manual-connections](https://github.com/steff2632/manual-connections) | +| Synology | No | Python | PF | [stmty9/synology](https://github.com/stmty9/synology) | +| TrueNAS | No | Bash | PF | [dak180/TrueNAS-Scripts](https://github.com/dak180/TrueNAS-Scripts/blob/master/pia-port-forward.sh) | +| UFW | Yes | Bash | Firewall Rules | [iPherian/manual-connections](https://github.com/iPherian/manual-connections) | +| Windows | No | PowerShell | Windows comptaibility | [ImjuzCY/pia-posh](https://github.com/ImjuzCY/pia-posh) | +| Windows | No | Powershell | OpenVPN, PF | [dougbenham/PIA-OpenVPN-Script](https://github.com/dougbenham/PIA-OpenVPN-Script) | + +## PIA Port Forwarding + +The PIA Port Forwarding service (a.k.a. PF) allows you run services on your own devices, and expose them to the internet by using the PIA VPN Network. The easiest way to set this up is by using a native PIA application. In case you require port forwarding on native clients, please follow this documentation in order to enable port forwarding for your VPN connection. + +This service can be used only AFTER establishing a VPN connection. + +## Automated Setup + +In order to help you use VPN services and PF on any device, we have prepared a few bash scripts that should help you through the process of setting everything up. The scripts also contain a lot of comments, just in case you require detailed information regarding how the technology works. The functionality is controlled via environment variables, so that you have an easy time automating your setup. + +The easiest way to trigger a fully automated connection is by running this oneliner: +``` +sudo VPN_PROTOCOL=wireguard DISABLE_IPV6=yes DIP_TOKEN=no AUTOCONNECT=true PIA_PF=false PIA_DNS=true PIA_USER=p0123456 PIA_PASS=xxxxxxxx ./run_setup.sh +``` + +Here is a list of scripts you could find useful: +* [Prompt based connection](run_setup.sh): This script allows connections with a one-line call, or will prompt for any missing or invalid variables. Variables available for one-line calls include: + * `PIA_USER` - your PIA username + * `PIA_PASS` - your PIA password + * `DIP_TOKEN` - your PIA dedicated IP token (can be purchased in the client control panel) + * `PIA_DNS` - true/false + * `PIA_PF` - true/false + * `MAX_LATENCY` - numeric value, in seconds + * `AUTOCONNECT` - true/false; this will test for and select the server with the lowest latency, it will override PREFERRED_REGION + * `PREFERRED_REGION` - the region ID for a PIA server + * `VPN_PROTOCOL` - wireguard or openvpn; openvpn will default to openvpn_udp_standard, but can also specify openvpn_tcp/udp_standad/strong + * `DISABLE_IPV6` - yes/no +* [Get region details](get_region.sh): This script will provide server details, validate `PREFERRED_REGION` input, and can determine the lowest latency location. The script can also trigger VPN connections, if you specify `VPN_PROTOCOL=wireguard` or `VPN_PROTOCOL=openvpn`; doing so requires a token. This script can reference `get_token.sh` with use of `PIA_USER` and `PIA_PASS`. If called without specifying `PREFERRED_REGION` this script writes a list of servers within lower than `MAX_LATENCY` to a `/opt/piavpn-manual/latencyList` for reference. +* [Get a token](get_token.sh): This script allows you to get an authentication token with a valid 'PIA_USER' and 'PIA_PASS'. It will write the token and its expiration date to `/opt/piavpn-manual/token` for reference. +* [Get DIP details](get_dip.sh): This script will provide necessary connection details to use a dedicated IP. +* [Connect to WireGuard](connect_to_wireguard_with_token.sh): This script allows you to connect to the VPN server via WireGuard. +* [Connect to OpenVPN](connect_to_openvpn_with_token.sh): This script allows you to connect to the VPN server via OpenVPN. +* [Enable Port Forwarding](port_forwarding.sh): Enables you to add Port Forwarding to an existing VPN connection. Adding the environment variable `PIA_PF=true` to any of the previous scripts will also trigger this script. + +## Manual PF Testing + +To use port forwarding on the NextGen network, first of all establish a connection with your favorite protocol. After this, you will need to find the private IP of the gateway you are connected to. In case you are WireGuard, the gateway will be part of the JSON response you get from the server, as you can see in the [bash script](https://github.com/pia-foss/manual-connections/blob/master/wireguard_and_pf.sh#L119). In case you are using OpenVPN, you can find the gateway by checking the routing table with `ip route s t all`. + +After connecting and finding out what the gateway is, get your payload and your signature by calling `getSignature` via HTTPS on port 19999. You will have to add your token as a GET var to prove you actually have an active account. + +Example: +```bash +bash-5.0# curl -k "https://10.4.128.1:19999/getSignature?token=$TOKEN" +{ + "status": "OK", + "payload": "eyJ0b2tlbiI6Inh4eHh4eHh4eCIsInBvcnQiOjQ3MDQ3LCJjcmVhdGVkX2F0IjoiMjAyMC0wNC0zMFQyMjozMzo0NC4xMTQzNjk5MDZaIn0=", + "signature": "a40Tf4OrVECzEpi5kkr1x5vR0DEimjCYJU9QwREDpLM+cdaJMBUcwFoemSuJlxjksncsrvIgRdZc0te4BUL6BA==" +} +``` + +The payload can be decoded with base64 to see your information: +```bash +$ echo eyJ0b2tlbiI6Inh4eHh4eHh4eCIsInBvcnQiOjQ3MDQ3LCJjcmVhdGVkX2F0IjoiMjAyMC0wNC0zMFQyMjozMzo0NC4xMTQzNjk5MDZaIn0= | base64 -d | jq +{ + "token": "xxxxxxxxx", + "port": 47047, + "expires_at": "2020-06-30T22:33:44.114369906Z" +} +``` +This is where you can also see the port you received. Please consider `expires_at` as your request will fail if the token is too old. All ports currently expire after 2 months. + +Use the payload and the signature to bind the port on any server you desire. This is also done by curling the gateway of the VPN server you are connected to. +```bash +bash-5.0# curl -sGk --data-urlencode "payload=${payload}" --data-urlencode "signature=${signature}" https://10.4.128.1:19999/bindPort +{ + "status": "OK", + "message": "port scheduled for add" +} +bash-5.0# +``` + +Call __/bindPort__ every 15 minutes, or the port will be deleted! + +### Testing Your New PF + +To test that it works, you can tcpdump on the port you received: + +``` +bash-5.0# tcpdump -ni any port 47047 +``` + +After that, use curl __from another machine__ on the IP of the traffic server and the port specified in the payload which in our case is `47047`: +```bash +$ curl "http://178.162.208.237:47047" +``` + +You should see the traffic in your tcpdump: +``` +bash-5.0# tcpdump -ni any port 47047 +tcpdump: verbose output suppressed, use -v or -vv for full protocol decode +listening on any, link-type LINUX_SLL (Linux cooked v1), capture size 262144 bytes +22:44:01.510804 IP 81.180.227.170.33884 > 10.4.143.34.47047: Flags [S], seq 906854496, win 64860, options [mss 1380,sackOK,TS val 2608022390 ecr 0,nop,wscale 7], length 0 +22:44:01.510895 IP 10.4.143.34.47047 > 81.180.227.170.33884: Flags [R.], seq 0, ack 906854497, win 0, length 0 +``` + +If you run curl on the same machine (the one that is connected to the VPN), you will see the traffic in tcpdump anyway and the test won't prove anything. At the same time, the request will get firewall so you will not be able to access the port from the same machine. This can only be tested properly by running curl on another system. diff --git a/ca.rsa.4096.crt b/ca.rsa.4096.crt new file mode 100644 index 0000000..82dec69 --- /dev/null +++ b/ca.rsa.4096.crt @@ -0,0 +1,43 @@ +-----BEGIN CERTIFICATE----- +MIIHqzCCBZOgAwIBAgIJAJ0u+vODZJntMA0GCSqGSIb3DQEBDQUAMIHoMQswCQYD +VQQGEwJVUzELMAkGA1UECBMCQ0ExEzARBgNVBAcTCkxvc0FuZ2VsZXMxIDAeBgNV +BAoTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMSAwHgYDVQQLExdQcml2YXRlIElu +dGVybmV0IEFjY2VzczEgMB4GA1UEAxMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3Mx +IDAeBgNVBCkTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMS8wLQYJKoZIhvcNAQkB +FiBzZWN1cmVAcHJpdmF0ZWludGVybmV0YWNjZXNzLmNvbTAeFw0xNDA0MTcxNzQw +MzNaFw0zNDA0MTIxNzQwMzNaMIHoMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0Ex +EzARBgNVBAcTCkxvc0FuZ2VsZXMxIDAeBgNVBAoTF1ByaXZhdGUgSW50ZXJuZXQg +QWNjZXNzMSAwHgYDVQQLExdQcml2YXRlIEludGVybmV0IEFjY2VzczEgMB4GA1UE +AxMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3MxIDAeBgNVBCkTF1ByaXZhdGUgSW50 +ZXJuZXQgQWNjZXNzMS8wLQYJKoZIhvcNAQkBFiBzZWN1cmVAcHJpdmF0ZWludGVy +bmV0YWNjZXNzLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALVk +hjumaqBbL8aSgj6xbX1QPTfTd1qHsAZd2B97m8Vw31c/2yQgZNf5qZY0+jOIHULN +De4R9TIvyBEbvnAg/OkPw8n/+ScgYOeH876VUXzjLDBnDb8DLr/+w9oVsuDeFJ9K +V2UFM1OYX0SnkHnrYAN2QLF98ESK4NCSU01h5zkcgmQ+qKSfA9Ny0/UpsKPBFqsQ +25NvjDWFhCpeqCHKUJ4Be27CDbSl7lAkBuHMPHJs8f8xPgAbHRXZOxVCpayZ2SND +fCwsnGWpWFoMGvdMbygngCn6jA/W1VSFOlRlfLuuGe7QFfDwA0jaLCxuWt/BgZyl +p7tAzYKR8lnWmtUCPm4+BtjyVDYtDCiGBD9Z4P13RFWvJHw5aapx/5W/CuvVyI7p +Kwvc2IT+KPxCUhH1XI8ca5RN3C9NoPJJf6qpg4g0rJH3aaWkoMRrYvQ+5PXXYUzj +tRHImghRGd/ydERYoAZXuGSbPkm9Y/p2X8unLcW+F0xpJD98+ZI+tzSsI99Zs5wi +jSUGYr9/j18KHFTMQ8n+1jauc5bCCegN27dPeKXNSZ5riXFL2XX6BkY68y58UaNz +meGMiUL9BOV1iV+PMb7B7PYs7oFLjAhh0EdyvfHkrh/ZV9BEhtFa7yXp8XR0J6vz +1YV9R6DYJmLjOEbhU8N0gc3tZm4Qz39lIIG6w3FDAgMBAAGjggFUMIIBUDAdBgNV +HQ4EFgQUrsRtyWJftjpdRM0+925Y6Cl08SUwggEfBgNVHSMEggEWMIIBEoAUrsRt +yWJftjpdRM0+925Y6Cl08SWhge6kgeswgegxCzAJBgNVBAYTAlVTMQswCQYDVQQI +EwJDQTETMBEGA1UEBxMKTG9zQW5nZWxlczEgMB4GA1UEChMXUHJpdmF0ZSBJbnRl +cm5ldCBBY2Nlc3MxIDAeBgNVBAsTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMSAw +HgYDVQQDExdQcml2YXRlIEludGVybmV0IEFjY2VzczEgMB4GA1UEKRMXUHJpdmF0 +ZSBJbnRlcm5ldCBBY2Nlc3MxLzAtBgkqhkiG9w0BCQEWIHNlY3VyZUBwcml2YXRl +aW50ZXJuZXRhY2Nlc3MuY29tggkAnS7684Nkme0wDAYDVR0TBAUwAwEB/zANBgkq +hkiG9w0BAQ0FAAOCAgEAJsfhsPk3r8kLXLxY+v+vHzbr4ufNtqnL9/1Uuf8NrsCt +pXAoyZ0YqfbkWx3NHTZ7OE9ZRhdMP/RqHQE1p4N4Sa1nZKhTKasV6KhHDqSCt/dv +Em89xWm2MVA7nyzQxVlHa9AkcBaemcXEiyT19XdpiXOP4Vhs+J1R5m8zQOxZlV1G +tF9vsXmJqWZpOVPmZ8f35BCsYPvv4yMewnrtAC8PFEK/bOPeYcKN50bol22QYaZu +LfpkHfNiFTnfMh8sl/ablPyNY7DUNiP5DRcMdIwmfGQxR5WEQoHL3yPJ42LkB5zs +6jIm26DGNXfwura/mi105+ENH1CaROtRYwkiHb08U6qLXXJz80mWJkT90nr8Asj3 +5xN2cUppg74nG3YVav/38P48T56hG1NHbYF5uOCske19F6wi9maUoto/3vEr0rnX +JUp2KODmKdvBI7co245lHBABWikk8VfejQSlCtDBXn644ZMtAdoxKNfR2WTFVEwJ +iyd1Fzx0yujuiXDROLhISLQDRjVVAvawrAtLZWYK31bY7KlezPlQnl/D9Asxe85l +8jO5+0LdJ6VyOs/Hd4w52alDW/MFySDZSfQHMTIc30hLBJ8OnCEIvluVQQ2UQvoW ++no177N9L2Y+M9TcTA62ZyMXShHQGeh20rb4kK8f+iFX8NxtdHVSkxMEFSfDDyQ= +-----END CERTIFICATE----- diff --git a/connect_to_openvpn_with_token.sh b/connect_to_openvpn_with_token.sh new file mode 100755 index 0000000..2b76162 --- /dev/null +++ b/connect_to_openvpn_with_token.sh @@ -0,0 +1,276 @@ +#!/usr/bin/env bash +# Copyright (C) 2020 Private Internet Access, Inc. +# +# 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. + +# This function allows you to check if the required tools have been installed. +check_tool() { + cmd=$1 + if ! command -v "$cmd" >/dev/null; then + echo "$cmd could not be found" + echo "Please install $cmd" + exit 1 + fi +} + +# Now we call the function to make sure we can use openvpn, curl and jq. +check_tool openvpn +check_tool curl +check_tool jq + +# Check if terminal allows output, if yes, define colors for output +if [[ -t 1 ]]; then + ncolors=$(tput colors) + if [[ -n $ncolors && $ncolors -ge 8 ]]; then + red=$(tput setaf 1) # ANSI red + green=$(tput setaf 2) # ANSI green + nc=$(tput sgr0) # No Color + else + red='' + green='' + nc='' # No Color + fi +fi + +# Check if manual PIA OpenVPN connection is already initialized. +# Multi-hop is out of the scope of this repo, but you should be able to +# get multi-hop running with both OpenVPN and WireGuard. +adapter_check=$( ip a s tun06 2>&1 ) +should_read="Device \"tun06\" does not exist" +pid_filepath="/opt/piavpn-manual/pia_pid" +if [[ $adapter_check != *"$should_read"* ]]; then + echo -e "${red}The tun06 adapter already exists, that interface is required" + echo -e "for this configuration.${nc}" + if [[ -f $pid_filepath ]]; then + old_pid=$( cat "$pid_filepath" ) + old_pid_name=$( ps -p "$old_pid" -o comm= ) + if [[ $old_pid_name == "openvpn" ]]; then + echo + echo -e "It seems likely that process ${red}$old_pid${nc} is an OpenVPN connection" + echo "that was established by using this script. Unless it is closed" + echo "you would not be able to get a new connection." + echo -ne "Do you want to run ${red}$ kill $old_pid${nc} (Y/n): " + read -r close_connection + fi + if echo "${close_connection:0:1}" | grep -iq n; then + echo -e "${red}Closing script. Resolve tun06 adapter conflict and run the script again." + exit 1 + fi + echo + echo -e "${green}Killing the existing OpenVPN process and waiting 5 seconds...${nc}" + kill "$old_pid" + echo + for i in {5..1}; do + echo -n "$i..." + sleep 1 + done + echo + echo + fi +fi + +# PIA currently does not support IPv6. In order to be sure your VPN +# connection does not leak, it is best to disabled IPv6 altogether. +# IPv6 can also be disabled via kernel commandline param, so we must +# first check if this is the case. +if [[ -f /proc/net/if_inet6 ]] && + [[ $(sysctl -n net.ipv6.conf.all.disable_ipv6) -ne 1 || + $(sysctl -n net.ipv6.conf.default.disable_ipv6) -ne 1 ]] +then + echo -e "${red}You should consider disabling IPv6 by running:" + echo "sysctl -w net.ipv6.conf.all.disable_ipv6=1" + echo -e "sysctl -w net.ipv6.conf.default.disable_ipv6=1${nc}" +fi + +# Check if the mandatory environment variables are set. +if [[ -z $OVPN_SERVER_IP || + -z $OVPN_HOSTNAME || + -z $PIA_TOKEN || + -z $CONNECTION_SETTINGS ]]; then + echo -e "${red}This script requires 4 env vars:" + echo "PIA_TOKEN - the token used for authentication" + echo "OVPN_SERVER_IP - IP that you want to connect to" + echo "OVPN_HOSTNAME - name of the server, required for ssl" + echo "CONNECTION_SETTINGS - the protocol and encryption specification" + echo " - available options for CONNECTION_SETTINGS are:" + echo " * openvpn_udp_standard" + echo " * openvpn_udp_strong" + echo " * openvpn_tcp_standard" + echo " * openvpn_tcp_strong" + echo + echo "You can also specify optional env vars:" + echo "PIA_PF - enable port forwarding" + echo "PAYLOAD_AND_SIGNATURE - In case you already have a port." + echo + echo "An easy solution is to just run get_region_and_token.sh" + echo "as it will guide you through getting the best server and" + echo "also a token. Detailed information can be found here:" + echo -e "https://github.com/pia-foss/manual-connections${nc}" + exit 1 +fi + +splitToken="dedicated_ip_$DIP_TOKEN" + +# Create a credentials file with the login token +echo -n "Trying to write /opt/piavpn-manual/pia.ovpn..." +mkdir -p /opt/piavpn-manual +rm -f /opt/piavpn-manual/credentials /opt/piavpn-manual/route_info + +if [[ -z $DIP_TOKEN ]]; then + echo "${PIA_TOKEN:0:62} +${PIA_TOKEN:62}" > /opt/piavpn-manual/credentials || exit 1 + chmod 600 /opt/piavpn-manual/credentials +else + echo "${splitToken:0:62} +${splitToken:62}" > /opt/piavpn-manual/credentials || exit 1 + chmod 600 /opt/piavpn-manual/credentials +fi +echo -e "${green}OK!${nc}" + +# Translate connection settings variable +IFS='_' +read -ra connection_settings <<< "$CONNECTION_SETTINGS" +IFS=' ' +protocol=${connection_settings[1]} +encryption=${connection_settings[2]} + +prefix_filepath="openvpn_config/standard.ovpn" +if [[ $encryption == "strong" ]]; then + prefix_filepath="openvpn_config/strong.ovpn" +fi + +if [[ $protocol == "udp" ]]; then + if [[ $encryption == "standard" ]]; then + port=1198 + else + port=1197 + fi +else + if [[ $encryption == "standard" ]]; then + port=502 + else + port=501 + fi +fi + +# Create the OpenVPN config based on the settings specified +cat "$prefix_filepath" > /opt/piavpn-manual/pia.ovpn || exit 1 +echo "remote $OVPN_SERVER_IP $port $protocol" >> /opt/piavpn-manual/pia.ovpn + +# Copy the up/down scripts to /opt/piavpn-manual/ +# based upon use of PIA DNS +if [[ $PIA_DNS != "true" ]]; then + cp openvpn_config/openvpn_up.sh /opt/piavpn-manual/ + cp openvpn_config/openvpn_down.sh /opt/piavpn-manual/ + echo -e "${red}This configuration will not use PIA DNS.${nc}" + echo "If you want to also enable PIA DNS, please start the script" + echo "with the env var PIA_DNS=true. Example:" + echo $ OVPN_SERVER_IP=\""$OVPN_SERVER_IP"\" OVPN_HOSTNAME=\""$OVPN_HOSTNAME"\" \ + PIA_TOKEN=\""$PIA_TOKEN"\" CONNECTION_SETTINGS=\""$CONNECTION_SETTINGS"\" \ + PIA_PF=true PIA_DNS=true ./connect_to_openvpn_with_token.sh +else + cp openvpn_config/openvpn_up_dnsoverwrite.sh /opt/piavpn-manual/openvpn_up.sh + cp openvpn_config/openvpn_down_dnsoverwrite.sh /opt/piavpn-manual/openvpn_down.sh +fi + +# Start the OpenVPN interface. +# If something failed, stop this script. +# If you get DNS errors because you miss some packages, +# just hardcode /etc/resolv.conf to "nameserver 10.0.0.242". +#rm -f /opt/piavpn-manual/debug_info +echo " +Trying to start the OpenVPN connection..." +openvpn --daemon \ + --config "/opt/piavpn-manual/pia.ovpn" \ + --writepid "/opt/piavpn-manual/pia_pid" \ + --log "/opt/piavpn-manual/debug_info" || exit 1 + +echo -n " +The OpenVPN connect command was issued. + +Confirming OpenVPN connection state..." + +# Check if manual PIA OpenVPN connection is initialized. +# Manually adjust the connection_wait_time if needed +connection_wait_time=10 +confirmation="Initialization Sequence Complete" +for (( timeout=0; timeout <= connection_wait_time; timeout++ )); do + sleep 1 + if grep -q "$confirmation" /opt/piavpn-manual/debug_info; then + connected=true + break + fi +done + +ovpn_pid=$( cat /opt/piavpn-manual/pia_pid ) +gateway_ip=$( cat /opt/piavpn-manual/route_info ) + +# Report and exit if connection was not initialized within 10 seconds. +if [[ $connected != "true" ]]; then + echo -e "${red}The VPN connection was not established within 10 seconds.${nc}" + kill "$ovpn_pid" + exit 1 +fi + +echo -e "${green}Initialization Sequence Complete!${nc} + +At this point, internet should work via VPN. +" + +echo -e "OpenVPN Process ID: ${green}$ovpn_pid${nc} +VPN route IP: ${green}$gateway_ip${nc} + +To disconnect the VPN, run: + +--> ${green}sudo kill $ovpn_pid${nc} <-- +" + +# This section will stop the script if PIA_PF is not set to "true". +if [[ $PIA_PF != "true" ]]; then + echo "If you want to also enable port forwarding, you can start the script:" + echo -e "$ ${green}PIA_TOKEN=$PIA_TOKEN" \ + "PF_GATEWAY=$gateway_ip" \ + "PF_HOSTNAME=$OVPN_HOSTNAME" \ + "./port_forwarding.sh${nc}" + echo + echo "The location used must be port forwarding enabled, or this will fail." + echo "Calling the ./get_region script with PIA_PF=true will provide a filtered list." + exit 1 +fi + +echo -ne "This script got started with ${green}PIA_PF=true${nc}. + +Starting port forwarding in " +for i in {5..1}; do + echo -n "$i..." + sleep 1 +done +echo +echo + +echo -e "Starting procedure to enable port forwarding by running the following command: +$ ${green}PIA_TOKEN=$PIA_TOKEN \\ + PF_GATEWAY=$gateway_ip \\ + PF_HOSTNAME=$OVPN_HOSTNAME \\ + ./port_forwarding.sh${nc}" + +PIA_TOKEN=$PIA_TOKEN \ + PF_GATEWAY=$gateway_ip \ + PF_HOSTNAME=$OVPN_HOSTNAME \ + ./port_forwarding.sh > /tmp/port-forwarding.log 2>&1 & diff --git a/connect_to_wireguard_with_token.sh b/connect_to_wireguard_with_token.sh new file mode 100755 index 0000000..65ffdb2 --- /dev/null +++ b/connect_to_wireguard_with_token.sh @@ -0,0 +1,207 @@ +#!/usr/bin/env bash +# Copyright (C) 2020 Private Internet Access, Inc. +# +# 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. + +# This function allows you to check if the required tools have been installed. +check_tool() { + cmd=$1 + pkg=$2 + if ! command -v "$cmd" >/dev/null; then + echo "$cmd could not be found" + echo "Please install $pkg" + exit 1 + fi +} + +# Now we call the function to make sure we can use wg-quick, curl and jq. +check_tool wg-quick wireguard-tools +check_tool curl curl +check_tool jq jq + +# Check if terminal allows output, if yes, define colors for output +if [[ -t 1 ]]; then + ncolors=$(tput colors) + if [[ -n $ncolors && $ncolors -ge 8 ]]; then + red=$(tput setaf 1) # ANSI red + green=$(tput setaf 2) # ANSI green + nc=$(tput sgr0) # No Color + else + red='' + green='' + nc='' # No Color + fi +fi + +# PIA currently does not support IPv6. In order to be sure your VPN +# connection does not leak, it is best to disabled IPv6 altogether. +# IPv6 can also be disabled via kernel commandline param, so we must +# first check if this is the case. +if [[ -f /proc/net/if_inet6 ]] && + [[ $(sysctl -n net.ipv6.conf.all.disable_ipv6) -ne 1 || + $(sysctl -n net.ipv6.conf.default.disable_ipv6) -ne 1 ]] +then + echo -e "${red}You should consider disabling IPv6 by running:" + echo "sysctl -w net.ipv6.conf.all.disable_ipv6=1" + echo -e "sysctl -w net.ipv6.conf.default.disable_ipv6=1${nc}" +fi + +# Check if the mandatory environment variables are set. +if [[ -z $WG_SERVER_IP || + -z $WG_HOSTNAME || + -z $PIA_TOKEN ]]; then + echo -e "${red}This script requires 3 env vars:" + echo "WG_SERVER_IP - IP that you want to connect to" + echo "WG_HOSTNAME - name of the server, required for ssl" + echo "PIA_TOKEN - your authentication token" + echo + echo "You can also specify optional env vars:" + echo "PIA_PF - enable port forwarding" + echo "PAYLOAD_AND_SIGNATURE - In case you already have a port." + echo + echo "An easy solution is to just run get_region_and_token.sh" + echo "as it will guide you through getting the best server and" + echo "also a token. Detailed information can be found here:" + echo -e "https://github.com/pia-foss/manual-connections${nc}" + exit 1 +fi + +# Create ephemeral wireguard keys, that we don't need to save to disk. +privKey=$(wg genkey) +export privKey +pubKey=$( echo "$privKey" | wg pubkey) +export pubKey + +# Authenticate via the PIA WireGuard RESTful API. +# This will return a JSON with data required for authentication. +# The certificate is required to verify the identity of the VPN server. +# In case you didn't clone the entire repo, get the certificate from: +# https://github.com/pia-foss/manual-connections/blob/master/ca.rsa.4096.crt +# In case you want to troubleshoot the script, replace -s with -v. +echo "Trying to connect to the PIA WireGuard API on $WG_SERVER_IP..." +if [[ -z $DIP_TOKEN ]]; then + wireguard_json="$(curl -s -G \ + --connect-to "$WG_HOSTNAME::$WG_SERVER_IP:" \ + --cacert "ca.rsa.4096.crt" \ + --data-urlencode "pt=${PIA_TOKEN}" \ + --data-urlencode "pubkey=$pubKey" \ + "https://${WG_HOSTNAME}:1337/addKey" )" +else + wireguard_json="$(curl -s -G \ + --connect-to "$WG_HOSTNAME::$WG_SERVER_IP:" \ + --cacert "ca.rsa.4096.crt" \ + --user "dedicated_ip_$DIP_TOKEN:$WG_SERVER_IP" \ + --data-urlencode "pubkey=$pubKey" \ + "https://$WG_HOSTNAME:1337/addKey" )" +fi +export wireguard_json + +# Check if the API returned OK and stop this script if it didn't. +if [[ $(echo "$wireguard_json" | jq -r '.status') != "OK" ]]; then + >&2 echo -e "${red}Server did not return OK. Stopping now.${nc}" + exit 1 +fi + +# Multi-hop is out of the scope of this repo, but you should be able to +# get multi-hop running with both WireGuard and OpenVPN by playing with +# these scripts. Feel free to fork the project and test it out. +echo +echo "Trying to disable a PIA WG connection in case it exists..." +wg-quick down pia && echo -e "${green}\nPIA WG connection disabled!${nc}" +echo + +# Create the WireGuard config based on the JSON received from the API +# In case you want this section to also add the DNS setting, please +# start the script with PIA_DNS=true. +# This uses a PersistentKeepalive of 25 seconds to keep the NAT active +# on firewalls. You can remove that line if your network does not +# require it. +if [[ $PIA_DNS == "true" ]]; then + dnsServer=$(echo "$wireguard_json" | jq -r '.dns_servers[0]') + echo "Trying to set up DNS to $dnsServer. In case you do not have resolvconf," + echo "this operation will fail and you will not get a VPN. If you have issues," + echo "start this script without PIA_DNS." + echo + dnsSettingForVPN="DNS = $dnsServer" +fi +echo -n "Trying to write /etc/wireguard/pia.conf..." +mkdir -p /etc/wireguard +echo " +[Interface] +Address = $(echo "$wireguard_json" | jq -r '.peer_ip') +PrivateKey = $privKey +$dnsSettingForVPN +[Peer] +PersistentKeepalive = 25 +PublicKey = $(echo "$wireguard_json" | jq -r '.server_key') +AllowedIPs = 0.0.0.0/0 +Endpoint = ${WG_SERVER_IP}:$(echo "$wireguard_json" | jq -r '.server_port') +" > /etc/wireguard/pia.conf || exit 1 +echo -e "${green}OK!${nc}" + +# Start the WireGuard interface. +# If something failed, stop this script. +# If you get DNS errors because you miss some packages, +# just hardcode /etc/resolv.conf to "nameserver 10.0.0.242". +echo +echo "Trying to create the wireguard interface..." +wg-quick up pia || exit 1 +echo +echo -e "${green}The WireGuard interface got created.${nc} + +At this point, internet should work via VPN. + +To disconnect the VPN, run: + +--> ${green}wg-quick down pia${nc} <-- +" + +# This section will stop the script if PIA_PF is not set to "true". +if [[ $PIA_PF != "true" ]]; then + echo "If you want to also enable port forwarding, you can start the script:" + echo -e "$ ${green}PIA_TOKEN=$PIA_TOKEN" \ + "PF_GATEWAY=$WG_SERVER_IP" \ + "PF_HOSTNAME=$WG_HOSTNAME" \ + "./port_forwarding.sh${nc}" + echo + echo "The location used must be port forwarding enabled, or this will fail." + echo "Calling the ./get_region script with PIA_PF=true will provide a filtered list." + exit 1 +fi + +echo -ne "This script got started with ${green}PIA_PF=true${nc}. + +Starting port forwarding in " +for i in {5..1}; do + echo -n "$i..." + sleep 1 +done +echo +echo + +echo -e "Starting procedure to enable port forwarding by running the following command: +$ ${green}PIA_TOKEN=$PIA_TOKEN \\ + PF_GATEWAY=$WG_SERVER_IP \\ + PF_HOSTNAME=$WG_HOSTNAME \\ + ./port_forwarding.sh${nc}" + +PIA_TOKEN=$PIA_TOKEN \ + PF_GATEWAY=$WG_SERVER_IP \ + PF_HOSTNAME=$WG_HOSTNAME \ + ./port_forwarding.sh diff --git a/get_dip.sh b/get_dip.sh new file mode 100755 index 0000000..1610aed --- /dev/null +++ b/get_dip.sh @@ -0,0 +1,109 @@ +#!/bin/bash +# Copyright (C) 2020 Private Internet Access, Inc. +# +# 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. + +# This function allows you to check if the required tools have been installed. +check_tool() { + cmd=$1 + if ! command -v $cmd &>/dev/null; then + echo "$cmd could not be found" + echo "Please install $cmd" + exit 1 + fi +} + +# Now we call the function to make sure we can use curl and jq. +check_tool curl +check_tool jq + +# Check if terminal allows output, if yes, define colors for output +if [[ -t 1 ]]; then + ncolors=$(tput colors) + if [[ -n $ncolors && $ncolors -ge 8 ]]; then + red=$(tput setaf 1) # ANSI red + green=$(tput setaf 2) # ANSI green + nc=$(tput sgr0) # No Color + else + red='' + green='' + nc='' # No Color + fi +fi + +# Only allow script to run as root +if (( EUID != 0 )); then + echo -e "${red}This script needs to be run as root. Try again with 'sudo $0'${nc}" + exit 1 +fi + +mkdir -p /opt/piavpn-manual + +if [[ -z $PIA_TOKEN ]]; then + echo "If you want this script to automatically retrieve dedicated IP location details" + echo "from the Meta service, please add the variables PIA_TOKEN and DIP_TOKEN. Example:" + echo "$ PIA_TOKEN DIP_TOKEN=DIP1a2b3c4d5e6f7g8h9i10j11k12l13 ./get_token.sh" + exit 1 +fi + +dipSavedLocation=/opt/piavpn-manual/dipAddress + +echo -n "Checking DIP token..." + +generateDIPResponse=$(curl -s --location --request POST \ + 'https://www.privateinternetaccess.com/api/client/v2/dedicated_ip' \ + --header 'Content-Type: application/json' \ + --header "Authorization: Token $PIA_TOKEN" \ + --data-raw '{ + "tokens":["'"$DIP_TOKEN"'"] + }') + +if [ "$(echo "$generateDIPResponse" | jq -r '.[0].status')" != "active" ]; then + echo + echo + echo -e "${red}Could not validate the dedicated IP token provided!${nc}" + echo + exit +fi + +echo -e ${green}OK!${nc} +echo +dipAddress=$(echo "$generateDIPResponse" | jq -r '.[0].ip') +dipHostname=$(echo "$generateDIPResponse" | jq -r '.[0].cn') +keyHostname=$(echo "dedicated_ip_$DIP_TOKEN") +dipExpiration=$(echo "$generateDIPResponse" | jq -r '.[0].dip_expire') +dipExpiration=$(date -d @$dipExpiration) +dipID=$(echo "$generateDIPResponse" | jq -r '.[0].id') +echo -e The hostname of your dedicated IP is ${green}$dipHostname${nc} +echo +echo -e The dedicated IP address is ${green}$dipAddress${nc} +echo +echo This dedicated IP is valid until $dipExpiration. +echo +pfCapable="true" +if [[ $dipID == us_* ]]; then + pfCapable="false" + echo This location does not have port forwarding capability. + echo +fi +echo $dipAddress > /opt/piavpn-manual/dipAddress || exit 1 +echo $dipHostname >> /opt/piavpn-manual/dipAddress +echo $keyHostname >> /opt/piavpn-manual/dipAddress +echo $dipExpiration >> /opt/piavpn-manual/dipAddress +echo $pfCapable >> /opt/piavpn-manual/dipAddress diff --git a/get_region.sh b/get_region.sh new file mode 100755 index 0000000..661bd27 --- /dev/null +++ b/get_region.sh @@ -0,0 +1,273 @@ +#!/usr/bin/env bash +# Copyright (C) 2020 Private Internet Access, Inc. +# +# 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. + +# This function allows you to check if the required tools have been installed. +check_tool() { + cmd=$1 + if ! command -v "$cmd" >/dev/null; then + echo "$cmd could not be found" + echo "Please install $cmd" + exit 1 + fi +} + +# Now we call the function to make sure we can use curl and jq. +check_tool curl +check_tool jq + +# If the server list has less than 1000 characters, it means curl failed. +check_all_region_data() { + echo + echo -n "Getting the server list..." + + if [[ ${#all_region_data} -lt 1000 ]]; then + echo -e "${red}Could not get correct region data. To debug this, run:" + echo "$ curl -v $serverlist_url" + echo -e "If it works, you will get a huge JSON as a response.${nc}" + exit 1 + fi + + # Notify the user that we got the server list. + echo -e "${green}OK!${nc} + " +} + +# Get all data for the selected region +# Exit with code 1 if the REGION_ID provided is invalid +get_selected_region_data() { + regionData="$( echo "$all_region_data" | + jq --arg REGION_ID "$selectedRegion" -r \ + '.regions[] | select(.id==$REGION_ID)')" + if [[ -z $regionData ]]; then + echo -e "${red}The REGION_ID $selectedRegion is not valid.${nc} + " + exit 1 + fi +} + +# Check if terminal allows output, if yes, define colors for output +if [[ -t 1 ]]; then + ncolors=$(tput colors) + if [[ -n $ncolors && $ncolors -ge 8 ]]; then + red=$(tput setaf 1) # ANSI red + green=$(tput setaf 2) # ANSI green + nc=$(tput sgr0) # No Color + else + red='' + green='' + nc='' # No Color + fi +fi + +# Only allow script to run as root +if (( EUID != 0 )); then + echo -e "${red}This script needs to be run as root. Try again with 'sudo $0'${nc}" + exit 1 +fi + +mkdir -p /opt/piavpn-manual +# Erase old latencyList file +rm -f /opt/piavpn-manual/latencyList +touch /opt/piavpn-manual/latencyList + +# This allows you to set the maximum allowed latency in seconds. +# All servers that respond slower than this will be ignored. +# You can inject this with the environment variable MAX_LATENCY. +# The default value is 50 milliseconds. +MAX_LATENCY=${MAX_LATENCY:-0.05} +export MAX_LATENCY + +serverlist_url='https://serverlist.piaservers.net/vpninfo/servers/v6' + +# This function checks the latency you have to a specific region. +# It will print a human-readable message to stderr, +# and it will print the variables to stdout +printServerLatency() { + serverIP=$1 + regionID=$2 + regionName="$(echo "${@:3}" | + sed 's/ false//' | sed 's/true/(geo)/')" + time=$(LC_NUMERIC=en_US.utf8 curl -o /dev/null -s \ + --connect-timeout "$MAX_LATENCY" \ + --write-out "%{time_connect}" \ + "http://$serverIP:443") + if [[ $? -eq 0 ]]; then + >&2 echo "Got latency ${time}s for region: $regionName" + echo "$time $regionID $serverIP" + # Write a list of servers with acceptable latency + # to /opt/piavpn-manual/latencyList + echo -e "$time" "$regionID"'\t'"$serverIP"'\t'"$regionName" >> /opt/piavpn-manual/latencyList + fi + # Sort the latencyList, ordered by latency + sort -no /opt/piavpn-manual/latencyList /opt/piavpn-manual/latencyList +} +export -f printServerLatency + +# If a server location or autoconnect isn't specified, set the variable to false/no. +if [[ -z $PREFERRED_REGION ]]; then + PREFERRED_REGION=none +fi +if [[ -z $VPN_PROTOCOL ]]; then + VPN_PROTOCOL=no +fi + +# Get all region data +all_region_data=$(curl -s "$serverlist_url" | head -1) + +# Set the region the user has specified +selectedRegion=$PREFERRED_REGION + +# If a server isn't being specified, auto-select the server with the lowest latency +if [[ $selectedRegion == "none" ]]; then + selectedOrLowestLatency="lowest latency" + check_all_region_data + + # Making sure this variable doesn't contain some strange string + if [[ $PIA_PF != "true" ]]; then + PIA_PF="false" + fi + + # Test one server from each region to get the closest region. + # If port forwarding is enabled, filter out regions that don't support it. + if [[ $PIA_PF == "true" ]]; then + echo "Port Forwarding is enabled, non-PF servers excluded." + echo + summarized_region_data="$( echo "$all_region_data" | + jq -r '.regions[] | select(.port_forward==true) | + .servers.meta[0].ip+" "+.id+" "+.name+" "+(.geo|tostring)' )" + else + summarized_region_data="$( echo "$all_region_data" | + jq -r '.regions[] | + .servers.meta[0].ip+" "+.id+" "+.name+" "+(.geo|tostring)' )" + fi + echo -e Testing regions that respond \ + faster than "${green}$MAX_LATENCY${nc}" seconds: + selectedRegion="$(echo "$summarized_region_data" | + xargs -I{} bash -c 'printServerLatency {}' | + sort | head -1 | awk '{ print $2 }')" + echo + + if [[ -z $selectedRegion ]]; then + echo -e "${red}No region responded within ${MAX_LATENCY}s, consider using a higher timeout." + echo "For example, to wait 1 second for each region, inject MAX_LATENCY=1 like this:" + echo -e "$ MAX_LATENCY=1 ./get_region.sh${nc}" + exit 1 + else + echo -e "A list of servers and connection details, ordered by latency can be +found in at : ${green}/opt/piavpn-manual/latencyList${nc} +" + fi +else + selectedOrLowestLatency="selected" + check_all_region_data +fi + +get_selected_region_data + +bestServer_meta_IP=$(echo "$regionData" | jq -r '.servers.meta[0].ip') +bestServer_meta_hostname=$(echo "$regionData" | jq -r '.servers.meta[0].cn') +bestServer_WG_IP=$(echo "$regionData" | jq -r '.servers.wg[0].ip') +bestServer_WG_hostname=$(echo "$regionData" | jq -r '.servers.wg[0].cn') +bestServer_OT_IP=$(echo "$regionData" | jq -r '.servers.ovpntcp[0].ip') +bestServer_OT_hostname=$(echo "$regionData" | jq -r '.servers.ovpntcp[0].cn') +bestServer_OU_IP=$(echo "$regionData" | jq -r '.servers.ovpnudp[0].ip') +bestServer_OU_hostname=$(echo "$regionData" | jq -r '.servers.ovpnudp[0].cn') + + +if [[ $VPN_PROTOCOL == "no" ]]; then + echo -ne "The $selectedOrLowestLatency region is ${green}$(echo "$regionData" | jq -r '.name')${nc}" + if echo "$regionData" | jq -r '.geo' | grep true > /dev/null; then + echo " (geolocated region)." + else + echo "." + fi + echo -e " +The script found the best servers from the region you selected. +When connecting to an IP (no matter which protocol), please verify +the SSL/TLS certificate actually contains the hostname so that you +are sure you are connecting to a secure server, validated by the +PIA authority. Please find below the list of best IPs and matching +hostnames for each protocol: +${green}Meta Services $bestServer_meta_IP\t- $bestServer_meta_hostname +WireGuard $bestServer_WG_IP\t- $bestServer_WG_hostname +OpenVPN TCP $bestServer_OT_IP\t- $bestServer_OT_hostname +OpenVPN UDP $bestServer_OU_IP\t- $bestServer_OU_hostname +${nc}" +fi + +# The script will check for an authentication token, and use it if present +# If no token exists, the script will check for login credentials to generate one +if [[ -z $PIA_TOKEN ]]; then + if [[ -z $PIA_USER || -z $PIA_PASS ]]; then + echo -e "${red}If you want this script to automatically get an authentication" + echo "token, please add the variables PIA_USER and PIA_PASS. Example:" + echo -e "$ PIA_USER=p0123456 PIA_PASS=xxx ./get_region.sh${nc}" + exit 0 + fi + ./get_token.sh + PIA_TOKEN=$( awk 'NR == 1' /opt/piavpn-manual/token ) + export PIA_TOKEN + rm -f /opt/piavpn-manual/token +else + echo -e "Using existing token ${green}$PIA_TOKEN${nc}." + echo +fi + +# Connect with WireGuard and clear authentication token file and latencyList +if [[ $VPN_PROTOCOL == "wireguard" ]]; then + echo "The ./get_region.sh script got started with" + echo -e "${green}VPN_PROTOCOL=wireguard${nc}, so we will automatically connect to WireGuard," + echo "by running this command:" + echo -e "$ ${green}PIA_TOKEN=$PIA_TOKEN \\" + echo "WG_SERVER_IP=$bestServer_WG_IP WG_HOSTNAME=$bestServer_WG_hostname \\" + echo -e "PIA_PF=$PIA_PF ./connect_to_wireguard_with_token.sh${nc}" + echo + PIA_PF=$PIA_PF PIA_TOKEN=$PIA_TOKEN WG_SERVER_IP=$bestServer_WG_IP \ + WG_HOSTNAME=$bestServer_WG_hostname ./connect_to_wireguard_with_token.sh + rm -f /opt/piavpn-manual/latencyList + exit 0 +fi + +# Connect with OpenVPN and clear authentication token file and latencyList +if [[ $VPN_PROTOCOL == openvpn* ]]; then + serverIP=$bestServer_OU_IP + serverHostname=$bestServer_OU_hostname + if [[ $VPN_PROTOCOL == *tcp* ]]; then + serverIP=$bestServer_OT_IP + serverHostname=$bestServer_OT_hostname + fi + echo "The ./get_region.sh script got started with" + echo -e "${green}VPN_PROTOCOL=$VPN_PROTOCOL${nc}, so we will automatically" + echo "connect to OpenVPN, by running this command:" + echo -e "$ ${green}PIA_PF=$PIA_PF PIA_TOKEN=$PIA_TOKEN \\" + echo " OVPN_SERVER_IP=$serverIP \\" + echo " OVPN_HOSTNAME=$serverHostname \\" + echo " CONNECTION_SETTINGS=$VPN_PROTOCOL \\" + echo -e " ./connect_to_openvpn_with_token.sh${nc}" + echo + PIA_PF=$PIA_PF PIA_TOKEN=$PIA_TOKEN \ + OVPN_SERVER_IP=$serverIP \ + OVPN_HOSTNAME=$serverHostname \ + CONNECTION_SETTINGS=$VPN_PROTOCOL \ + ./connect_to_openvpn_with_token.sh + rm -f /opt/piavpn-manual/latencyList + exit 0 +fi diff --git a/get_token.sh b/get_token.sh new file mode 100755 index 0000000..bca23ce --- /dev/null +++ b/get_token.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# Copyright (C) 2020 Private Internet Access, Inc. +# +# 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. + +# This function allows you to check if the required tools have been installed. +check_tool() { + cmd=$1 + if ! command -v "$cmd" >/dev/null; then + echo "$cmd could not be found" + echo "Please install $cmd" + exit 1 + fi +} + +# Now we call the function to make sure we can use curl and jq. +check_tool curl +check_tool jq + +# This function creates a timestamp, to use for setting $TOKEN_EXPIRATION +timeout_timestamp() { + date +"%c" --date='1 day' # Timestamp 24 hours +} + +# Check if terminal allows output, if yes, define colors for output +if [[ -t 1 ]]; then + ncolors=$(tput colors) + if [[ -n $ncolors && $ncolors -ge 8 ]]; then + red=$(tput setaf 1) # ANSI red + green=$(tput setaf 2) # ANSI green + nc=$(tput sgr0) # No Color + else + red='' + green='' + nc='' # No Color + fi +fi + +# Only allow script to run as root +if (( EUID != 0 )); then + echo -e "${red}This script needs to be run as root. Try again with 'sudo $0'${nc}" + exit 1 +fi + +mkdir -p /opt/piavpn-manual + +if [[ -z $PIA_USER || -z $PIA_PASS ]]; then + echo "If you want this script to automatically get a token from the Meta" + echo "service, please add the variables PIA_USER and PIA_PASS. Example:" + echo "$ PIA_USER=p0123456 PIA_PASS=xxx ./get_token.sh" + exit 1 +fi + +echo -n "Checking login credentials..." + +generateTokenResponse=$(curl -s --location --request POST \ + 'https://www.privateinternetaccess.com/api/client/v2/token' \ + --form "username=$PIA_USER" \ + --form "password=$PIA_PASS" ) + +if [ "$(echo "$generateTokenResponse" | jq -r '.token')" == "" ]; then + echo + echo + echo -e "${red}Could not authenticate with the login credentials provided!${nc}" + echo + exit +fi + +echo -e "${green}OK!" +echo +token=$(echo "$generateTokenResponse" | jq -r '.token') +tokenExpiration=$(timeout_timestamp) +tokenLocation=/opt/piavpn-manual/token +echo -e "PIA_TOKEN=$token${nc}" +echo "$token" > "$tokenLocation" || exit 1 +echo "$tokenExpiration" >> "$tokenLocation" +echo +echo "This token will expire in 24 hours, on $tokenExpiration." +echo diff --git a/openvpn_config/openvpn_down.sh b/openvpn_config/openvpn_down.sh new file mode 100755 index 0000000..24d47b5 --- /dev/null +++ b/openvpn_config/openvpn_down.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +# Remove process and route information when connection closes +rm -rf /opt/piavpn-manual/pia_pid /opt/pia-manual/route_info diff --git a/openvpn_config/openvpn_down_dnsoverwrite.sh b/openvpn_config/openvpn_down_dnsoverwrite.sh new file mode 100755 index 0000000..900d403 --- /dev/null +++ b/openvpn_config/openvpn_down_dnsoverwrite.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +# Remove process and route information when connection closes +rm -rf /opt/piavpn-manual/pia_pid /opt/pia-manual/route_info + +# Replace resolv.conf with original stored as backup +cat /opt/piavpn-manual/resolv_conf_backup > /etc/resolv.conf diff --git a/openvpn_config/openvpn_up.sh b/openvpn_config/openvpn_up.sh new file mode 100755 index 0000000..4d38385 --- /dev/null +++ b/openvpn_config/openvpn_up.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +# Write gateway IP for reference +echo "$route_vpn_gateway" > /opt/piavpn-manual/route_info diff --git a/openvpn_config/openvpn_up_dnsoverwrite.sh b/openvpn_config/openvpn_up_dnsoverwrite.sh new file mode 100755 index 0000000..3e6dbf1 --- /dev/null +++ b/openvpn_config/openvpn_up_dnsoverwrite.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +# Write gateway IP for reference +echo "$route_vpn_gateway" > /opt/piavpn-manual/route_info + +# Back up resolv.conf and create new one with PIA DNS +cat /etc/resolv.conf > /opt/piavpn-manual/resolv_conf_backup +echo "# Generated by /connect_to_openvpn_with_token.sh +nameserver 10.0.0.241" > /etc/resolv.conf diff --git a/openvpn_config/standard.ovpn b/openvpn_config/standard.ovpn new file mode 100644 index 0000000..08f74aa --- /dev/null +++ b/openvpn_config/standard.ovpn @@ -0,0 +1,56 @@ +client +dev tun06 +resolv-retry infinite +nobind +persist-key +persist-tun +cipher aes-128-cbc +auth sha1 +tls-client +remote-cert-tls server + +auth-user-pass /opt/piavpn-manual/credentials +compress +verb 1 +reneg-sec 0 + + +-----BEGIN CERTIFICATE----- +MIIFqzCCBJOgAwIBAgIJAKZ7D5Yv87qDMA0GCSqGSIb3DQEBDQUAMIHoMQswCQYD +VQQGEwJVUzELMAkGA1UECBMCQ0ExEzARBgNVBAcTCkxvc0FuZ2VsZXMxIDAeBgNV +BAoTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMSAwHgYDVQQLExdQcml2YXRlIElu +dGVybmV0IEFjY2VzczEgMB4GA1UEAxMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3Mx +IDAeBgNVBCkTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMS8wLQYJKoZIhvcNAQkB +FiBzZWN1cmVAcHJpdmF0ZWludGVybmV0YWNjZXNzLmNvbTAeFw0xNDA0MTcxNzM1 +MThaFw0zNDA0MTIxNzM1MThaMIHoMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0Ex +EzARBgNVBAcTCkxvc0FuZ2VsZXMxIDAeBgNVBAoTF1ByaXZhdGUgSW50ZXJuZXQg +QWNjZXNzMSAwHgYDVQQLExdQcml2YXRlIEludGVybmV0IEFjY2VzczEgMB4GA1UE +AxMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3MxIDAeBgNVBCkTF1ByaXZhdGUgSW50 +ZXJuZXQgQWNjZXNzMS8wLQYJKoZIhvcNAQkBFiBzZWN1cmVAcHJpdmF0ZWludGVy +bmV0YWNjZXNzLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPXD +L1L9tX6DGf36liA7UBTy5I869z0UVo3lImfOs/GSiFKPtInlesP65577nd7UNzzX +lH/P/CnFPdBWlLp5ze3HRBCc/Avgr5CdMRkEsySL5GHBZsx6w2cayQ2EcRhVTwWp +cdldeNO+pPr9rIgPrtXqT4SWViTQRBeGM8CDxAyTopTsobjSiYZCF9Ta1gunl0G/ +8Vfp+SXfYCC+ZzWvP+L1pFhPRqzQQ8k+wMZIovObK1s+nlwPaLyayzw9a8sUnvWB +/5rGPdIYnQWPgoNlLN9HpSmsAcw2z8DXI9pIxbr74cb3/HSfuYGOLkRqrOk6h4RC +OfuWoTrZup1uEOn+fw8CAwEAAaOCAVQwggFQMB0GA1UdDgQWBBQv63nQ/pJAt5tL +y8VJcbHe22ZOsjCCAR8GA1UdIwSCARYwggESgBQv63nQ/pJAt5tLy8VJcbHe22ZO +sqGB7qSB6zCB6DELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRMwEQYDVQQHEwpM +b3NBbmdlbGVzMSAwHgYDVQQKExdQcml2YXRlIEludGVybmV0IEFjY2VzczEgMB4G +A1UECxMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3MxIDAeBgNVBAMTF1ByaXZhdGUg +SW50ZXJuZXQgQWNjZXNzMSAwHgYDVQQpExdQcml2YXRlIEludGVybmV0IEFjY2Vz +czEvMC0GCSqGSIb3DQEJARYgc2VjdXJlQHByaXZhdGVpbnRlcm5ldGFjY2Vzcy5j +b22CCQCmew+WL/O6gzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4IBAQAn +a5PgrtxfwTumD4+3/SYvwoD66cB8IcK//h1mCzAduU8KgUXocLx7QgJWo9lnZ8xU +ryXvWab2usg4fqk7FPi00bED4f4qVQFVfGfPZIH9QQ7/48bPM9RyfzImZWUCenK3 +7pdw4Bvgoys2rHLHbGen7f28knT2j/cbMxd78tQc20TIObGjo8+ISTRclSTRBtyC +GohseKYpTS9himFERpUgNtefvYHbn70mIOzfOJFTVqfrptf9jXa9N8Mpy3ayfodz +1wiqdteqFXkTYoSDctgKMiZ6GdocK9nMroQipIQtpnwd4yBDWIyC6Bvlkrq5TQUt +YDQ8z9v+DMO6iwyIDRiU +-----END CERTIFICATE----- + + +disable-occ +script-security 2 +up /opt/piavpn-manual/openvpn_up.sh +down /opt/piavpn-manual/openvpn_down.sh diff --git a/openvpn_config/strong.ovpn b/openvpn_config/strong.ovpn new file mode 100644 index 0000000..2f30e0f --- /dev/null +++ b/openvpn_config/strong.ovpn @@ -0,0 +1,66 @@ +client +dev tun06 +resolv-retry infinite +nobind +persist-key +persist-tun +cipher aes-256-cbc +auth sha256 +tls-client +remote-cert-tls server + +auth-user-pass /opt/piavpn-manual/credentials +compress +verb 1 +reneg-sec 0 + + +-----BEGIN CERTIFICATE----- +MIIHqzCCBZOgAwIBAgIJAJ0u+vODZJntMA0GCSqGSIb3DQEBDQUAMIHoMQswCQYD +VQQGEwJVUzELMAkGA1UECBMCQ0ExEzARBgNVBAcTCkxvc0FuZ2VsZXMxIDAeBgNV +BAoTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMSAwHgYDVQQLExdQcml2YXRlIElu +dGVybmV0IEFjY2VzczEgMB4GA1UEAxMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3Mx +IDAeBgNVBCkTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMS8wLQYJKoZIhvcNAQkB +FiBzZWN1cmVAcHJpdmF0ZWludGVybmV0YWNjZXNzLmNvbTAeFw0xNDA0MTcxNzQw +MzNaFw0zNDA0MTIxNzQwMzNaMIHoMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0Ex +EzARBgNVBAcTCkxvc0FuZ2VsZXMxIDAeBgNVBAoTF1ByaXZhdGUgSW50ZXJuZXQg +QWNjZXNzMSAwHgYDVQQLExdQcml2YXRlIEludGVybmV0IEFjY2VzczEgMB4GA1UE +AxMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3MxIDAeBgNVBCkTF1ByaXZhdGUgSW50 +ZXJuZXQgQWNjZXNzMS8wLQYJKoZIhvcNAQkBFiBzZWN1cmVAcHJpdmF0ZWludGVy +bmV0YWNjZXNzLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALVk +hjumaqBbL8aSgj6xbX1QPTfTd1qHsAZd2B97m8Vw31c/2yQgZNf5qZY0+jOIHULN +De4R9TIvyBEbvnAg/OkPw8n/+ScgYOeH876VUXzjLDBnDb8DLr/+w9oVsuDeFJ9K +V2UFM1OYX0SnkHnrYAN2QLF98ESK4NCSU01h5zkcgmQ+qKSfA9Ny0/UpsKPBFqsQ +25NvjDWFhCpeqCHKUJ4Be27CDbSl7lAkBuHMPHJs8f8xPgAbHRXZOxVCpayZ2SND +fCwsnGWpWFoMGvdMbygngCn6jA/W1VSFOlRlfLuuGe7QFfDwA0jaLCxuWt/BgZyl +p7tAzYKR8lnWmtUCPm4+BtjyVDYtDCiGBD9Z4P13RFWvJHw5aapx/5W/CuvVyI7p +Kwvc2IT+KPxCUhH1XI8ca5RN3C9NoPJJf6qpg4g0rJH3aaWkoMRrYvQ+5PXXYUzj +tRHImghRGd/ydERYoAZXuGSbPkm9Y/p2X8unLcW+F0xpJD98+ZI+tzSsI99Zs5wi +jSUGYr9/j18KHFTMQ8n+1jauc5bCCegN27dPeKXNSZ5riXFL2XX6BkY68y58UaNz +meGMiUL9BOV1iV+PMb7B7PYs7oFLjAhh0EdyvfHkrh/ZV9BEhtFa7yXp8XR0J6vz +1YV9R6DYJmLjOEbhU8N0gc3tZm4Qz39lIIG6w3FDAgMBAAGjggFUMIIBUDAdBgNV +HQ4EFgQUrsRtyWJftjpdRM0+925Y6Cl08SUwggEfBgNVHSMEggEWMIIBEoAUrsRt +yWJftjpdRM0+925Y6Cl08SWhge6kgeswgegxCzAJBgNVBAYTAlVTMQswCQYDVQQI +EwJDQTETMBEGA1UEBxMKTG9zQW5nZWxlczEgMB4GA1UEChMXUHJpdmF0ZSBJbnRl +cm5ldCBBY2Nlc3MxIDAeBgNVBAsTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMSAw +HgYDVQQDExdQcml2YXRlIEludGVybmV0IEFjY2VzczEgMB4GA1UEKRMXUHJpdmF0 +ZSBJbnRlcm5ldCBBY2Nlc3MxLzAtBgkqhkiG9w0BCQEWIHNlY3VyZUBwcml2YXRl +aW50ZXJuZXRhY2Nlc3MuY29tggkAnS7684Nkme0wDAYDVR0TBAUwAwEB/zANBgkq +hkiG9w0BAQ0FAAOCAgEAJsfhsPk3r8kLXLxY+v+vHzbr4ufNtqnL9/1Uuf8NrsCt +pXAoyZ0YqfbkWx3NHTZ7OE9ZRhdMP/RqHQE1p4N4Sa1nZKhTKasV6KhHDqSCt/dv +Em89xWm2MVA7nyzQxVlHa9AkcBaemcXEiyT19XdpiXOP4Vhs+J1R5m8zQOxZlV1G +tF9vsXmJqWZpOVPmZ8f35BCsYPvv4yMewnrtAC8PFEK/bOPeYcKN50bol22QYaZu +LfpkHfNiFTnfMh8sl/ablPyNY7DUNiP5DRcMdIwmfGQxR5WEQoHL3yPJ42LkB5zs +6jIm26DGNXfwura/mi105+ENH1CaROtRYwkiHb08U6qLXXJz80mWJkT90nr8Asj3 +5xN2cUppg74nG3YVav/38P48T56hG1NHbYF5uOCske19F6wi9maUoto/3vEr0rnX +JUp2KODmKdvBI7co245lHBABWikk8VfejQSlCtDBXn644ZMtAdoxKNfR2WTFVEwJ +iyd1Fzx0yujuiXDROLhISLQDRjVVAvawrAtLZWYK31bY7KlezPlQnl/D9Asxe85l +8jO5+0LdJ6VyOs/Hd4w52alDW/MFySDZSfQHMTIc30hLBJ8OnCEIvluVQQ2UQvoW ++no177N9L2Y+M9TcTA62ZyMXShHQGeh20rb4kK8f+iFX8NxtdHVSkxMEFSfDDyQ= +-----END CERTIFICATE----- + + +disable-occ +script-security 2 +up /opt/piavpn-manual/openvpn_up.sh +down /opt/piavpn-manual/openvpn_down.sh diff --git a/pivpn_install.sh b/pivpn_install.sh new file mode 100755 index 0000000..91a9be6 --- /dev/null +++ b/pivpn_install.sh @@ -0,0 +1,3700 @@ +#!/usr/bin/env bash +# PiVPN: Trivial OpenVPN or WireGuard setup and configuration +# Easiest setup and mangement of OpenVPN or WireGuard on Raspberry Pi +# https://pivpn.io +# Heavily adapted from the pi-hole.net project and... +# https://github.com/StarshipEngineer/OpenVPN-Setup/ +# +# Install with this command (from your Pi): +# +# curl -sSfL https://install.pivpn.io | bash +# Make sure you have `curl` installed + +######## VARIABLES ######### +pivpnGitUrl="https://github.com/pivpn/pivpn.git" +# Uncomment to checkout a custom branch for local pivpn files +#pivpnGitBranch="custombranchtocheckout" +setupVarsFile="setupVars.conf" +setupConfigDir="/etc/pivpn" +tempsetupVarsFile="/tmp/setupVars.conf" +pivpnFilesDir="/usr/local/src/pivpn" +pivpnScriptDir="/opt/pivpn" + +piholeSetupVars="/etc/pihole/setupVars.conf" +dnsmasqConfig="/etc/dnsmasq.d/02-pivpn.conf" + +dhcpcdFile="/etc/dhcpcd.conf" +ovpnUserGroup="openvpn:openvpn" + +######## PKG Vars ######## +PKG_MANAGER="apt-get" +### FIXME: quoting UPDATE_PKG_CACHE and PKG_INSTALL hangs the script, +### shellcheck SC2086 +UPDATE_PKG_CACHE="${PKG_MANAGER} update -y" +PKG_INSTALL="${PKG_MANAGER} --yes --no-install-recommends install" +PKG_COUNT="${PKG_MANAGER} -s -o Debug::NoLocking=true upgrade | grep -c ^Inst || true" +CHECK_PKG_INSTALLED='dpkg-query -s' + +# Dependencies that are required by the script, +# regardless of the VPN protocol chosen +BASE_DEPS=(git tar curl grep dnsutils grepcidr whiptail net-tools) +BASE_DEPS+=(bsdmainutils bash-completion) + +BASE_DEPS_ALPINE=(git grep bind-tools newt net-tools bash-completion coreutils) +BASE_DEPS_ALPINE+=(openssl util-linux openrc iptables ip6tables coreutils sed) +BASE_DEPS_ALPINE+=(perl) + +# Dependencies that where actually installed by the script. For example if the +# script requires grep and dnsutils but dnsutils is already installed, we save +# grep here. This way when uninstalling PiVPN we won't prompt to remove packages +# that may have been installed by the user for other reasons +INSTALLED_PACKAGES=() + +######## URLs ######## +easyrsaVer="3.1.0" +easyrsaRel="https://github.com/OpenVPN/easy-rsa/releases/download/v${easyrsaVer}/EasyRSA-${easyrsaVer}.tgz" + +######## Undocumented Flags. Shhh ######## +runUnattended=false +skipSpaceCheck=false +reconfigure=false +showUnsupportedNICs=false + +######## Some vars that might be empty +# but need to be defined for checks +pivpnPERSISTENTKEEPALIVE="" +pivpnDNS2="" + +######## IPv6 related config +# cli parameter "--noipv6" allows to disable IPv6 which also prevents forced +# IPv6 route +# cli parameter "--ignoreipv6leak" allows to skip the forced IPv6 route if +# required (not recommended) + +## Force IPv6 through VPN even if IPv6 is not supported by the server +## This will prevent an IPv6 leak on the client site but might cause +## issues on the client site accessing IPv6 addresses. +## This option is useless if routes are set manually. +## It's also irrelevant when IPv6 is (forced) enabled. +pivpnforceipv6route=1 + +## Enable or disable IPv6. +## Leaving it empty or set to "1" will trigger an IPv6 uplink check +pivpnenableipv6="" + +## Enable to skip IPv6 connectivity check and also force client IPv6 traffic +## through wireguard regardless if there is a working IPv6 route on the server. +pivpnforceipv6=0 + +######## SCRIPT ######## + +# Find the rows and columns. Will default to 80x24 if it can not be detected. +screen_size="$(stty size 2> /dev/null || echo 24 80)" +rows="$(echo "${screen_size}" | awk '{print $1}')" +columns="$(echo "${screen_size}" | awk '{print $2}')" + +# Divide by two so the dialogs take up half of the screen, which looks nice. +r=$((rows / 2)) +c=$((columns / 2)) +# Unless the screen is tiny +r=$((r < 20 ? 20 : r)) +c=$((c < 70 ? 70 : c)) + +# Override localization settings so the output is in English language. +export LC_ALL=C + +main() { + # Pre install checks and configs + distroCheck + rootCheck + flagsCheck "$@" + unattendedCheck + checkExistingInstall "$@" + checkHostname + + # Verify there is enough disk space for the install + if [[ "${skipSpaceCheck}" == 'true' ]]; then + echo -n "::: --skip-space-check passed to script, " + echo "skipping free disk space verification!" + else + verifyFreeDiskSpace + fi + + updatePackageCache + notifyPackageUpdatesAvailable + preconfigurePackages + + if [[ "${PLAT}" == 'Alpine' ]]; then + installDependentPackages BASE_DEPS_ALPINE[@] + else + installDependentPackages BASE_DEPS[@] + fi + + welcomeDialogs + + if [[ "${pivpnforceipv6}" -eq 1 ]]; then + echo "::: Forced IPv6 config, skipping IPv6 uplink check!" + pivpnenableipv6=1 + else + if [[ -z "${pivpnenableipv6}" ]] \ + || [[ "${pivpnenableipv6}" -eq 1 ]]; then + checkipv6uplink + fi + + if [[ "${pivpnenableipv6}" -eq 0 ]] \ + && [[ "${pivpnforceipv6route}" -eq 1 ]]; then + askforcedipv6route + fi + fi + + chooseInterface + + if checkStaticIpSupported; then + getStaticIPv4Settings + + if [[ -z "${dhcpReserv}" ]] \ + || [[ "${dhcpReserv}" -ne 1 ]]; then + setStaticIPv4 + fi + else + staticIpNotSupported + fi + + chooseUser + cloneOrUpdateRepos + + # Install + if installPiVPN; then + echo "::: Install Complete..." + else + exit 1 + fi + + restartServices + # Ask if unattended-upgrades will be enabled + askUnattendedUpgrades + + if [[ "${UNATTUPG}" -eq 1 ]]; then + confUnattendedUpgrades + fi + + writeConfigFiles + installScripts + displayFinalMessage + echo ":::" +} + +####### FUNCTIONS ########## + +err() { + echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $*" >&2 +} + +rootCheck() { + ######## FIRST CHECK ######## + # Must be root to install + echo ":::" + + if [[ "${EUID}" -eq 0 ]]; then + echo "::: You are root." + else + echo "::: sudo will be used for the install." + + # Check if it is actually installed + # If it isn't, exit because the install cannot complete + if eval "${CHECK_PKG_INSTALLED} sudo" &> /dev/null; then + export SUDO="sudo" + export SUDOE="sudo -E" + else + err "::: Please install sudo or run this as root." + exit 1 + fi + fi +} + +flagsCheck() { + # Check arguments for the undocumented flags + for ((i = 1; i <= "$#"; i++)); do + j="$((i + 1))" + + case "${!i}" in + "--skip-space-check") + skipSpaceCheck=true + ;; + "--unattended") + runUnattended=true + unattendedConfig="${!j}" + ;; + "--reconfigure") + reconfigure=true + ;; + "--show-unsupported-nics") + showUnsupportedNICs=true + ;; + "--giturl") + pivpnGitUrl="${!j}" + ;; + "--gitbranch") + pivpnGitBranch="${!j}" + ;; + "--noipv6") + pivpnforceipv6=0 + pivpnenableipv6=0 + pivpnforceipv6route=0 + ;; + "--ignoreipv6leak") + pivpnforceipv6route=0 + ;; + esac + done +} + +unattendedCheck() { + if [[ "${runUnattended}" == 'true' ]]; then + echo -n "::: --unattended passed to install script, " + echo "no whiptail dialogs will be displayed" + + if [[ -z "${unattendedConfig}" ]]; then + err "::: No configuration file passed" + exit 1 + else + if [[ -r "${unattendedConfig}" ]]; then + # shellcheck disable=SC1090 + . "${unattendedConfig}" + else + err "::: Can't open ${unattendedConfig}" + exit 1 + fi + fi + fi +} + +checkExistingInstall() { + # see which setup already exists + if [[ -r "${setupConfigDir}/wireguard/${setupVarsFile}" ]]; then + setupVars="${setupConfigDir}/wireguard/${setupVarsFile}" + elif [[ -r "${setupConfigDir}/openvpn/${setupVarsFile}" ]]; then + setupVars="${setupConfigDir}/openvpn/${setupVarsFile}" + fi + + if [[ -r "${setupVars}" ]]; then + if [[ "${reconfigure}" == 'true' ]]; then + echo -n "::: --reconfigure passed to install script, " + echo "will reinstall PiVPN overwriting existing settings" + UpdateCmd="Reconfigure" + elif [[ "${runUnattended}" == 'true' ]]; then + ### What should the script do when passing --unattended to + ### an existing installation? + UpdateCmd="Reconfigure" + else + askAboutExistingInstall "${setupVars}" + fi + fi + + if [[ -z "${UpdateCmd}" ]] \ + || [[ "${UpdateCmd}" == "Reconfigure" ]]; then + : + elif [[ "${UpdateCmd}" == "Update" ]]; then + ${SUDO} "${pivpnScriptDir}/update.sh" "$@" + exit "$?" + elif [[ "${UpdateCmd}" == "Repair" ]]; then + # shellcheck disable=SC1090 + . "${setupVars}" + runUnattended=true + fi +} + +askAboutExistingInstall() { + opt1a="Update" + opt1b="Get the latest PiVPN scripts" + + opt2a="Repair" + opt2b="Reinstall PiVPN using existing settings" + + opt3a="Reconfigure" + opt3b="Reinstall PiVPN with new settings" + + UpdateCmd="$(whiptail \ + --title "Existing Install Detected!" \ + --menu " +We have detected an existing install. +${1} + +Please choose from the following options \ +(Reconfigure can be used to add a second VPN type):" "${r}" "${c}" 3 \ + "${opt1a}" "${opt1b}" \ + "${opt2a}" "${opt2b}" \ + "${opt3a}" "${opt3b}" \ + 3>&2 2>&1 1>&3)" \ + || { + err "::: Cancel selected. Exiting" + exit 1 + } + + echo "::: ${UpdateCmd} option selected." +} + +distroCheck() { + # Check for supported distribution + # Compatibility, functions to check for supported OS + # distroCheck, maybeOSSupport, noOSSupport + # if lsb_release command is on their system + if command -v lsb_release > /dev/null; then + PLAT="$(lsb_release -si)" + OSCN="$(lsb_release -sc)" + else # else get info from os-release + . /etc/os-release + PLAT="$(awk '{print $1}' <<< "${NAME}")" + VER="${VERSION_ID}" + declare -A VER_MAP=(["9"]="stretch" + ["10"]="buster" + ["11"]="bullseye" + ["16.04"]="xenial" + ["18.04"]="bionic" + ["20.04"]="focal" + ["22.04"]="jammy") + OSCN="${VER_MAP["${VER}"]}" + + # Alpine support + if [[ -z "${OSCN}" ]]; then + OSCN="${VER}" + fi + fi + + case "${PLAT}" in + Debian | Raspbian | Ubuntu) + case "${OSCN}" in + stretch | buster | bullseye | xenial | bionic | focal | jammy) + : + ;; + *) + maybeOSSupport + ;; + esac + ;; + Alpine) + PKG_MANAGER='apk' + UPDATE_PKG_CACHE="${PKG_MANAGER} update" + PKG_INSTALL="${PKG_MANAGER} --no-cache add" + PKG_COUNT="${PKG_MANAGER} list -u | wc -l || true" + CHECK_PKG_INSTALLED="${PKG_MANAGER} --no-cache info -e" + ;; + *) + noOSSupport + ;; + esac + + { + echo "PLAT=${PLAT}" + echo "OSCN=${OSCN}" + } > "${tempsetupVarsFile}" +} + +noOSSupport() { + if [[ "${runUnattended}" == 'true' ]]; then + err "::: Invalid OS detected" + err "::: We have not been able to detect a supported OS." + err "::: Currently this installer supports Raspbian, Debian and Ubuntu." + exit 1 + fi + + whiptail \ + --backtitle "INVALID OS DETECTED" \ + --title "Invalid OS" \ + --msgbox "We have not been able to detect a supported OS. +Currently this installer supports Raspbian, Debian and Ubuntu. +For more details, check our documentation at \ +https://github.com/pivpn/pivpn/wiki" "${r}" "${c}" + exit 1 +} + +maybeOSSupport() { + if [[ "${runUnattended}" == 'true' ]]; then + echo "::: OS Not Supported" + echo -n "::: You are on an OS that we have not tested but MAY work, " + echo "continuing anyway..." + return + fi + + if whiptail \ + --backtitle "Untested OS" \ + --title "Untested OS" \ + --yesno "You are on an OS that we have not tested but MAY work. +Currently this installer supports Raspbian, Debian and Ubuntu. +For more details about supported OS please check our documentation \ +at https://github.com/pivpn/pivpn/wiki +Would you like to continue anyway?" "${r}" "${c}"; then + echo "::: Did not detect perfectly supported OS but," + echo -n "::: Continuing installation at user's own " + echo "risk..." + else + err "::: Exiting due to untested OS" + exit 1 + fi +} + +checkHostname() { + # Checks for hostname Length + host_name="$(hostname -s)" + + if [[ "${#host_name}" -gt 28 ]]; then + if [[ "${runUnattended}" == 'true' ]]; then + err "::: Your hostname is too long." + err "::: Use 'hostnamectl set-hostname YOURHOSTNAME' to set a new hostname" + err "::: It must be less then 28 characters long and it must not use special characters" + exit 1 + fi + + until [[ "${#host_name}" -le 28 ]] \ + && [[ "${host_name}" =~ ^[a-zA-Z0-9][a-zA-Z0-9-]{1,28}$ ]]; do + host_name="$(whiptail \ + --title "Hostname too long" \ + --inputbox "Your hostname is too long. +Enter new hostname with less then 28 characters +No special characters allowed." "${r}" "${c}" \ + 3>&1 1>&2 2>&3)" + ${SUDO} hostnamectl set-hostname "${host_name}" + + if [[ "${#host_name}" -le 28 ]] \ + && [[ "${host_name}" =~ ^[a-zA-Z0-9][a-zA-Z0-9-]{1,28}$ ]]; then + echo "::: Hostname valid and length OK, proceeding..." + fi + done + else + echo "::: Hostname length OK" + fi +} + +spinner() { + local pid="${1}" + local delay=0.50 + local spinstr='/-\|' + + while ps a | awk '{print $1}' | grep -q "${pid}"; do + local temp="${spinstr#?}" + printf " [%c] " "${spinstr}" + local spinstr="${temp}${spinstr%"$temp"}" + sleep "${delay}" + printf "\\b\\b\\b\\b\\b\\b" + done + + printf " \\b\\b\\b\\b" +} + +verifyFreeDiskSpace() { + # If user installs unattended-upgrades we'd need about 60MB so + # will check for 75MB free + echo "::: Verifying free disk space..." + local required_free_kilobytes=76800 + local existing_free_kilobytes + existing_free_kilobytes="$(df -Pk \ + | grep -m1 '\/$' \ + | awk '{print $4}')" + + # - Unknown free disk space , not a integer + if [[ ! "${existing_free_kilobytes}" =~ ^([0-9])+$ ]]; then + echo "::: Unknown free disk space!" + echo -n "::: We were unable to determine available free disk space " + echo "on this system." + + if [[ "${runUnattended}" == 'true' ]]; then + exit 1 + fi + + echo -n "::: You may continue with the installation, however, " + echo "it is not recommended." + echo -n "::: If you are sure you want to continue, " + echo -n "type YES and press enter :: " + read -r response + + case "${response}" in + [Yy][Ee][Ss]) + : + ;; + *) + err "::: Confirmation not received, exiting..." + exit 1 + ;; + esac + # - Insufficient free disk space + elif [[ "${existing_free_kilobytes}" -lt "${required_free_kilobytes}" ]]; then + err "::: Insufficient Disk Space!" + err "::: Your system appears to be low on disk space. PiVPN recommends a minimum of ${required_free_kilobytes} KiloBytes." + err "::: You only have ${existing_free_kilobytes} KiloBytes free." + err "::: If this is a new install on a Raspberry Pi you may need to expand your disk." + err "::: Try running 'sudo raspi-config', and choose the 'expand file system option'" + err "::: After rebooting, run this installation again. (curl -sSfL https://install.pivpn.io | bash)" + err "Insufficient free space, exiting..." + exit 1 + fi +} + +updatePackageCache() { + # update package lists + echo ":::" + echo -e "::: Package Cache update is needed, running ${UPDATE_PKG_CACHE} ..." + # shellcheck disable=SC2086 + ${SUDO} ${UPDATE_PKG_CACHE} &> /dev/null & + spinner "$!" + echo " done!" +} + +notifyPackageUpdatesAvailable() { + # Let user know if they have outdated packages on their system and + # advise them to run a package update at soonest possible. + echo ":::" + echo -n "::: Checking ${PKG_MANAGER} for upgraded packages...." + updatesToInstall="$(eval "${PKG_COUNT}")" + echo " done!" + echo ":::" + + if [[ "${updatesToInstall}" -eq 0 ]]; then + echo "::: Your system is up to date! Continuing with PiVPN installation..." + else + echo "::: There are ${updatesToInstall} updates available for your system!" + echo "::: We recommend you update your OS after installing PiVPN! " + echo ":::" + fi +} + +preconfigurePackages() { + # Install packages used by this installation script + # If apt is older than 1.5 we need to install an additional package to add + # support for https repositories that will be used later on + if [[ "${PKG_MANAGER}" == 'apt-get' ]] \ + && [[ -f /etc/apt/sources.list ]]; then + INSTALLED_APT="$(apt-cache policy apt \ + | grep -m1 'Installed: ' \ + | grep -v '(none)' \ + | awk '{print $2}')" + + if dpkg --compare-versions "${INSTALLED_APT}" lt 1.5; then + BASE_DEPS+=("apt-transport-https") + fi + fi + + # We set static IP only on Raspberry Pi OS + if checkStaticIpSupported; then + BASE_DEPS+=(dhcpcd5) + fi + + if [[ "${PKG_MANAGER}" == 'apt-get' ]]; then + DPKG_ARCH="$(dpkg --print-architecture)" + elif [[ "${PKG_MANAGER}" == 'apk' ]]; then + DPKG_ARCH="$(apk --print-arch)" + fi + + if [[ "${PKG_MANAGER}" == 'apt-get' ]]; then + AVAILABLE_OPENVPN="$(apt-cache policy openvpn \ + | grep -m1 'Candidate: ' \ + | grep -v '(none)' \ + | awk '{print $2}')" + elif [[ "${PKG_MANAGER}" == 'apk' ]]; then + AVAILABLE_OPENVPN="$(apk search -e openvpn \ + | sed -E -e 's/openvpn\-(.*)/\1/')" + fi + + OPENVPN_SUPPORT=0 + NEED_OPENVPN_REPO=0 + + # We require OpenVPN 2.4 or later for ECC support. If not available in the + # repositories but we are running x86 Debian or Ubuntu, add the official repo + # which provides the updated package. + if [[ "${PKG_MANAGER}" == 'apt-get' ]]; then + if [[ -n "${AVAILABLE_OPENVPN}" ]] \ + && dpkg --compare-versions "${AVAILABLE_OPENVPN}" ge 2.4; then + OPENVPN_SUPPORT=1 + else + if [[ "${PLAT}" == "Debian" ]] \ + || [[ "${PLAT}" == "Ubuntu" ]]; then + if [[ "${DPKG_ARCH}" == "amd64" ]] \ + || [[ "${DPKG_ARCH}" == "i386" ]]; then + NEED_OPENVPN_REPO=1 + OPENVPN_SUPPORT=1 + else + OPENVPN_SUPPORT=0 + fi + else + OPENVPN_SUPPORT=0 + fi + fi + elif [[ "${PKG_MANAGER}" == 'apk' ]]; then + if [[ -n "${AVAILABLE_OPENVPN}" ]] \ + && [[ "$(apk version -t "${AVAILABLE_OPENVPN}" 2.4)" == '>' ]]; then + OPENVPN_SUPPORT=1 + else + OPENVPN_SUPPORT=0 + fi + fi + + if [[ "${PKG_MANAGER}" == 'apt-get' ]]; then + AVAILABLE_WIREGUARD="$(apt-cache policy wireguard \ + | grep -m1 'Candidate: ' \ + | grep -v '(none)' \ + | awk '{print $2}')" + elif [[ "${PKG_MANAGER}" == 'apk' ]]; then + AVAILABLE_WIREGUARD="$(apk search -e wireguard-tools \ + | sed -E -e 's/wireguard\-tools\-(.*)/\1/')" + fi + + WIREGUARD_SUPPORT=0 + + # If a wireguard kernel object is found and is part of any installed package, + # then it has not been build via DKMS or manually (installing via + # wireguard-dkms does not make the module part of the package since the + # module itself is built at install time and not part of the .deb). + # Source: https://github.com/MichaIng/DietPi/blob/7bf5e1041f3b2972d7827c48215069d1c90eee07/dietpi/dietpi-software#L1807-L1815 + WIREGUARD_BUILTIN=0 + + if [[ "${PKG_MANAGER}" == 'apt-get' ]]; then + if dpkg-query -S '/lib/modules/*/wireguard.ko*' &> /dev/null \ + || modinfo wireguard 2> /dev/null \ + | grep -q '^filename:[[:blank:]]*(builtin)$'; then + WIREGUARD_BUILTIN=1 + fi + fi + + # case 1: If the module is builtin and the package available, + # we only need to install wireguard-tools. + # case 2: If the package is not available, on Debian and + # Raspbian we can add it via Bullseye repository. + # case 3: If the module is not builtin, on Raspbian we know + # the headers package: raspberrypi-kernel-headers + # case 4: On Alpine, the kernel must be linux-lts or linux-virt + # if we want to load the kernel module + # case 5: On Alpine Docker Container, the responsibility to have + # a WireGuard module on the host system is at user side + # case 6: On Debian (and Ubuntu), we can only reliably assume the + # headers package for amd64: linux-image-amd64 + # case 7: On Ubuntu, additionally the WireGuard package needs to + # be available, since we didn't test mixing Ubuntu repositories. + # case 8: Ubuntu focal has wireguard support + + if [[ "${WIREGUARD_BUILTIN}" -eq 1 && -n "${AVAILABLE_WIREGUARD}" ]] \ + || [[ "${WIREGUARD_BUILTIN}" -eq 1 && ("${PLAT}" == 'Debian' || "${PLAT}" == 'Raspbian') ]] \ + || [[ "${PLAT}" == 'Raspbian' ]] \ + || [[ "${PLAT}" == 'Alpine' && ! -f /.dockerenv && "$(uname -mrs)" =~ ^Linux\ +[0-9\.\-]+\-((lts)|(virt))\ +.*$ ]] \ + || [[ "${PLAT}" == 'Alpine' && -f /.dockerenv ]] \ + || [[ "${PLAT}" == 'Debian' && "${DPKG_ARCH}" == 'amd64' ]] \ + || [[ "${PLAT}" == 'Ubuntu' && "${DPKG_ARCH}" == 'amd64' && -n "${AVAILABLE_WIREGUARD}" ]] \ + || [[ "${PLAT}" == 'Ubuntu' && "${DPKG_ARCH}" == 'arm64' && "${OSCN}" == 'focal' && -n "${AVAILABLE_WIREGUARD}" ]]; then + WIREGUARD_SUPPORT=1 + fi + + if [[ "${OPENVPN_SUPPORT}" -eq 0 ]] \ + && [[ "${WIREGUARD_SUPPORT}" -eq 0 ]]; then + err "::: Neither OpenVPN nor WireGuard are available to install by PiVPN, exiting..." + exit 1 + fi + + # if ufw is enabled, configure that. + # running as root because sometimes the executable is not in the user's $PATH + if ${SUDO} bash -c 'command -v ufw' > /dev/null; then + if ! ${SUDO} ufw status || ${SUDO} ufw status | grep -q inactive; then + USING_UFW=0 + else + USING_UFW=1 + fi + else + USING_UFW=0 + fi + + if [[ "${PKG_MANAGER}" == 'apt-get' ]] && [[ "${USING_UFW}" -eq 0 ]]; then + BASE_DEPS+=(iptables-persistent) + echo iptables-persistent iptables-persistent/autosave_v4 boolean true \ + | ${SUDO} debconf-set-selections + echo iptables-persistent iptables-persistent/autosave_v6 boolean false \ + | ${SUDO} debconf-set-selections + fi + + if [[ "${PLAT}" == 'Alpine' ]] \ + && ! command -v grepcidr &> /dev/null; then + local down_dir + ## install dependencies + # shellcheck disable=SC2086 + ${SUDO} ${PKG_INSTALL} build-base make curl tar + + if ! down_dir="$(mktemp -d)"; then + err "::: Failed to create download directory for grepcidr!" + exit 1 + fi + + ## download binaries + curl -fLo "${down_dir}/master.tar.gz" \ + https://github.com/pivpn/grepcidr/archive/master.tar.gz + tar -xzC "${down_dir}" -f "${down_dir}/master.tar.gz" + + ( + cd "${down_dir}/grepcidr-master" || exit + + ## personalize binaries + sed -i -E -e 's/^PREFIX\=.*/PREFIX\=\/usr\nCC\=gcc/' Makefile + + ## install + make + ${SUDO} make install + + if ! command -v grepcidr &> /dev/null; then + err "::: Failed to compile and install grepcidr!" + exit + fi + ) || exit 1 + fi + + echo "USING_UFW=${USING_UFW}" >> "${tempsetupVarsFile}" +} + +installDependentPackages() { + # Install packages passed via argument array + # No spinner - conflicts with set -e + local FAILED=0 + local APTLOGFILE + declare -a TO_INSTALL=() + declare -a argArray1=("${!1}") + + for i in "${argArray1[@]}"; do + echo -n "::: Checking for ${i}..." + + if [[ "${PKG_MANAGER}" == 'apt-get' ]]; then + if dpkg-query -W -f='${Status}' "${i}" 2> /dev/null \ + | grep -q "ok installed"; then + echo " already installed!" + else + echo " not installed!" + # Add this package to the list of packages in the argument array that + # need to be installed + TO_INSTALL+=("${i}") + fi + elif [[ "${PKG_MANAGER}" == 'apk' ]]; then + if eval "${SUDO} ${CHECK_PKG_INSTALLED} ${i}" &> /dev/null; then + echo " already installed!" + else + echo " not installed!" + # Add this package to the list of packages in the argument array that + # need to be installed + TO_INSTALL+=("${i}") + fi + fi + done + + APTLOGFILE="$(${SUDO} mktemp)" + + # shellcheck disable=SC2086 + ${SUDO} ${PKG_INSTALL} "${TO_INSTALL[@]}" + + for i in "${TO_INSTALL[@]}"; do + if [[ "${PKG_MANAGER}" == 'apt-get' ]]; then + if dpkg-query -W -f='${Status}' "${i}" 2> /dev/null \ + | grep -q "ok installed"; then + echo "::: Package ${i} successfully installed!" + # Add this package to the total list of packages that were actually + # installed by the script + INSTALLED_PACKAGES+=("${i}") + else + echo "::: Failed to install ${i}!" + ((FAILED++)) + fi + elif [[ "${PKG_MANAGER}" == 'apk' ]]; then + if eval "${SUDO} ${CHECK_PKG_INSTALLED} ${i}" &> /dev/null; then + echo "::: Package ${i} successfully installed!" + # Add this package to the total list of packages that were actually + # installed by the script + INSTALLED_PACKAGES+=("${i}") + else + echo "::: Failed to install ${i}!" + ((FAILED++)) + fi + fi + done + + if [[ "${FAILED}" -gt 0 ]]; then + echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]:" >&2 + ${SUDO} cat "${APTLOGFILE}" >&2 + exit 1 + fi +} + +welcomeDialogs() { + if [[ "${runUnattended}" == 'true' ]]; then + echo "::: PiVPN Automated Installer" + echo -n "::: This installer will transform your ${PLAT} host into an " + echo "OpenVPN or WireGuard server!" + echo "::: Initiating network interface" + return + fi + + # Display the welcome dialog + whiptail \ + --backtitle "Welcome" \ + --title "PiVPN Automated Installer" \ + --msgbox "This installer will transform your Raspberry Pi into an \ +OpenVPN or WireGuard server!" "${r}" "${c}" + + # Explain the need for a static address + whiptail \ + --backtitle "Initiating network interface" \ + --title "Static IP Needed" \ + --msgbox "The PiVPN is a SERVER so it needs a STATIC IP ADDRESS to \ +function properly. + +In the next section, you can choose to use your current network settings \ +(DHCP) or to manually edit them." "${r}" "${c}" +} + +chooseInterface() { + # Find interfaces and let the user choose one + + # Turn the available interfaces into an array so it can be used with + # a whiptail dialog + local interfacesArray=() + # Number of available interfaces + local interfaceCount + # Whiptail variable storage + local chooseInterfaceCmd + # Temporary Whiptail options storage + local chooseInterfaceOptions + # Loop sentinel variable + local firstloop=1 + + availableInterfaces="$(ip -o link)" + + if [[ "${showUnsupportedNICs}" == 'true' ]]; then + # Show every network interface, could be useful for those who + # install PiVPN inside virtual machines or on Raspberry Pis + # with USB adapters + availableInterfaces="$(echo "${availableInterfaces}" \ + | awk '{print $2}')" + else + # Find network interfaces whose state is UP + availableInterfaces="$(echo "${availableInterfaces}" \ + | awk '/state UP/ {print $2}')" + fi + + # Skip virtual, loopback and docker interfaces + availableInterfaces="$(echo "${availableInterfaces}" \ + | cut -d ':' -f 1 \ + | cut -d '@' -f 1 \ + | grep -v -w 'lo' \ + | grep -v '^docker')" + + if [[ -z "${availableInterfaces}" ]]; then + err "::: Could not find any active network interface, exiting" + exit 1 + else + while read -r line; do + mode="OFF" + + if [[ "${firstloop}" -eq 1 ]]; then + firstloop=0 + mode="ON" + fi + + interfacesArray+=("${line}" "available" "${mode}") + ((interfaceCount++)) + done <<< "${availableInterfaces}" + fi + + if [[ "${runUnattended}" == 'true' ]]; then + if [[ -z "${IPv4dev}" ]]; then + if [[ "${interfaceCount}" -eq 1 ]]; then + IPv4dev="${availableInterfaces}" + echo -n "::: No interface specified for IPv4, but only ${IPv4dev} " + echo "is available, using it" + else + err "::: No interface specified for IPv4 and failed to determine one" + exit 1 + fi + else + if ip -o link | grep -qw "${IPv4dev}"; then + echo "::: Using interface: ${IPv4dev} for IPv4" + else + err "::: Interface ${IPv4dev} for IPv4 does not exist" + exit 1 + fi + fi + + if [[ "${pivpnenableipv6}" -eq 1 ]]; then + if [[ -z "${IPv6dev}" ]]; then + if [[ "${interfaceCount}" -eq 1 ]]; then + IPv6dev="${availableInterfaces}" + echo -n "::: No interface specified for IPv6, but only ${IPv6dev} " + echo "is available, using it" + else + err "::: No interface specified for IPv6 and failed to determine one" + exit 1 + fi + else + if ip -o link | grep -qw "${IPv6dev}"; then + echo "::: Using interface: ${IPv6dev} for IPv6" + else + err "::: Interface ${IPv6dev} for IPv6 does not exist" + exit 1 + fi + fi + fi + + { + echo "IPv4dev=${IPv4dev}" + + if [[ "${pivpnenableipv6}" -eq 1 ]] \ + && [[ -z "${IPv6dev}" ]]; then + echo "IPv6dev=${IPv6dev}" + fi + } >> "${tempsetupVarsFile}" + + return + else + if [[ "${interfaceCount}" -eq 1 ]]; then + IPv4dev="${availableInterfaces}" + + { + echo "IPv4dev=${IPv4dev}" + + if [[ "${pivpnenableipv6}" -eq 1 ]]; then + IPv6dev="${availableInterfaces}" + echo "IPv6dev=${IPv6dev}" + fi + } >> "${tempsetupVarsFile}" + + return + fi + fi + + chooseInterfaceCmd=(whiptail + --separate-output + --radiolist "Choose An interface for IPv4 \ +(press space to select):" "${r}" "${c}" "${interfaceCount}") + + if chooseInterfaceOptions="$("${chooseInterfaceCmd[@]}" \ + "${interfacesArray[@]}" \ + 2>&1 > /dev/tty)"; then + for desiredInterface in ${chooseInterfaceOptions}; do + IPv4dev="${desiredInterface}" + echo "::: Using interface: ${IPv4dev}" + echo "IPv4dev=${IPv4dev}" >> "${tempsetupVarsFile}" + done + else + err "::: Cancel selected, exiting...." + exit 1 + fi + + if [[ "${pivpnenableipv6}" -eq 1 ]]; then + chooseInterfaceCmd=(whiptail + --separate-output + --radiolist "Choose An interface for IPv6, usually the same as used by \ +IPv4 (press space to select):" "${r}" "${c}" "${interfaceCount}") + + if chooseInterfaceOptions="$("${chooseInterfaceCmd[@]}" \ + "${interfacesArray[@]}" \ + 2>&1 > /dev/tty)"; then + for desiredInterface in ${chooseInterfaceOptions}; do + IPv6dev="${desiredInterface}" + echo "::: Using interface: ${IPv6dev}" + echo "IPv6dev=${IPv6dev}" >> "${tempsetupVarsFile}" + done + else + err "::: Cancel selected, exiting...." + exit 1 + fi + fi +} + +checkStaticIpSupported() { + # Not really robust and correct, we should actually check for dhcpcd, + # not the distro, but works on Raspbian and Debian. + if [[ "${PLAT}" == "Raspbian" ]]; then + return 0 + # If we are on 'Debian' but the raspi.list file is present, + # then we actually are on 64-bit Raspberry Pi OS. + elif [[ "${PLAT}" == "Debian" ]] \ + && [[ -s /etc/apt/sources.list.d/raspi.list ]]; then + return 0 + else + return 1 + fi +} + +staticIpNotSupported() { + if [[ "${runUnattended}" == 'true' ]]; then + echo -n "::: Since we think you are not using Raspberry Pi OS, " + echo "we will not configure a static IP for you." + return + fi + + # If we are in Ubuntu then they need to have previously set their network, + # so just use what you have. + whiptail \ + --backtitle "IP Information" \ + --title "IP Information" \ + --msgbox "Since we think you are not using Raspberry Pi OS, we will not \ +configure a static IP for you. +If you are in Amazon then you can not configure a static IP anyway. Just \ +ensure before this installer started you had set an elastic IP on your \ +instance." "${r}" "${c}" +} + +validIP() { + local ip="${1}" + local stat=1 + + if [[ "${ip}" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then + OIFS="${IFS}" + IFS='.' + read -r -a ip <<< "${ip}" + IFS="${OIFS}" + + [[ "${ip[0]}" -le 255 && "${ip[1]}" -le 255 && "${ip[2]}" -le 255 && "${ip[3]}" -le 255 ]] + + stat="$?" + fi + + return "${stat}" +} + +validIPAndNetmask() { + # shellcheck disable=SC2178 + local ip="${1}" + local stat=1 + + # shellcheck disable=SC2178 + ip="${ip/\//.}" + + # shellcheck disable=SC2128 + if [[ "${ip}" =~ ^([0-9]{1,3}\.){4}[0-9]{1,2}$ ]]; then + OIFS="${IFS}" + IFS='.' + # shellcheck disable=SC2128 + read -r -a ip <<< "${ip}" + IFS="${OIFS}" + + [[ "${ip[0]}" -le 255 && "${ip[1]}" -le 255 && "${ip[2]}" -le 255 && "${ip[3]}" -le 255 && "${ip[4]}" -le 32 ]] + + stat="$?" + fi + + return "${stat}" +} + +checkipv6uplink() { + curl \ + --max-time 3 \ + --connect-timeout 3 \ + --silent \ + --fail \ + -6 \ + https://google.com \ + > /dev/null + curlv6testres="$?" + + if [[ "${curlv6testres}" -ne 0 ]]; then + echo -n "::: IPv6 test connections to google.com have failed. " + echo -n "Disabling IPv6 support. " + echo "(The curl test failed with code: ${curlv6testres})" + pivpnenableipv6=0 + else + echo -n "::: IPv6 test connections to google.com successful. " + echo "Enabling IPv6 support." + pivpnenableipv6=1 + fi +} + +askforcedipv6route() { + if [[ "${runUnattended}" == 'true' ]]; then + echo "::: Enable forced IPv6 route with no IPv6 uplink on server." + echo "pivpnforceipv6route=${pivpnforceipv6route}" >> "${tempsetupVarsFile}" + return + fi + + if whiptail \ + --backtitle "Privacy setting" \ + --title "IPv6 leak" \ + --yesno "Although this server doesn't seem to have a working IPv6 \ +connection or IPv6 was disabled on purpose, it is still recommended you \ +force all IPv6 connections through the VPN.\\n\\nThis will prevent the \ +client from bypassing the tunnel and leaking its real IPv6 address to servers, \ +though it might cause the client to have slow response when browsing the web \ +on IPv6 networks. + +Do you want to force routing IPv6 to block the leakage?" "${r}" "${c}"; then + pivpnforceipv6route=1 + else + pivpnforceipv6route=0 + fi + + echo "pivpnforceipv6route=${pivpnforceipv6route}" >> "${tempsetupVarsFile}" +} + +getStaticIPv4Settings() { + # Find the gateway IP used to route to outside world + CurrentIPv4gw="$(ip -o route get 192.0.2.1 \ + | grep -oE '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' \ + | awk 'NR==2')" + + # Find the IP address (and netmask) of the desidered interface + CurrentIPv4addr="$(ip -o -f inet address show dev "${IPv4dev}" \ + | grep -oE '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\/[0-9]{1,2}')" + + # Grab their current DNS servers + IPv4dns="$(grep -v "^#" /etc/resolv.conf \ + | grep -w nameserver \ + | grep -oE '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' \ + | xargs)" + + if [[ "${runUnattended}" == 'true' ]]; then + if [[ -z "${dhcpReserv}" ]] \ + || [[ "${dhcpReserv}" -ne 1 ]]; then + local MISSING_STATIC_IPV4_SETTINGS=0 + + if [[ -z "${IPv4addr}" ]]; then + echo "::: Missing static IP address" + ((MISSING_STATIC_IPV4_SETTINGS++)) + fi + + if [[ -z "${IPv4gw}" ]]; then + echo "::: Missing static IP gateway" + ((MISSING_STATIC_IPV4_SETTINGS++)) + fi + + if [[ "${MISSING_STATIC_IPV4_SETTINGS}" -eq 0 ]]; then + # If both settings are not empty, check if they are valid and proceed + if validIPAndNetmask "${IPv4addr}"; then + echo "::: Your static IPv4 address: ${IPv4addr}" + else + err "::: ${IPv4addr} is not a valid IP address" + exit 1 + fi + + if validIP "${IPv4gw}"; then + echo "::: Your static IPv4 gateway: ${IPv4gw}" + else + err "::: ${IPv4gw} is not a valid IP address" + exit 1 + fi + elif [[ "${MISSING_STATIC_IPV4_SETTINGS}" -eq 1 ]]; then + # If either of the settings is missing, consider the input inconsistent + err "::: Incomplete static IP settings" + exit 1 + elif [[ "${MISSING_STATIC_IPV4_SETTINGS}" -eq 2 ]]; then + # If both of the settings are missing, + # assume the user wants to use current settings + IPv4addr="${CurrentIPv4addr}" + IPv4gw="${CurrentIPv4gw}" + echo "::: No static IP settings, using current settings" + echo "::: Your static IPv4 address: ${IPv4addr}" + echo "::: Your static IPv4 gateway: ${IPv4gw}" + fi + else + echo "::: Skipping setting static IP address" + fi + + { + echo "dhcpReserv=${dhcpReserv}" + echo "IPv4addr=${IPv4addr}" + echo "IPv4gw=${IPv4gw}" + } >> "${tempsetupVarsFile}" + return + fi + + local ipSettingsCorrect + local IPv4AddrValid + local IPv4gwValid + # Some users reserve IP addresses on another DHCP Server or on their routers, + # Lets ask them if they want to make any changes to their interfaces. + + if whiptail \ + --backtitle "Calibrating network interface" \ + --title "DHCP Reservation" \ + --defaultno \ + --yesno "Are you Using DHCP Reservation on your Router/DHCP Server? +These are your current Network Settings: + + IP address: ${CurrentIPv4addr} + Gateway: ${CurrentIPv4gw} + +Yes: Keep using DHCP reservation +No: Setup static IP address +Don't know what DHCP Reservation is? Answer No." "${r}" "${c}"; then + dhcpReserv=1 + + { + echo "dhcpReserv=${dhcpReserv}" + # We don't really need to save them as we won't set a static IP + # but they might be useful for debugging + echo "IPv4addr=${CurrentIPv4addr}" + echo "IPv4gw=${CurrentIPv4gw}" + } >> "${tempsetupVarsFile}" + else + # Ask if the user wants to use DHCP settings as their static IP + if whiptail \ + --backtitle "Calibrating network interface" \ + --title "Static IP Address" \ + --yesno "Do you want to use your current network settings as a static \ +address? + + IP address: ${CurrentIPv4addr} + Gateway: ${CurrentIPv4gw}" "${r}" "${c}"; then + IPv4addr="${CurrentIPv4addr}" + IPv4gw="${CurrentIPv4gw}" + + { + echo "IPv4addr=${IPv4addr}" + echo "IPv4gw=${IPv4gw}" + } >> "${tempsetupVarsFile}" + + # If they choose yes, let the user know that the IP address will not + # be available via DHCP and may cause a conflict. + whiptail \ + --backtitle "IP information" \ + --title "FYI: IP Conflict" \ + --msgbox "It is possible your router could still try to assign this \ +IP to a device, which would cause a conflict. But in most cases the router is \ +smart enough to not do that. +If you are worried, either manually set the address, or modify the DHCP \ +reservation pool so it does not include the IP you want. +It is also possible to use a DHCP reservation, but if you are going to do \ +that, you mightas well set a static address." "${r}" "${c}" + # Nothing else to do since the variables are already set above + else + # Otherwise, we need to ask the user to input their desired settings. + # Start by getting the IPv4 address + # (pre-filling it with info gathered from DHCP) + # Start a loop to let the user enter their information with the chance + # to go back and edit it if necessary + until [[ "${ipSettingsCorrect}" == 'true' ]]; do + until [[ "${IPv4AddrValid}" == 'true' ]]; do + # Ask for the IPv4 address + if IPv4addr="$(whiptail \ + --backtitle "Calibrating network interface" \ + --title "IPv4 address" \ + --inputbox "Enter your desired \ +IPv4 address" "${r}" "${c}" "${CurrentIPv4addr}" \ + 3>&1 1>&2 2>&3)"; then + if validIPAndNetmask "${IPv4addr}"; then + echo "::: Your static IPv4 address: ${IPv4addr}" + IPv4AddrValid=true + else + whiptail \ + --backtitle "Calibrating network interface" \ + --title "IPv4 address" \ + --msgbox "You've entered an invalid IP address: ${IPv4addr} + +Please enter an IP address in the CIDR notation, example: 192.168.23.211/24 + +If you are not sure, please just keep the default." "${r}" "${c}" + echo "::: Invalid IPv4 address: ${IPv4addr}" + IPv4AddrValid=false + fi + else + # Cancelling IPv4 settings window + err "::: Cancel selected. Exiting..." + exit 1 + fi + done + + until [[ "${IPv4gwValid}" == 'true' ]]; do + # Ask for the gateway + if IPv4gw="$(whiptail \ + --backtitle "Calibrating network interface" \ + --title "IPv4 gateway (router)" \ + --inputbox "Enter your desired IPv4 \ +default gateway" "${r}" "${c}" "${CurrentIPv4gw}" \ + 3>&1 1>&2 2>&3)"; then + if validIP "${IPv4gw}"; then + echo "::: Your static IPv4 gateway: ${IPv4gw}" + IPv4gwValid=true + else + whiptail \ + --backtitle "Calibrating network interface" \ + --title "IPv4 gateway (router)" \ + --msgbox "You've entered an invalid gateway IP: ${IPv4gw} + +Please enter the IP address of your gateway (router), example: 192.168.23.1 + +If you are not sure, please just keep the default." "${r}" "${c}" + echo "::: Invalid IPv4 gateway: ${IPv4gw}" + IPv4gwValid=false + fi + else + # Cancelling gateway settings window + err "::: Cancel selected. Exiting..." + exit 1 + fi + done + + # Give the user a chance to review their settings before moving on + if whiptail \ + --backtitle "Calibrating network interface" \ + --title "Static IP Address" \ + --yesno "Are these settings correct? + + IP address: ${IPv4addr} + Gateway: ${IPv4gw}" "${r}" "${c}"; then + # If the settings are correct, then we need to set the pivpnIP + echo "IPv4addr=${IPv4addr}" >> "${tempsetupVarsFile}" + echo "IPv4gw=${IPv4gw}" >> "${tempsetupVarsFile}" + # After that's done, the loop ends and we move on + ipSettingsCorrect=true + else + # If the settings are wrong, the loop continues + ipSettingsCorrect=false + IPv4AddrValid=false + IPv4gwValid=false + fi + done + # End the if statement for DHCP vs. static + fi + # End of If Statement for DCHCP Reservation + fi +} + +setDHCPCD() { + # Append these lines to dhcpcd.conf to enable a static IP + { + echo "interface ${IPv4dev}" + echo "static ip_address=${IPv4addr}" + echo "static routers=${IPv4gw}" + echo "static domain_name_servers=${IPv4dns}" + } | ${SUDO} tee -a "${dhcpcdFile}" > /dev/null +} + +setStaticIPv4() { + # Tries to set the IPv4 address + if [[ -f /etc/dhcpcd.conf ]]; then + if grep -q "${IPv4addr}" "${dhcpcdFile}"; then + echo "::: Static IP already configured." + else + setDHCPCD + ${SUDO} ip addr replace dev "${IPv4dev}" "${IPv4addr}" + echo ":::" + echo -n "::: Setting IP to ${IPv4addr}. " + echo "You may need to restart after the install is complete." + echo ":::" + fi + else + err "::: Critical: Unable to locate configuration file to set static IPv4 address!" + exit 1 + fi +} + +chooseUser() { + # Choose the user for the ovpns + if [[ "${runUnattended}" == 'true' ]]; then + if [[ -z "${install_user}" ]]; then + if [[ "$(awk -F ':' \ + 'BEGIN {count=0} $3>=1000 && $3<=60000 { count++ } END{ print count }' \ + /etc/passwd)" -eq 1 ]]; then + install_user="$(awk -F ':' \ + '$3>=1000 && $3<=60000 {print $1}' \ + /etc/passwd)" + echo -n "::: No user specified, but only ${install_user} is available, " + echo "using it" + else + err "::: No user specified" + exit 1 + fi + else + if awk -F':' '$3>=1000 && $3<=60000 {print $1}' /etc/passwd \ + | grep -qw "${install_user}"; then + echo "::: ${install_user} will hold your ovpn configurations." + else + echo "::: User ${install_user} does not exist, creating..." + + if [[ "${PLAT}" == 'Alpine' ]]; then + ${SUDO} adduser -s /bin/bash "${install_user}" + ${SUDO} addgroup "${install_user}" wheel + else + ${SUDO} useradd -ms /bin/bash "${install_user}" + fi + + echo -n "::: User created without a password, " + echo "please do sudo passwd ${install_user} to create one" + fi + fi + + install_home="$(grep -m1 "^${install_user}:" /etc/passwd \ + | cut -d ':' -f 6)" + install_home="${install_home%/}" + + { + echo "install_user=${install_user}" + echo "install_home=${install_home}" + } >> "${tempsetupVarsFile}" + return + fi + + # Explain the local user + whiptail \ + --msgbox \ + --backtitle "Parsing User List" \ + --title "Local Users" \ + "Choose a local user that will hold your ovpn configurations." \ + "${r}" \ + "${c}" + # First, let's check if there is a user available. + numUsers="$(awk -F ':' \ + 'BEGIN {count=0} $3>=1000 && $3<=60000 { count++ } END{ print count }' \ + /etc/passwd)" + + if [[ "${numUsers}" -eq 0 ]]; then + # We don't have a user, let's ask to add one. + if userToAdd="$(whiptail \ + --title "Choose A User" \ + --inputbox \ + "No non-root user account was found. Please type a new username." \ + "${r}" \ + "${c}" \ + 3>&1 1>&2 2>&3)"; then + # See https://askubuntu.com/a/667842/459815 + PASSWORD="$(whiptail \ + --title "password dialog" \ + --passwordbox \ + "Please enter the new user password" \ + "${r}" \ + "${c}" \ + 3>&1 1>&2 2>&3)" + CRYPT="$(perl \ + -e 'printf("%s\n", crypt($ARGV[0], "password"))' "${PASSWORD}")" + + if [[ "${PLAT}" == 'Alpine' ]]; then + if ${SUDO} adduser -Ds /bin/bash "${userToAdd}"; then + ${SUDO} addgroup "${userToAdd}" wheel + + ${SUDO} chpasswd <<< "${userToAdd}:${PASSWORD}" + ${SUDO} passwd -u "${userToAdd}" + + echo "Succeeded" + ((numUsers += 1)) + else + exit 1 + fi + else + if ${SUDO} useradd -mp "${CRYPT}" -s /bin/bash "${userToAdd}"; then + echo "Succeeded" + ((numUsers += 1)) + else + exit 1 + fi + fi + else + exit 1 + fi + fi + + availableUsers="$(awk -F':' '$3>=1000 && $3<=60000 {print $1}' /etc/passwd)" + local userArray=() + local firstloop=1 + + while read -r line; do + mode="OFF" + + if [[ "${firstloop}" -eq 1 ]]; then + firstloop=0 + mode="ON" + fi + + userArray+=("${line}" "" "${mode}") + done <<< "${availableUsers}" + + chooseUserCmd=(whiptail + --title "Choose A User" + --separate-output + --radiolist + "Choose (press space to select):" + "${r}" + "${c}" + "${numUsers}") + + if chooseUserOptions=$("${chooseUserCmd[@]}" \ + "${userArray[@]}" \ + 2>&1 > /dev/tty); then + for desiredUser in ${chooseUserOptions}; do + install_user=${desiredUser} + echo "::: Using User: ${install_user}" + install_home=$(grep -m1 "^${install_user}:" /etc/passwd \ + | cut -d ':' -f 6) + install_home=${install_home%/} # remove possible trailing slash + + { + echo "install_user=${install_user}" + echo "install_home=${install_home}" + } >> "${tempsetupVarsFile}" + done + else + err "::: Cancel selected, exiting...." + exit 1 + fi +} + +isRepo() { + # If the directory does not have a .git folder it is not a repo + echo -n "::: Checking ${1} is a repo..." + cd "${1}" &> /dev/null || { + echo " not found!" + return 1 + } + ${SUDO} git status &> /dev/null && echo " OK!" + return 0 || echo " not found!" + return 1 +} + +updateRepo() { + if [[ "${UpdateCmd}" == "Repair" ]]; then + echo -n "::: Repairing an existing installation, " + echo "not downloading/updating local repos" + else + # Pull the latest commits + echo -n "::: Updating repo in ${1} from ${2} ..." + + ### FIXME: Never call rm -rf with a plain variable. Never again as SU! + #${SUDO} rm -rf "${1}" + if [[ -n "${1}" ]]; then + ${SUDO} rm -rf "$(dirname "${1}")/pivpn" + fi + + # Go back to /usr/local/src otherwise git will complain when the current + # working directory has just been deleted (/usr/local/src/pivpn). + cd /usr/local/src \ + && ${SUDO} git clone -q \ + --depth 1 \ + --no-single-branch \ + "${2}" \ + "${1}" \ + > /dev/null & + spinner $! + cd "${1}" || exit 1 + echo " done!" + + if [[ -n "${pivpnGitBranch}" ]]; then + echo "::: Checkout branch '${pivpnGitBranch}' from ${2} in ${1}..." + ${SUDOE} git checkout -q "${pivpnGitBranch}" + echo "::: Custom branch checkout done!" + elif [[ -z "${TESTING+x}" ]]; then + : + else + echo "::: Checkout branch 'test' from ${2} in ${1}..." + ${SUDOE} git checkout -q test + echo "::: 'test' branch checkout done!" + fi + fi +} + +makeRepo() { + # Remove the non-repos interface and clone the interface + echo -n "::: Cloning ${2} into ${1} ..." + + ### FIXME: Never call rm -rf with a plain variable. Never again as SU! + #${SUDO} rm -rf "${1}" + if [[ -n "${1}" ]]; then + ${SUDO} rm -rf "$(dirname "${1}")/pivpn" + fi + + # Go back to /usr/local/src otherwhise git will complain when the current + # working directory has just been deleted (/usr/local/src/pivpn). + cd /usr/local/src \ + && ${SUDO} git clone -q \ + --depth 1 \ + --no-single-branch \ + "${2}" \ + "${1}" \ + > /dev/null & + spinner $! + cd "${1}" || exit 1 + echo " done!" + + if [[ -n "${pivpnGitBranch}" ]]; then + echo "::: Checkout branch '${pivpnGitBranch}' from ${2} in ${1}..." + ${SUDOE} git checkout -q "${pivpnGitBranch}" + echo "::: Custom branch checkout done!" + elif [[ -z "${TESTING+x}" ]]; then + : + else + echo "::: Checkout branch 'test' from ${2} in ${1}..." + ${SUDOE} git checkout -q test + echo "::: 'test' branch checkout done!" + fi +} + +getGitFiles() { + # Setup git repos for base files + echo ":::" + echo "::: Checking for existing base files..." + + if isRepo "${1}"; then + updateRepo "${1}" "${2}" + else + makeRepo "${1}" "${2}" + fi +} + +cloneOrUpdateRepos() { + # Clone/Update the repos + # /usr/local should always exist, not sure about the src subfolder though + ${SUDO} mkdir -p /usr/local/src + + # Get Git files + getGitFiles "${pivpnFilesDir}" "${pivpnGitUrl}" \ + || { + err "!!! Unable to clone ${pivpnGitUrl} into ${pivpnFilesDir}, unable to continue." + exit 1 + } +} + +installPiVPN() { + ${SUDO} mkdir -p /etc/pivpn/ + askWhichVPN + setVPNDefaultVars + + if [[ "${VPN}" == 'openvpn' ]]; then + setOpenVPNDefaultVars + askAboutCustomizing + installOpenVPN + askCustomProto + elif [[ "${VPN}" == 'wireguard' ]]; then + setWireguardDefaultVars + installWireGuard + fi + + askCustomPort + askClientDNS + + if [[ "${VPN}" == 'openvpn' ]]; then + askCustomDomain + fi + + askPublicIPOrDNS + + if [[ "${VPN}" == 'openvpn' ]]; then + askEncryption + confOpenVPN + confOVPN + elif [[ "${VPN}" == 'wireguard' ]]; then + confWireGuard + fi + + confNetwork + + if [[ "${VPN}" == 'openvpn' ]]; then + confLogging + elif [[ "${VPN}" == 'wireguard' ]]; then + writeWireguardTempVarsFile + fi + + writeVPNTempVarsFile +} + +setVPNDefaultVars() { + # Allow custom subnetClass via unattend setupVARs file. + # Use default if not provided. + if [[ -z "${subnetClass}" ]]; then + subnetClass="24" + fi + + if [[ -z "${subnetClassv6}" ]]; then + subnetClassv6="64" + fi +} + +generateRandomSubnet() { +# local MATCHES +# # Source: https://community.openvpn.net/openvpn/wiki/AvoidRoutingConflicts +# declare -a SUBNET_EXCLUDE_LIST +# +# SUBNET_EXCLUDE_LIST=(10.0.0.0/24) +# SUBNET_EXCLUDE_LIST+=(10.0.1.0/24) +# SUBNET_EXCLUDE_LIST+=(10.1.1.0/24) +# SUBNET_EXCLUDE_LIST+=(10.1.10.0/24) +# SUBNET_EXCLUDE_LIST+=(10.2.0.0/24) +# SUBNET_EXCLUDE_LIST+=(10.8.0.0/24) +# SUBNET_EXCLUDE_LIST+=(10.10.1.0/24) +# SUBNET_EXCLUDE_LIST+=(10.90.90.0/24) +# SUBNET_EXCLUDE_LIST+=(10.100.1.0/24) +# SUBNET_EXCLUDE_LIST+=(10.255.255.0/24) +# +# readarray -t CURRENTLY_USED_SUBNETS <<< "$(ip route show \ +# | grep -oE '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\/[0-9]{1,2}')" +# SUBNET_EXCLUDE_LIST=("${SUBNET_EXCLUDE_LIST[@]}" +# "${CURRENTLY_USED_SUBNETS[@]}") +# +# while true; do +# MATCHES=0 +# pivpnNET="10.$((RANDOM % 256)).$((RANDOM % 256)).0" +# +# for SUB in "${SUBNET_EXCLUDE_LIST[@]}"; do +# if grepcidr "${SUB}" <<< "${pivpnNET}/${subnetClass}" \ +# 2>&1 > /dev/null; then +# ((MATCHES++)) +# fi +# done +# +# if [[ "${MATCHES}" -eq 0 ]]; then +# break +# fi +# done +# +# echo "${pivpnNET}" + pivpnNET="172.16.17.0" + echo "${pivpnNET}" +} + +setOpenVPNDefaultVars() { + pivpnDEV="tun0" + + # Allow custom NET via unattend setupVARs file. + # Use default if not provided. + if [[ -z "${pivpnNET}" ]]; then + pivpnNET="$(generateRandomSubnet)" + fi + + vpnGw="$(cut -d '.' -f 1-3 <<< "${pivpnNET}").1" +} + +setWireguardDefaultVars() { + # Since WireGuard only uses UDP, askCustomProto() is never + # called so we set the protocol here. + pivpnPROTO="udp" + pivpnDEV="wg0" + + # Allow custom NET via unattend setupVARs file. + # Use default if not provided. + if [[ -z "${pivpnNET}" ]]; then + pivpnNET="$(generateRandomSubnet)" + fi + + if [[ "${pivpnenableipv6}" -eq 1 ]] \ + && [[ -z "${pivpnNETv6}" ]]; then + pivpnNETv6="fd11:5ee:bad:c0de::" + fi + + vpnGw="$(cut -d '.' -f 1-3 <<< "${pivpnNET}").1" + + if [[ "${pivpnenableipv6}" -eq 1 ]]; then + vpnGwv6="${pivpnNETv6}1" + fi + + # Allow custom allowed IPs via unattend setupVARs file. + # Use default if not provided. + if [[ -z "${ALLOWED_IPS}" ]]; then + ALLOWED_IPS="0.0.0.0/0" + + # Forward all traffic through PiVPN (i.e. full-tunnel), may be modified by + # the user after the installation. + if [[ "${pivpnenableipv6}" -eq 1 ]] \ + || [[ "${pivpnforceipv6route}" -eq 1 ]]; then + ALLOWED_IPS="${ALLOWED_IPS}, ::0/0" + fi + fi + + # The default MTU should be fine for most users but we allow to set a + # custom MTU via unattend setupVARs file. Use default if not provided. + if [[ -z "${pivpnMTU}" ]]; then + # Using default Wireguard MTU + pivpnMTU="1420" + fi + + CUSTOMIZE=0 +} + +writeVPNTempVarsFile() { + { + echo "pivpnDEV=${pivpnDEV}" + echo "pivpnNET=${pivpnNET}" + echo "subnetClass=${subnetClass}" + echo "pivpnenableipv6=${pivpnenableipv6}" + + if [[ "${pivpnenableipv6}" -eq 1 ]]; then + echo "pivpnNETv6=\"${pivpnNETv6}\"" + echo "subnetClassv6=${subnetClassv6}" + fi + + echo "ALLOWED_IPS=\"${ALLOWED_IPS}\"" + } >> "${tempsetupVarsFile}" +} + +writeWireguardTempVarsFile() { + { + echo "pivpnPROTO=${pivpnPROTO}" + echo "pivpnMTU=${pivpnMTU}" + + # Write PERSISTENTKEEPALIVE if provided via unattended file + # May also be added manually to /etc/pivpn/wireguard/setupVars.conf + # post installation to be used for client profile generation + if [[ -n "${pivpnPERSISTENTKEEPALIVE}" ]]; then + echo "pivpnPERSISTENTKEEPALIVE=${pivpnPERSISTENTKEEPALIVE}" + fi + } >> "${tempsetupVarsFile}" +} + +askWhichVPN() { + if [[ "${runUnattended}" == 'true' ]]; then + if [[ "${WIREGUARD_SUPPORT}" -eq 1 ]]; then + if [[ -z "${VPN}" ]]; then + echo ":: No VPN protocol specified, using WireGuard" + VPN="wireguard" + else + VPN="${VPN,,}" + + if [[ "${VPN}" == "wireguard" ]]; then + echo "::: WireGuard will be installed" + elif [[ "${VPN}" == "openvpn" ]]; then + echo "::: OpenVPN will be installed" + else + err ":: ${VPN} is not a supported VPN protocol, please specify 'wireguard' or 'openvpn'" + exit 1 + fi + fi + else + if [[ -z "${VPN}" ]]; then + echo ":: No VPN protocol specified, using OpenVPN" + VPN="openvpn" + else + VPN="${VPN,,}" + + if [[ "${VPN}" == "openvpn" ]]; then + echo "::: OpenVPN will be installed" + else + err ":: ${VPN} is not a supported VPN protocol on ${DPKG_ARCH} ${PLAT}, only 'openvpn' is" + exit 1 + fi + fi + fi + else + if [[ "${WIREGUARD_SUPPORT}" -eq 1 ]] \ + && [[ "${OPENVPN_SUPPORT}" -eq 1 ]]; then + chooseVPNCmd=(whiptail + --backtitle "Setup PiVPN" + --title "Installation mode" + --separate-output + --radiolist "WireGuard is a new kind of VPN that provides \ +near-instantaneous connection speed, high performance, and modern cryptography. + +It's the recommended choice especially if you use mobile devices where \ +WireGuard is easier on battery than OpenVPN. + +OpenVPN is still available if you need the traditional, flexible, trusted \ +VPN protocol or if you need features like TCP and custom search domain. + +Choose a VPN (press space to select):" "${r}" "${c}" 2) + VPNChooseOptions=(WireGuard "" on + OpenVPN "" off) + + if VPN="$("${chooseVPNCmd[@]}" \ + "${VPNChooseOptions[@]}" \ + 2>&1 > /dev/tty)"; then + echo "::: Using VPN: ${VPN}" + VPN="${VPN,,}" + else + err "::: Cancel selected, exiting...." + exit 1 + fi + elif [[ "${OPENVPN_SUPPORT}" -eq 1 ]] \ + && [[ "${WIREGUARD_SUPPORT}" -eq 0 ]]; then + echo "::: Using VPN: OpenVPN" + VPN="openvpn" + elif [[ "${OPENVPN_SUPPORT}" -eq 0 ]] \ + && [[ "${WIREGUARD_SUPPORT}" -eq 1 ]]; then + echo "::: Using VPN: WireGuard" + VPN="wireguard" + fi + fi + + echo "VPN=${VPN}" >> "${tempsetupVarsFile}" +} + +askAboutCustomizing() { + if [[ "${runUnattended}" == 'false' ]]; then + if whiptail \ + --backtitle "Setup PiVPN" \ + --title "Installation mode" \ + --defaultno \ + --yesno "PiVPN uses the following settings that we believe are good \ +defaults for most users. However, we still want to keep flexibility, so if \ +you need to customize them, choose Yes. + +* UDP or TCP protocol: UDP +* Custom search domain for the DNS field: None +* Modern features or best compatibility: Modern features \ +(256 bit certificate + additional TLS encryption)" "${r}" "${c}"; then + CUSTOMIZE=1 + else + CUSTOMIZE=0 + fi + fi +} + +installOpenVPN() { + local PIVPN_DEPS gpg_path + gpg_path="${pivpnFilesDir}/files/etc/apt/repo-public.gpg" + echo "::: Installing OpenVPN from Debian package... " + + if [[ "${NEED_OPENVPN_REPO}" -eq 1 ]]; then + # gnupg is used by apt-key to import the openvpn GPG key into the + # APT keyring + PIVPN_DEPS=(gnupg) + installDependentPackages PIVPN_DEPS[@] + + # OpenVPN repo's public GPG key + # (fingerprint 0x30EBF4E73CCE63EEE124DD278E6DA8B4E158C569) + echo "::: Adding repository key..." + + if ! ${SUDO} apt-key add "${gpg_path}"; then + err "::: Can't import OpenVPN GPG key" + exit 1 + fi + + echo "::: Adding OpenVPN repository... " + echo "deb https://build.openvpn.net/debian/openvpn/stable ${OSCN} main" \ + | ${SUDO} tee /etc/apt/sources.list.d/pivpn-openvpn-repo.list > /dev/null + + echo "::: Updating package cache..." + updatePackageCache + fi + + PIVPN_DEPS=(openvpn) + + installDependentPackages PIVPN_DEPS[@] +} + +installWireGuard() { + local PIVPN_DEPS + + echo -n "::: Installing WireGuard" + PIVPN_DEPS=(wireguard-tools) + + if [[ "${PLAT}" == "Raspbian" ]]; then + echo " from Raspbian package..." + + # qrencode is used to generate qrcodes from config file, + # for use with mobile clients + PIVPN_DEPS+=(qrencode) + elif [[ "${PLAT}" == "Debian" ]]; then + echo " from Debian package..." + + PIVPN_DEPS+=(qrencode) + + if [[ "${WIREGUARD_BUILTIN}" -eq 0 ]]; then + # Explicitly install the module if not built-in + PIVPN_DEPS+=(linux-headers-amd64 wireguard-dkms) + fi + elif [[ "${PLAT}" == "Ubuntu" ]]; then + echo "..." + + PIVPN_DEPS+=(qrencode) + + if [[ "${WIREGUARD_BUILTIN}" -eq 0 ]]; then + PIVPN_DEPS+=(linux-headers-generic wireguard-dkms) + fi + elif [[ "${PLAT}" == 'Alpine' ]]; then + echo "..." + + PIVPN_DEPS+=(libqrencode) + fi + + if [[ "${PLAT}" == "Raspbian" || "${PLAT}" == "Debian" ]] \ + && [[ -z "${AVAILABLE_WIREGUARD}" ]]; then + if [[ "${PLAT}" == "Debian" ]]; then + echo "::: Adding Debian Bullseye repository... " + echo "deb https://deb.debian.org/debian/ bullseye main" \ + | ${SUDO} tee /etc/apt/sources.list.d/pivpn-bullseye-repo.list > /dev/null + else + echo "::: Adding Raspbian Bullseye repository... " + echo "deb http://raspbian.raspberrypi.org/raspbian/ bullseye main" \ + | ${SUDO} tee /etc/apt/sources.list.d/pivpn-bullseye-repo.list > /dev/null + fi + + { + printf 'Package: *\n' + printf 'Pin: release n=bullseye\n' + printf 'Pin-Priority: -1\n\n' + printf 'Package: wireguard wireguard-dkms wireguard-tools\n' + printf 'Pin: release n=bullseye\n' + printf 'Pin-Priority: 100\n' + } | ${SUDO} tee /etc/apt/preferences.d/pivpn-limit-bullseye > /dev/null + + echo "::: Updating package cache..." + updatePackageCache + fi + + installDependentPackages PIVPN_DEPS[@] +} + +askCustomProto() { + if [[ "${runUnattended}" == 'true' ]]; then + if [[ -z "${pivpnPROTO}" ]]; then + echo "::: No TCP/IP protocol specified, using the default protocol udp" + pivpnPROTO="udp" + else + pivpnPROTO="${pivpnPROTO,,}" + + if [[ "${pivpnPROTO}" == "udp" ]] \ + || [[ "${pivpnPROTO}" == "tcp" ]]; then + echo "::: Using the ${pivpnPROTO} protocol" + else + err ":: ${pivpnPROTO} is not a supported TCP/IP protocol, please specify 'udp' or 'tcp'" + exit 1 + fi + fi + + echo "pivpnPROTO=${pivpnPROTO}" >> "${tempsetupVarsFile}" + return + fi + + if [[ "${CUSTOMIZE}" -eq 0 ]]; then + if [[ "${VPN}" == "openvpn" ]]; then + pivpnPROTO="udp" + echo "pivpnPROTO=${pivpnPROTO}" >> "${tempsetupVarsFile}" + return + fi + fi + + # Set the available protocols into an array so it can be used + # with a whiptail dialog + if pivpnPROTO="$(whiptail \ + --title "Protocol" \ + --radiolist "Choose a protocol (press space to select). \ +Please only choose TCP if you know why you need TCP." "${r}" "${c}" 2 \ + "UDP" "" ON \ + "TCP" "" OFF \ + 3>&1 1>&2 2>&3)"; then + # Convert option into lowercase (UDP->udp) + pivpnPROTO="${pivpnPROTO,,}" + echo "::: Using protocol: ${pivpnPROTO}" + echo "pivpnPROTO=${pivpnPROTO}" >> "${tempsetupVarsFile}" + else + err "::: Cancel selected, exiting...." + exit 1 + fi +} + +askCustomPort() { + if [[ "${runUnattended}" == 'true' ]]; then + if [[ -z "${pivpnPORT}" ]]; then + if [[ "${VPN}" == "wireguard" ]]; then + echo "::: No port specified, using the default port 51820" + pivpnPORT=51820 + elif [[ "${VPN}" == "openvpn" ]]; then + if [[ "${pivpnPROTO}" == "udp" ]]; then + echo "::: No port specified, using the default port 1194" + pivpnPORT=1194 + elif [[ "${pivpnPROTO}" == "tcp" ]]; then + echo "::: No port specified, using the default port 443" + pivpnPORT=443 + fi + fi + else + if [[ "${pivpnPORT}" =~ ^[0-9]+$ ]] \ + && [[ "${pivpnPORT}" -ge 1 ]] \ + && [[ "${pivpnPORT}" -le 65535 ]]; then + echo "::: Using port ${pivpnPORT}" + else + err "::: ${pivpnPORT} is not a valid port, use a port within the range [1,65535] (inclusive)" + exit 1 + fi + fi + + echo "pivpnPORT=${pivpnPORT}" >> "${tempsetupVarsFile}" + return + fi + + until [[ "${PORTNumCorrect}" == 'true' ]]; do + portInvalid="Invalid" + + if [[ "${VPN}" == "wireguard" ]]; then + DEFAULT_PORT=51820 + elif [[ "${VPN}" == "openvpn" ]]; then + if [[ "${pivpnPROTO}" == "udp" ]]; then + DEFAULT_PORT=1194 + else + DEFAULT_PORT=443 + fi + fi + + if pivpnPORT="$(whiptail \ + --title "Default ${VPN} Port" \ + --inputbox "You can modify the default ${VPN} port. +Enter a new value or hit 'Enter' to retain \ +the default" "${r}" "${c}" "${DEFAULT_PORT}" \ + 3>&1 1>&2 2>&3)"; then + if [[ "${pivpnPORT}" =~ ^[0-9]+$ ]] \ + && [[ "${pivpnPORT}" -ge 1 ]] \ + && [[ "${pivpnPORT}" -le 65535 ]]; then + : + else + pivpnPORT="${portInvalid}" + fi + else + err "::: Cancel selected, exiting...." + exit 1 + fi + + if [[ "${pivpnPORT}" == "${portInvalid}" ]]; then + whiptail \ + --backtitle "Invalid Port" \ + --title "Invalid Port" \ + --msgbox "You entered an invalid Port number. + Please enter a number from 1 - 65535. + If you are not sure, please just keep the default." "${r}" "${c}" + PORTNumCorrect=false + else + if whiptail \ + --backtitle "Specify Custom Port" \ + --title "Confirm Custom Port Number" \ + --yesno "Are these settings correct? + PORT: ${pivpnPORT}" "${r}" "${c}"; then + PORTNumCorrect=true + else + # If the settings are wrong, the loop continues + PORTNumCorrect=false + fi + fi + done + + # write out the port + echo "pivpnPORT=${pivpnPORT}" >> "${tempsetupVarsFile}" +} + +askClientDNS() { + if [[ "${runUnattended}" == 'true' ]]; then + if [[ -z "${pivpnDNS1}" ]] \ + && [[ -n "${pivpnDNS2}" ]]; then + pivpnDNS1="${pivpnDNS2}" + unset pivpnDNS2 + elif [[ -z "${pivpnDNS1}" ]] \ + && [[ -z "${pivpnDNS2}" ]]; then + pivpnDNS1="9.9.9.9" + pivpnDNS2="149.112.112.112" + echo -n "::: No DNS provider specified, " + echo "using Quad9 DNS (${pivpnDNS1} ${pivpnDNS2})" + fi + + local INVALID_DNS_SETTINGS=0 + + if ! validIP "${pivpnDNS1}"; then + INVALID_DNS_SETTINGS=1 + echo "::: Invalid DNS ${pivpnDNS1}" + fi + + if [[ -n "${pivpnDNS2}" ]] \ + && ! validIP "${pivpnDNS2}"; then + INVALID_DNS_SETTINGS=1 + echo "::: Invalid DNS ${pivpnDNS2}" + fi + + if [[ "${INVALID_DNS_SETTINGS}" -eq 0 ]]; then + echo "::: Using DNS ${pivpnDNS1} ${pivpnDNS2}" + else + exit 1 + fi + + { + echo "pivpnDNS1=${pivpnDNS1}" + echo "pivpnDNS2=${pivpnDNS2}" + } >> "${tempsetupVarsFile}" + return + fi + + # Detect and offer to use Pi-hole + if command -v pihole > /dev/null; then + if whiptail \ + --backtitle "Setup PiVPN" \ + --title "Pi-hole" \ + --yesno "We have detected a Pi-hole installation, \ +do you want to use it as the DNS server for the VPN, so you \ +get ad blocking on the go?" "${r}" "${c}"; then + if [[ ! -r "${piholeSetupVars}" ]]; then + err "::: Unable to read ${piholeSetupVars}" + exit 1 + fi + + # Add a custom hosts file for VPN clients so they appear + # as 'name.pivpn' in the Pi-hole dashboard as well as resolve + # by their names. + echo "addn-hosts=/etc/pivpn/hosts.${VPN}" \ + | ${SUDO} tee "${dnsmasqConfig}" > /dev/null + + # Then create an empty hosts file or clear if it exists. + ${SUDO} bash -c "> /etc/pivpn/hosts.${VPN}" + + # Setting Pi-hole to "Listen on all interfaces" allows + # dnsmasq to listen on the VPN interface while permitting + # queries only from hosts whose address is on the LAN and + # VPN subnets. + ${SUDO} pihole -a -i local + + # Use the Raspberry Pi VPN IP as DNS server. + pivpnDNS1="${vpnGw}" + + { + echo "pivpnDNS1=${pivpnDNS1}" + echo "pivpnDNS2=${pivpnDNS2}" + } >> "${tempsetupVarsFile}" + + # Allow incoming DNS requests through UFW. + if [[ "${USING_UFW}" -eq 1 ]]; then + ${SUDO} ufw insert 1 allow in \ + on "${pivpnDEV}" to any port 53 \ + from "${pivpnNET}/${subnetClass}" > /dev/null + fi + + return + fi + fi + + DNSChoseCmd=(whiptail + --backtitle "Setup PiVPN" + --title "DNS Provider" + --separate-output + --radiolist "Select the DNS Provider for your VPN Clients \ +(press space to select). +To use your own, select Custom. + +In case you have a local resolver running, i.e. unbound, select \ +\"PiVPN-is-local-DNS\" and make sure your resolver is listening on \ +\"${vpnGw}\", allowing requests from \ +\"${pivpnNET}/${subnetClass}\"." "${r}" "${c}" 6) + DNSChooseOptions=(Quad9 "" on + OpenDNS "" off + Level3 "" off + DNS.WATCH "" off + Norton "" off + FamilyShield "" off + CloudFlare "" off + Google "" off + PiVPN-is-local-DNS "" off + Custom "" off) + + if DNSchoices="$("${DNSChoseCmd[@]}" \ + "${DNSChooseOptions[@]}" \ + 2>&1 > /dev/tty)"; then + if [[ "${DNSchoices}" != "Custom" ]]; then + echo "::: Using ${DNSchoices} servers." + declare -A DNS_MAP=(["Quad9"]="9.9.9.9 149.112.112.112" + ["OpenDNS"]="208.67.222.222 208.67.220.220" + ["Level3"]="209.244.0.3 209.244.0.4" + ["DNS.WATCH"]="84.200.69.80 84.200.70.40" + ["Norton"]="199.85.126.10 199.85.127.10" + ["FamilyShield"]="208.67.222.123 208.67.220.123" + ["CloudFlare"]="1.1.1.1 1.0.0.1" + ["Google"]="8.8.8.8 8.8.4.4" + ["PiVPN-is-local-DNS"]="${vpnGw}") + pivpnDNS1=$(awk '{print $1}' <<< "${DNS_MAP["${DNSchoices}"]}") + pivpnDNS2=$(awk '{print $2}' <<< "${DNS_MAP["${DNSchoices}"]}") + else + until [[ "${DNSSettingsCorrect}" == 'true' ]]; do + strInvalid="Invalid" + + if pivpnDNS="$(whiptail \ + --backtitle "Specify Upstream DNS Provider(s)" \ + --inputbox "Enter your desired upstream DNS provider(s), \ +separated by a comma. + +For example '1.1.1.1, 9.9.9.9'" "${r}" "${c}" "" \ + 3>&1 1>&2 2>&3)"; then + pivpnDNS1="$(echo "${pivpnDNS}" \ + | sed 's/[, \t]\+/,/g' \ + | awk -F, '{print$1}')" + pivpnDNS2="$(echo "${pivpnDNS}" \ + | sed 's/[, \t]\+/,/g' \ + | awk -F, '{print$2}')" + + if ! validIP "${pivpnDNS1}" \ + || [[ ! "${pivpnDNS1}" ]]; then + pivpnDNS1="${strInvalid}" + fi + + if ! validIP "${pivpnDNS2}" \ + && [[ "${pivpnDNS2}" ]]; then + pivpnDNS2="${strInvalid}" + fi + else + err "::: Cancel selected, exiting...." + exit 1 + fi + + if [[ "${pivpnDNS1}" == "${strInvalid}" ]] \ + || [[ "${pivpnDNS2}" == "${strInvalid}" ]]; then + whiptail \ + --backtitle "Invalid IP" \ + --title "Invalid IP" \ + --msgbox "One or both entered IP addresses were invalid. \ +Please try again. + DNS Server 1: ${pivpnDNS1} + DNS Server 2: ${pivpnDNS2}" "${r}" "${c}" + + if [[ "${pivpnDNS1}" == "${strInvalid}" ]]; then + pivpnDNS1="" + fi + + if [[ "${pivpnDNS2}" == "${strInvalid}" ]]; then + pivpnDNS2="" + fi + + DNSSettingsCorrect=false + else + if whiptail \ + --backtitle "Specify Upstream DNS Provider(s)" \ + --title "Upstream DNS Provider(s)" \ + --yesno "Are these settings correct? + DNS Server 1: ${pivpnDNS1} + DNS Server 2: ${pivpnDNS2}" "${r}" "${c}"; then + DNSSettingsCorrect=true + else + # If the settings are wrong, the loop continues + DNSSettingsCorrect=false + fi + fi + done + fi + + else + err "::: Cancel selected. Exiting..." + exit 1 + fi + + { + echo "pivpnDNS1=${pivpnDNS1}" + echo "pivpnDNS2=${pivpnDNS2}" + } >> "${tempsetupVarsFile}" +} + +# Call this function to use a regex to check user +# input for a valid custom domain +validDomain() { + local domain="${1}" + local perl_regexp='(?=^.{4,253}$)' + perl_regexp="${perl_regexp}(^(?:[a-zA-Z0-9](?:(?:[a-zA-Z0-9\-]){0,61}" + perl_regexp="${perl_regexp}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$)" + grep -qP "${perl_regexp}" <<< "${domain}" +} + +# This procedure allows a user to specify a custom +# search domain if they have one. +askCustomDomain() { + if [[ "${runUnattended}" == 'true' ]]; then + if [[ -n "${pivpnSEARCHDOMAIN}" ]]; then + if validDomain "${pivpnSEARCHDOMAIN}"; then + echo "::: Using custom domain ${pivpnSEARCHDOMAIN}" + else + err "::: Custom domain ${pivpnSEARCHDOMAIN} is not valid" + exit 1 + fi + else + echo "::: Skipping custom domain" + fi + + echo "pivpnSEARCHDOMAIN=${pivpnSEARCHDOMAIN}" >> "${tempsetupVarsFile}" + return + fi + + if [[ "${CUSTOMIZE}" -eq 0 ]]; then + if [[ "${VPN}" == "openvpn" ]]; then + echo "pivpnSEARCHDOMAIN=${pivpnSEARCHDOMAIN}" >> "${tempsetupVarsFile}" + return + fi + fi + + DomainSettingsCorrect=false + + if whiptail \ + --backtitle "Custom Search Domain" \ + --title "Custom Search Domain" \ + --defaultno \ + --yesno "Would you like to add a custom search domain? +(This is only for advanced users who have their own domain) +" "${r}" "${c}"; then + until [[ "${DomainSettingsCorrect}" == 'true' ]]; do + if pivpnSEARCHDOMAIN="$(whiptail \ + --inputbox "Enter Custom Domain +Format: mydomain.com" "${r}" "${c}" \ + --title "Custom Domain" \ + 3>&1 1>&2 2>&3)"; then + if validDomain "${pivpnSEARCHDOMAIN}"; then + if whiptail \ + --backtitle "Custom Search Domain" \ + --title "Custom Search Domain" \ + --yesno "Are these settings correct? + Custom Search Domain: ${pivpnSEARCHDOMAIN}" "${r}" "${c}"; then + DomainSettingsCorrect=true + else + # If the settings are wrong, the loop continues + DomainSettingsCorrect=false + fi + else + whiptail \ + --backtitle "Invalid Domain" \ + --title "Invalid Domain" \ + --msgbox "Domain is invalid. Please try again. + DOMAIN: ${pivpnSEARCHDOMAIN} +" "${r}" "${c}" + DomainSettingsCorrect=false + fi + else + err "::: Cancel selected. Exiting..." + exit 1 + fi + done + fi + + echo "pivpnSEARCHDOMAIN=${pivpnSEARCHDOMAIN}" >> "${tempsetupVarsFile}" +} + +askPublicIPOrDNS() { + if ! IPv4pub="$(dig +short myip.opendns.com @208.67.222.222)" \ + || ! validIP "${IPv4pub}"; then + err "dig failed, now trying to curl checkip.amazonaws.com" + + if ! IPv4pub="$(curl -sSf https://checkip.amazonaws.com)" \ + || ! validIP "${IPv4pub}"; then + err "checkip.amazonaws.com failed, please check your internet connection/DNS" + exit 1 + fi + fi + + if [[ "${runUnattended}" == 'true' ]]; then + if [[ -z "${pivpnHOST}" ]]; then + echo "::: No IP or domain name specified, using public IP ${IPv4pub}" + pivpnHOST="${IPv4pub}" + else + if validIP "${pivpnHOST}"; then + echo "::: Using public IP ${pivpnHOST}" + elif validDomain "${pivpnHOST}"; then + echo "::: Using domain name ${pivpnHOST}" + else + err "::: ${pivpnHOST} is not a valid IP or domain name" + exit 1 + fi + fi + + echo "pivpnHOST=${pivpnHOST}" >> "${tempsetupVarsFile}" + return + fi + + local publicDNSCorrect + local publicDNSValid + + if METH="$(whiptail \ + --title "Public IP or DNS" \ + --radiolist \ + "Will clients use a Public IP or DNS Name to connect to your server \ +(press space to select)?" "${r}" "${c}" 2 \ + "${IPv4pub}" "Use this public IP" "ON" \ + "DNS Entry" "Use a public DNS" "OFF" \ + 3>&1 1>&2 2>&3)"; then + if [[ "${METH}" == "${IPv4pub}" ]]; then + pivpnHOST="${IPv4pub}" + else + until [[ "${publicDNSCorrect}" == 'true' ]]; do + until [[ "${publicDNSValid}" == 'true' ]]; do + if PUBLICDNS="$(whiptail \ + --title "PiVPN Setup" \ + --inputbox "What is the public DNS \ +name of this Server?" "${r}" "${c}" \ + 3>&1 1>&2 2>&3)"; then + if validDomain "${PUBLICDNS}"; then + publicDNSValid=true + pivpnHOST="${PUBLICDNS}" + else + whiptail \ + --backtitle "PiVPN Setup" \ + --title "Invalid DNS name" \ + --msgbox "This DNS name is invalid. Please try again. + DNS name: ${PUBLICDNS} +" "${r}" "${c}" + publicDNSValid=false + fi + else + err "::: Cancel selected. Exiting..." + exit 1 + fi + done + + if whiptail \ + --backtitle "PiVPN Setup" \ + --title "Confirm DNS Name" \ + --yesno "Is this correct? +Public DNS Name: ${PUBLICDNS}" "${r}" "${c}"; then + publicDNSCorrect=true + else + publicDNSCorrect=false + publicDNSValid=false + fi + done + fi + else + err "::: Cancel selected. Exiting..." + exit 1 + fi + + echo "pivpnHOST=${pivpnHOST}" >> "${tempsetupVarsFile}" +} + +askEncryption() { + if [[ "${runUnattended}" == 'true' ]]; then + if [[ -z "${TWO_POINT_FOUR}" ]] \ + || [[ "${TWO_POINT_FOUR}" -eq 1 ]]; then + TWO_POINT_FOUR=1 + echo "::: Using OpenVPN 2.4 features" + + if [[ -z "${pivpnENCRYPT}" ]]; then + pivpnENCRYPT=256 + fi + + if [[ "${pivpnENCRYPT}" -eq 256 ]] \ + || [[ "${pivpnENCRYPT}" -eq 384 ]] \ + || [[ "${pivpnENCRYPT}" -eq 521 ]]; then + echo "::: Using a ${pivpnENCRYPT}-bit certificate" + else + err "::: ${pivpnENCRYPT} is not a valid certificate size, use 256, 384, or 521" + exit 1 + fi + else + TWO_POINT_FOUR=0 + echo "::: Using traditional OpenVPN configuration" + + if [[ -z "${pivpnENCRYPT}" ]]; then + pivpnENCRYPT=2048 + fi + + if [[ "${pivpnENCRYPT}" -eq 2048 ]] \ + || [[ "${pivpnENCRYPT}" -eq 3072 ]] \ + || [[ "${pivpnENCRYPT}" -eq 4096 ]]; then + echo "::: Using a ${pivpnENCRYPT}-bit certificate" + else + err "::: ${pivpnENCRYPT} is not a valid certificate size, use 2048, 3072, or 4096" + exit 1 + fi + + if [[ -z "${USE_PREDEFINED_DH_PARAM}" ]]; then + USE_PREDEFINED_DH_PARAM=1 + fi + + if [[ "${USE_PREDEFINED_DH_PARAM}" -eq 1 ]]; then + echo "::: Pre-defined DH parameters will be used" + else + echo "::: DH parameters will be generated locally" + fi + fi + + { + echo "TWO_POINT_FOUR=${TWO_POINT_FOUR}" + echo "pivpnENCRYPT=${pivpnENCRYPT}" + echo "USE_PREDEFINED_DH_PARAM=${USE_PREDEFINED_DH_PARAM}" + } >> "${tempsetupVarsFile}" + return + fi + + if [[ "${CUSTOMIZE}" -eq 0 ]]; then + if [[ "${VPN}" == "openvpn" ]]; then + TWO_POINT_FOUR=1 + pivpnENCRYPT=256 + + { + echo "TWO_POINT_FOUR=${TWO_POINT_FOUR}" + echo "pivpnENCRYPT=${pivpnENCRYPT}" + echo "USE_PREDEFINED_DH_PARAM=${USE_PREDEFINED_DH_PARAM}" + } >> "${tempsetupVarsFile}" + return + fi + fi + + if whiptail \ + --backtitle "Setup OpenVPN" \ + --title "Installation mode" \ + --yesno "OpenVPN 2.4 can take advantage of Elliptic Curves \ +to provide higher connection speed and improved security over \ +RSA, while keeping smaller certificates. + +Moreover, the 'tls-crypt' directive encrypts the certificates \ +being used while authenticating, increasing privacy. + +If your clients do run OpenVPN 2.4 or later you can enable \ +these features, otherwise choose 'No' for best \ +compatibility." \ + "${r}" \ + "${c}"; then + TWO_POINT_FOUR=1 + pivpnENCRYPT="$(whiptail \ + --backtitle "Setup OpenVPN" \ + --title "ECDSA certificate size" \ + --radiolist "Choose the desired size of your certificate \ +(press space to select): +This is a certificate that will be generated on your system. \ +The larger the certificate, the more time this will take. \ +For most applications, it is recommended to use 256 bits. \ +You can increase the number of bits if you care about, however, consider \ +that 256 bits are already as secure as 3072 bit RSA." "${r}" "${c}" 3 \ + "256" "Use a 256-bit certificate (recommended level)" ON \ + "384" "Use a 384-bit certificate" OFF \ + "521" "Use a 521-bit certificate (paranoid level)" OFF \ + 3>&1 1>&2 2>&3)" + else + TWO_POINT_FOUR=0 + pivpnENCRYPT="$(whiptail \ + --backtitle "Setup OpenVPN" \ + --title "RSA certificate size" \ + --radiolist "Choose the desired size of your certificate \ +(press space to select): +This is a certificate that will be generated on your system. \ +The larger the certificate, the more time this will take. \ +For most applications, it is recommended to use 2048 bits. \ +If you are paranoid about ... things... \ +then grab a cup of joe and pick 4096 bits." "${r}" "${c}" 3 \ + "2048" "Use a 2048-bit certificate (recommended level)" ON \ + "3072" "Use a 3072-bit certificate " OFF \ + "4096" "Use a 4096-bit certificate (paranoid level)" OFF \ + 3>&1 1>&2 2>&3)" + fi + + exitstatus="$?" + + if [[ "${exitstatus}" != 0 ]]; then + err "::: Cancel selected. Exiting..." + exit 1 + fi + + if [[ "${pivpnENCRYPT}" -ge 2048 ]] \ + && whiptail \ + --backtitle "Setup OpenVPN" \ + --title "Generate Diffie-Hellman Parameters" \ + --yesno "Generating DH parameters can take many hours on a Raspberry Pi. \ +You can instead use Pre-defined DH parameters recommended by the \ +Internet Engineering Task Force. +More information about those can be found here: \ +https://wiki.mozilla.org/Security/Archive/Server_Side_TLS_4.0#\ +Pre-defined_DHE_groups +If you want unique parameters, choose 'No' and new Diffie-Hellman \ +parameters will be generated on your device." "${r}" "${c}"; then + USE_PREDEFINED_DH_PARAM=1 + else + USE_PREDEFINED_DH_PARAM=0 + fi + + { + echo "TWO_POINT_FOUR=${TWO_POINT_FOUR}" + echo "pivpnENCRYPT=${pivpnENCRYPT}" + echo "USE_PREDEFINED_DH_PARAM=${USE_PREDEFINED_DH_PARAM}" + } >> "${tempsetupVarsFile}" +} + +cidrToMask() { + # Source: https://stackoverflow.com/a/20767392 + set -- $((5 - ($1 / 8))) \ + 255 255 255 255 \ + $(((255 << (8 - ($1 % 8))) & 255)) \ + 0 0 0 + shift "${1}" + echo "${1-0}.${2-0}.${3-0}.${4-0}" +} + +confOpenVPN() { + local sed_pattern file_pattern + + # Grab the existing Hostname + host_name="$(hostname -s)" + # Generate a random UUID for this server so that we can use + # verify-x509-name later that is unique for this server + # installation. + NEW_UUID="$(< /proc/sys/kernel/random/uuid)" + # Create a unique server name using the host name and UUID + SERVER_NAME="${host_name}_${NEW_UUID}" + + # Backup the openvpn folder + OPENVPN_BACKUP="openvpn_$(date +%Y-%m-%d-%H%M%S).tar.gz" + echo "::: Backing up the openvpn folder to /etc/${OPENVPN_BACKUP}" + CURRENT_UMASK="$(umask)" + umask 0077 + ${SUDO} tar -czf "/etc/${OPENVPN_BACKUP}" /etc/openvpn &> /dev/null + umask "${CURRENT_UMASK}" + + if [[ -f /etc/openvpn/server.conf ]]; then + ${SUDO} rm /etc/openvpn/server.conf + fi + + if [[ -d /etc/openvpn/ccd ]]; then + ${SUDO} rm -rf /etc/openvpn/ccd + fi + + # Create folder to store client specific directives used to push static IPs + ${SUDO} mkdir /etc/openvpn/ccd + + # If easy-rsa exists, remove it + if [[ -d /etc/openvpn/easy-rsa/ ]]; then + ${SUDO} rm -rf /etc/openvpn/easy-rsa/ + fi + + # Get easy-rsa + curl -sSfL "${easyrsaRel}" \ + | ${SUDO} tar -xz --one-top-level=/etc/openvpn/easy-rsa --strip-components 1 + + if [[ ! -s /etc/openvpn/easy-rsa/easyrsa ]]; then + err "${0}: ERR: Failed to download EasyRSA." + exit 1 + fi + + # fix ownership + ${SUDO} chown -R root:root /etc/openvpn/easy-rsa + ${SUDO} mkdir /etc/openvpn/easy-rsa/pki + ${SUDO} chmod 700 /etc/openvpn/easy-rsa/pki + + cd /etc/openvpn/easy-rsa || exit 1 + + if [[ "${TWO_POINT_FOUR}" -eq 1 ]]; then + pivpnCERT="ec" + pivpnTLSPROT="tls-crypt" + else + pivpnCERT="rsa" + pivpnTLSPROT="tls-auth" + fi + + # Remove any previous keys + ${SUDOE} ./easyrsa --batch init-pki + + # Copy template vars file + ${SUDOE} cp vars.example pki/vars + + # Set elliptic curve certificate or traditional rsa certificates + ${SUDOE} sed -i \ + "s/#set_var EASYRSA_ALGO.*/set_var EASYRSA_ALGO ${pivpnCERT}/" \ + pki/vars + + # Set expiration for the CRL to 10 years + ${SUDOE} sed -i \ + 's/#set_var EASYRSA_CRL_DAYS.*/set_var EASYRSA_CRL_DAYS 3650/' \ + pki/vars + + if [[ "${pivpnENCRYPT}" -ge 2048 ]]; then + # Set custom key size if different from the default + sed_pattern="s/#set_var EASYRSA_KEY_SIZE.*/" + sed_pattern="${sed_pattern} set_var EASYRSA_KEY_SIZE ${pivpnENCRYPT}/" + ${SUDOE} sed -i "${sed_pattern}" pki/vars + else + # If less than 2048, then it must be 521 or lower, + # which means elliptic curve certificate was selected. + # We set the curve in this case. + declare -A ECDSA_MAP=(["256"]="prime256v1" + ["384"]="secp384r1" + ["521"]="secp521r1") + + sed_pattern="s/#set_var EASYRSA_CURVE.*/" + sed_pattern="${sed_pattern} set_var EASYRSA_CURVE" + sed_pattern="${sed_pattern} ${ECDSA_MAP["${pivpnENCRYPT}"]}/" + ${SUDOE} sed -i "${sed_pattern}" pki/vars + fi + + # Build the certificate authority + printf "::: Building CA...\\n" + ${SUDOE} ./easyrsa --batch build-ca nopass + printf "\\n::: CA Complete.\\n" + + if [[ "${pivpnCERT}" == "rsa" ]] \ + && [[ "${USE_PREDEFINED_DH_PARAM}" -ne 1 ]]; then + if [[ "${runUnattended}" == 'true' ]]; then + echo "::: The server key, Diffie-Hellman parameters, \ +and HMAC key will now be generated." + else + whiptail \ + --msgbox \ + --backtitle "Setup OpenVPN" \ + --title "Server Information" \ + "The server key, Diffie-Hellman parameters, \ +and HMAC key will now be generated." \ + "${r}" \ + "${c}" + fi + elif [[ "${pivpnCERT}" == "ec" ]] \ + || [[ "${pivpnCERT}" == "rsa" && "${USE_PREDEFINED_DH_PARAM}" -eq 1 ]]; then + if [[ "${runUnattended}" == 'true' ]]; then + echo "::: The server key and HMAC key will now be generated." + else + whiptail \ + --msgbox \ + --backtitle "Setup OpenVPN" \ + --title "Server Information" \ + "The server key and HMAC key will now be generated." \ + "${r}" \ + "${c}" + fi + fi + + # Build the server + EASYRSA_CERT_EXPIRE=3650 ${SUDOE} ./easyrsa \ + build-server-full \ + "${SERVER_NAME}" \ + nopass + + if [[ "${pivpnCERT}" == "rsa" ]]; then + if [[ "${USE_PREDEFINED_DH_PARAM}" -eq 1 ]]; then + file_pattern="${pivpnFilesDir}/files/etc/openvpn" + file_pattern="${file_pattern}/easy-rsa/pki/ffdhe${pivpnENCRYPT}.pem" + # Use Diffie-Hellman parameters from RFC 7919 (FFDHE) + ${SUDOE} install -m 644 "${file_pattern}" \ + "pki/dh${pivpnENCRYPT}.pem" + else + # Generate Diffie-Hellman key exchange + ${SUDOE} ./easyrsa gen-dh + ${SUDOE} mv pki/dh.pem "pki/dh${pivpnENCRYPT}".pem + fi + fi + + # Generate static HMAC key to defend against DDoS + ${SUDOE} openvpn --genkey --secret pki/ta.key + + # Generate an empty Certificate Revocation List + ${SUDOE} ./easyrsa gen-crl + ${SUDOE} cp pki/crl.pem /etc/openvpn/crl.pem + + if ! getent passwd "${ovpnUserGroup%:*}"; then + if [[ "${PLAT}" == 'Alpine' ]]; then + ${SUDOE} adduser -SD \ + -h /var/lib/openvpn/ \ + -s /sbin/nologin \ + "${ovpnUserGroup%:*}" + else + ${SUDOE} useradd \ + --system \ + --home /var/lib/openvpn/ \ + --shell /usr/sbin/nologin \ + "${ovpnUserGroup%:*}" + fi + fi + + ${SUDOE} chown "${ovpnUserGroup}" /etc/openvpn/crl.pem + + # Write config file for server using the template.txt file + ${SUDO} install -m 644 \ + "${pivpnFilesDir}/files/etc/openvpn/server_config.txt" \ + /etc/openvpn/server.conf + + # Apply client DNS settings + ${SUDOE} sed -i \ + "0,/\(dhcp-option DNS \)/ s/\(dhcp-option DNS \).*/\1${pivpnDNS1}\"/" \ + /etc/openvpn/server.conf + + if [[ -z "${pivpnDNS2}" ]]; then + ${SUDOE} sed -i '/\(dhcp-option DNS \)/{n;N;d}' /etc/openvpn/server.conf + else + ${SUDOE} sed -i \ + "0,/\(dhcp-option DNS \)/! s/\(dhcp-option DNS \).*/\1${pivpnDNS2}\"/" \ + /etc/openvpn/server.conf + fi + + # Set the user encryption key size + ${SUDO} sed -i \ + "s#\\(dh /etc/openvpn/easy-rsa/pki/dh\\).*#\\1${pivpnENCRYPT}.pem#" \ + /etc/openvpn/server.conf + + if [[ "${pivpnTLSPROT}" == "tls-crypt" ]]; then + # If they enabled 2.4 use tls-crypt instead of + # tls-auth to encrypt control channel + sed_pattern="s/tls-auth" + sed_pattern="${sed_pattern} \/etc\/openvpn\/easy-rsa\/pki\/ta.key 0/" + sed_pattern="${sed_pattern} tls-crypt" + sed_pattern="${sed_pattern} \/etc\/openvpn\/easy-rsa\/pki\/ta.key/" + ${SUDO} sed -i "${sed_pattern}" /etc/openvpn/server.conf + fi + + if [[ "${pivpnCERT}" == "ec" ]]; then + # If they enabled 2.4 disable dh parameters and specify the + # matching curve from the ECDSA certificate + sed_pattern="s/\(dh \/etc\/openvpn\/easy-rsa\/pki\/dh\).*/dh" + sed_pattern="${sed_pattern} none\necdh-curve" + sed_pattern="${sed_pattern} ${ECDSA_MAP["${pivpnENCRYPT}"]}/" + ${SUDO} sed -i \ + "${sed_pattern}" \ + /etc/openvpn/server.conf + elif [[ "${pivpnCERT}" == "rsa" ]]; then + # Otherwise set the user encryption key size + ${SUDO} sed -i \ + "s#\\(dh /etc/openvpn/easy-rsa/pki/dh\\).*#\\1${pivpnENCRYPT}.pem#" \ + /etc/openvpn/server.conf + fi + + # if they modified VPN network put value in server.conf + if [[ "${pivpnNET}" != "10.8.0.0" ]]; then + ${SUDO} sed -i "s/10.8.0.0/${pivpnNET}/g" /etc/openvpn/server.conf + fi + + # if they modified VPN subnet class put value in server.conf + if [[ "$(cidrToMask "${subnetClass}")" != "255.255.255.0" ]]; then + ${SUDO} sed -i \ + "s/255.255.255.0/$(cidrToMask "${subnetClass}")/g" \ + /etc/openvpn/server.conf + fi + + # if they modified port put value in server.conf + if [[ "${pivpnPORT}" -ne 1194 ]]; then + ${SUDO} sed -i "s/1194/${pivpnPORT}/g" /etc/openvpn/server.conf + fi + + # if they modified protocol put value in server.conf + if [[ "${pivpnPROTO}" != "udp" ]]; then + ${SUDO} sed -i "s/proto udp/proto tcp/g" /etc/openvpn/server.conf + fi + + if [[ -n "${pivpnSEARCHDOMAIN}" ]]; then + sed_pattern="0,/\\(.*dhcp-option.*\\)/" + sed_pattern="${sed_pattern}s//push \"dhcp-option " + sed_pattern="${sed_pattern}DOMAIN ${pivpnSEARCHDOMAIN}\" \\n&/" + ${SUDO} sed -i \ + "${sed_pattern}" \ + /etc/openvpn/server.conf + fi + + # write out server certs to conf file + ${SUDO} sed -i \ + "s#\\(key /etc/openvpn/easy-rsa/pki/private/\\).*#\\1${SERVER_NAME}.key#" \ + /etc/openvpn/server.conf + ${SUDO} sed -i \ + "s#\\(cert /etc/openvpn/easy-rsa/pki/issued/\\).*#\\1${SERVER_NAME}.crt#" \ + /etc/openvpn/server.conf + + # On Alpine Linux, the default config file for OpenVPN is + # "/etc/openvpn/openvpn.conf". + # To avoid crash thorugh OpenRC, we symlink this file. + if [[ "${PLAT}" == 'Alpine' ]]; then + ${SUDO} ln -sfT \ + /etc/openvpn/server.conf \ + /etc/openvpn/openvpn.conf \ + > /dev/null + fi +} + +confOVPN() { + ${SUDO} install -m 644 \ + "${pivpnFilesDir}/files/etc/openvpn/easy-rsa/pki/Default.txt" \ + /etc/openvpn/easy-rsa/pki/Default.txt + + ${SUDO} sed -i \ + "s/IPv4pub/${pivpnHOST}/" \ + /etc/openvpn/easy-rsa/pki/Default.txt + + # if they modified port put value in Default.txt for clients to use + if [[ "${pivpnPORT}" -ne 1194 ]]; then + ${SUDO} sed -i \ + "s/1194/${pivpnPORT}/g" \ + /etc/openvpn/easy-rsa/pki/Default.txt + fi + + # if they modified protocol put value in Default.txt for clients to use + if [[ "${pivpnPROTO}" != "udp" ]]; then + ${SUDO} sed -i \ + "s/proto udp/proto tcp/g" \ + /etc/openvpn/easy-rsa/pki/Default.txt + fi + + # verify server name to strengthen security + ${SUDO} sed -i \ + "s/SRVRNAME/${SERVER_NAME}/" \ + /etc/openvpn/easy-rsa/pki/Default.txt + + if [[ "${pivpnTLSPROT}" == "tls-crypt" ]]; then + # If they enabled 2.4 remove key-direction options since it's not required + ${SUDO} sed -i \ + "/key-direction 1/d" \ + /etc/openvpn/easy-rsa/pki/Default.txt + fi +} + +confWireGuard() { + # Reload job type is not yet available in wireguard-tools shipped with + # Ubuntu 20.04 + if [[ "${PLAT}" == 'Alpine' ]]; then + echo '::: Adding wg-quick unit' + ${SUDO} install -m 0755 \ + "${pivpnFilesDir}/files/etc/init.d/wg-quick" \ + /etc/init.d/wg-quick + else + if ! grep -q 'ExecReload' /lib/systemd/system/wg-quick@.service; then + local wireguard_service_path + wireguard_service_path="${pivpnFilesDir}/files/etc/systemd/system" + wireguard_service_path="${wireguard_service_path}/wg-quick@.service.d" + wireguard_service_path="${wireguard_service_path}/override.conf" + echo "::: Adding additional reload job type for wg-quick unit" + ${SUDO} install -Dm 644 \ + "${wireguard_service_path}" \ + /etc/systemd/system/wg-quick@.service.d/override.conf + ${SUDO} systemctl daemon-reload + fi + fi + + if [[ -d /etc/wireguard ]]; then + # Backup the wireguard folder + WIREGUARD_BACKUP="wireguard_$(date +%Y-%m-%d-%H%M%S).tar.gz" + echo "::: Backing up the wireguard folder to /etc/${WIREGUARD_BACKUP}" + CURRENT_UMASK="$(umask)" + umask 0077 + ${SUDO} tar -czf "/etc/${WIREGUARD_BACKUP}" /etc/wireguard &> /dev/null + umask "${CURRENT_UMASK}" + + if [[ -f /etc/wireguard/wg0.conf ]]; then + ${SUDO} rm /etc/wireguard/wg0.conf + fi + else + # If compiled from source, the wireguard folder is not being created + ${SUDO} mkdir /etc/wireguard + fi + + # Ensure that only root is able to enter the wireguard folder + ${SUDO} chown root:root /etc/wireguard + ${SUDO} chmod 700 /etc/wireguard + + if [[ "${runUnattended}" == 'true' ]]; then + echo "::: The Server Keys will now be generated." + else + whiptail \ + --title "Server Information" \ + --msgbox "The Server Keys will now be generated." \ + "${r}" \ + "${c}" + fi + + # Remove configs and keys folders to make space for a new server when + # using 'Repair' or 'Reconfigure' over an existing installation + ${SUDO} rm -rf /etc/wireguard/configs + ${SUDO} rm -rf /etc/wireguard/keys + + ${SUDO} mkdir -p /etc/wireguard/configs + ${SUDO} touch /etc/wireguard/configs/clients.txt + ${SUDO} mkdir -p /etc/wireguard/keys + + # Generate private key and derive public key from it + wg genkey \ + | ${SUDO} tee /etc/wireguard/keys/server_priv &> /dev/null + ${SUDO} cat /etc/wireguard/keys/server_priv \ + | wg pubkey \ + | ${SUDO} tee /etc/wireguard/keys/server_pub &> /dev/null + + echo "::: Server Keys have been generated." + + { + echo '[Interface]' + echo "PrivateKey = $(${SUDO} cat /etc/wireguard/keys/server_priv)" + echo -n "Address = ${vpnGw}/${subnetClass}" + + if [[ "${pivpnenableipv6}" -eq 1 ]]; then + echo ",${vpnGwv6}/${subnetClassv6}" + else + echo + fi + + echo "MTU = ${pivpnMTU}" + echo "ListenPort = ${pivpnPORT}" + } | ${SUDO} tee /etc/wireguard/wg0.conf &> /dev/null + + echo "::: Server config generated." +} + +confNetwork() { + # Enable forwarding of internet traffic + echo 'net.ipv4.ip_forward=1' \ + | ${SUDO} tee /etc/sysctl.d/99-pivpn.conf > /dev/null + + if [[ "${pivpnenableipv6}" -eq 1 ]]; then + { + echo "net.ipv6.conf.all.forwarding=1" + echo "net.ipv6.conf.${IPv6dev}.accept_ra=2" + } | ${SUDO} tee -a /etc/sysctl.d/99-pivpn.conf > /dev/null + fi + + ${SUDO} sysctl -p /etc/sysctl.d/99-pivpn.conf > /dev/null + + if [[ "${USING_UFW}" -eq 1 ]]; then + echo "::: Detected UFW is enabled." + echo "::: Adding UFW rules..." + + ### Basic safeguard: if file is empty, there's been something weird going + ### on. + ### Note: no safeguard against imcomplete content as a result of previous + ### failures. + if [[ -s /etc/ufw/before.rules ]]; then + ${SUDO} cp -f /etc/ufw/before.rules /etc/ufw/before.rules.pre-pivpn + else + err "${0}: ERR: Sorry, won't touch empty file \"/etc/ufw/before.rules\"." + exit 1 + fi + + if [[ -s /etc/ufw/before6.rules ]]; then + ${SUDO} cp -f /etc/ufw/before6.rules /etc/ufw/before6.rules.pre-pivpn + else + err "${0}: ERR: Sorry, won't touch empty file \"/etc/ufw/before6.rules\"." + exit 1 + fi + + ### If there is already a "*nat" section just add our POSTROUTING MASQUERADE + if ${SUDO} grep -q "*nat" /etc/ufw/before.rules; then + local sed_pattern + + ### Onyl add the IPv4 NAT rule if it isn't already there + if ! ${SUDO} grep -q "${VPN}-nat-rule" /etc/ufw/before.rules; then + sed_pattern="/^*nat/{n;" + sed_pattern="${sed_pattern}s/\(:POSTROUTING ACCEPT .*\)/" + sed_pattern="${sed_pattern}\1\n-I POSTROUTING" + sed_pattern="${sed_pattern} -s ${pivpnNET}\/${subnetClass}" + sed_pattern="${sed_pattern} -o ${IPv4dev}" + sed_pattern="${sed_pattern} -j MASQUERADE" + sed_pattern="${sed_pattern} -m comment" + sed_pattern="${sed_pattern} --comment ${VPN}-nat-rule/" + sed_pattern="${sed_pattern}}" + ${SUDO} sed "${sed_pattern}" -i /etc/ufw/before.rules + fi + else + sed_pattern="/delete these required/i" + sed_pattern="${sed_pattern} *nat\n:POSTROUTING ACCEPT [0:0]\n" + sed_pattern="${sed_pattern}-I POSTROUTING" + sed_pattern="${sed_pattern} -s ${pivpnNET}\/${subnetClass}" + sed_pattern="${sed_pattern} -o ${IPv4dev}" + sed_pattern="${sed_pattern} -j MASQUERADE" + sed_pattern="${sed_pattern} -m comment" + sed_pattern="${sed_pattern} --comment ${VPN}-nat-rule\n" + sed_pattern="${sed_pattern}COMMIT\n" + ${SUDO} sed "${sed_pattern}" -i /etc/ufw/before.rules + fi + + if [[ "${pivpnenableipv6}" -eq 1 ]]; then + local sed_pattern + + if ${SUDO} grep -q "*nat" /etc/ufw/before6.rules; then + ### Onyl add the IPv6 NAT rule if it isn't already there + if ! ${SUDO} grep -q "${VPN}-nat-rule" /etc/ufw/before6.rules; then + sed_pattern="/^*nat/{n;" + sed_pattern="${sed_pattern}s/\(:POSTROUTING ACCEPT .*\)/" + sed_pattern="${sed_pattern}\1\n-I POSTROUTING" + sed_pattern="${sed_pattern} -s ${pivpnNETv6}\/${subnetClassv6}" + sed_pattern="${sed_pattern} -o ${IPv6dev}" + sed_pattern="${sed_pattern} -j MASQUERADE" + sed_pattern="${sed_pattern} -m comment" + sed_pattern="${sed_pattern} --comment ${VPN}-nat-rule/" + sed_pattern="${sed_pattern}}" + ${SUDO} sed "${sed_pattern}" -i /etc/ufw/before6.rules + fi + else + sed_pattern="/delete these required/i" + sed_pattern="${sed_pattern} *nat\n:POSTROUTING ACCEPT [0:0]\n" + sed_pattern="${sed_pattern}-I POSTROUTING" + sed_pattern="${sed_pattern} -s ${pivpnNETv6}\/${subnetClassv6}" + sed_pattern="${sed_pattern} -o ${IPv6dev}" + sed_pattern="${sed_pattern} -j MASQUERADE" + sed_pattern="${sed_pattern} -m comment" + sed_pattern="${sed_pattern} --comment ${VPN}-nat-rule\n" + sed_pattern="${sed_pattern}COMMIT\n" + ${SUDO} sed "${sed_pattern}" -i /etc/ufw/before6.rules + fi + fi + + # Checks for any existing UFW rules and + # insert rules at the beginning of the chain + # (in case there are other rules that may drop the traffic) + if ${SUDO} ufw status numbered | grep -E "\[.[0-9]{1}\]" > /dev/null; then + ${SUDO} ufw insert 1 \ + allow "${pivpnPORT}/${pivpnPROTO}" \ + comment "allow-${VPN}" > /dev/null + + ${SUDO} ufw route insert 1 \ + allow in on "${pivpnDEV}" \ + from "${pivpnNET}/${subnetClass}" \ + out on "${IPv4dev}" to any > /dev/null + + if [[ "${pivpnenableipv6}" -eq 1 ]]; then + ${SUDO} ufw route insert 1 \ + allow in on "${pivpnDEV}" \ + from "${pivpnNETv6}/${subnetClassv6}" \ + out on "${IPv6dev}" to any > /dev/null + fi + fi + + ${SUDO} ufw reload > /dev/null + echo "::: UFW configuration completed." + return + fi + + # Now some checks to detect which rules we need to add. + # On a newly installed system all policies should be ACCEPT, + # so the only required rule would be the MASQUERADE one. + + if ! ${SUDO} iptables -t nat -S \ + | grep -q "${VPN}-nat-rule"; then + ${SUDO} iptables \ + -t nat \ + -I POSTROUTING \ + -s "${pivpnNET}/${subnetClass}" \ + -o "${IPv4dev}" \ + -j MASQUERADE \ + -m comment \ + --comment "${VPN}-nat-rule" + fi + + if [[ "${pivpnenableipv6}" -eq 1 ]]; then + if ! ${SUDO} ip6tables -t nat -S \ + | grep -q "${VPN}-nat-rule"; then + ${SUDO} ip6tables \ + -t nat \ + -I POSTROUTING \ + -s "${pivpnNETv6}/${subnetClassv6}" \ + -o "${IPv6dev}" \ + -j MASQUERADE \ + -m comment \ + --comment "${VPN}-nat-rule" + fi + fi + + # Count how many rules are in the INPUT and FORWARD chain. + # When parsing input from iptables -S, '^-P' skips the policies + # and 'ufw-' skips ufw chains (in case ufw was found + # installed but not enabled). + + # Grep returns non 0 exit code where there are no matches, + # however that would make the script exit, + # for this reasons we use '|| true' to force exit code 0 + INPUT_RULES_COUNT="$(${SUDO} iptables -S INPUT \ + | grep -vcE '(^-P|ufw-)')" + FORWARD_RULES_COUNT="$(${SUDO} iptables -S FORWARD \ + | grep -vcE '(^-P|ufw-)')" + INPUT_POLICY="$(${SUDO} iptables -S INPUT \ + | grep '^-P' \ + | awk '{print $3}')" + FORWARD_POLICY="$(${SUDO} iptables -S FORWARD \ + | grep '^-P' \ + | awk '{print $3}')" + + if [[ "${pivpnenableipv6}" -eq 1 ]]; then + INPUT_RULES_COUNTv6="$(${SUDO} ip6tables -S INPUT \ + | grep -vcE '(^-P|ufw-)')" + FORWARD_RULES_COUNTv6="$(${SUDO} ip6tables -S FORWARD \ + | grep -vcE '(^-P|ufw-)')" + INPUT_POLICYv6="$(${SUDO} ip6tables -S INPUT \ + | grep '^-P' \ + | awk '{print $3}')" + FORWARD_POLICYv6="$(${SUDO} ip6tables -S FORWARD \ + | grep '^-P' \ + | awk '{print $3}')" + fi + + # If rules count is not zero, we assume we need to explicitly allow traffic. + # Same conclusion if there are no rules and the policy is not ACCEPT. + # Note that rules are being added to the top of the chain (using -I). + + if [[ "${INPUT_RULES_COUNT}" -ne 0 ]] \ + || [[ "${INPUT_POLICY}" != "ACCEPT" ]]; then + if ! ${SUDO} iptables -S \ + | grep -q "${VPN}-input-rule"; then + ${SUDO} iptables \ + -I INPUT 1 \ + -i "${IPv4dev}" \ + -p "${pivpnPROTO}" \ + --dport "${pivpnPORT}" \ + -j ACCEPT \ + -m comment \ + --comment "${VPN}-input-rule" + fi + + INPUT_CHAIN_EDITED=1 + else + INPUT_CHAIN_EDITED=0 + fi + + if [[ "${pivpnenableipv6}" -eq 1 ]]; then + if [[ "${INPUT_RULES_COUNTv6}" -ne 0 ]] \ + || [[ "${INPUT_POLICYv6}" != "ACCEPT" ]]; then + if ! ${SUDO} ip6tables -S \ + | grep -q "${VPN}-input-rule"; then + ${SUDO} ip6tables \ + -I INPUT 1 \ + -i "${IPv6dev}" \ + -p "${pivpnPROTO}" \ + --dport "${pivpnPORT}" \ + -j ACCEPT \ + -m comment \ + --comment "${VPN}-input-rule" + fi + + INPUT_CHAIN_EDITEDv6=1 + else + INPUT_CHAIN_EDITEDv6=0 + fi + fi + + if [[ "${FORWARD_RULES_COUNT}" -ne 0 ]] \ + || [[ "${FORWARD_POLICY}" != "ACCEPT" ]]; then + if ! ${SUDO} iptables -S \ + | grep -q "${VPN}-forward-rule"; then + ${SUDO} iptables \ + -I FORWARD 1 \ + -d "${pivpnNET}/${subnetClass}" \ + -i "${IPv4dev}" \ + -o "${pivpnDEV}" \ + -m conntrack \ + --ctstate RELATED,ESTABLISHED \ + -j ACCEPT \ + -m comment \ + --comment "${VPN}-forward-rule" + ${SUDO} iptables \ + -I FORWARD 2 \ + -s "${pivpnNET}/${subnetClass}" \ + -i "${pivpnDEV}" \ + -o "${IPv4dev}" \ + -j ACCEPT \ + -m comment \ + --comment "${VPN}-forward-rule" + fi + + FORWARD_CHAIN_EDITED=1 + else + FORWARD_CHAIN_EDITED=0 + fi + + if [[ "${pivpnenableipv6}" -eq 1 ]]; then + if [[ "${FORWARD_RULES_COUNTv6}" -ne 0 ]] \ + || [[ "${FORWARD_POLICYv6}" != "ACCEPT" ]]; then + if ! ${SUDO} ip6tables -S \ + | grep -q "${VPN}-forward-rule"; then + ${SUDO} ip6tables \ + -I FORWARD 1 \ + -d "${pivpnNETv6}/${subnetClassv6}" \ + -i "${IPv6dev}" \ + -o "${pivpnDEV}" \ + -m conntrack \ + --ctstate RELATED,ESTABLISHED \ + -j ACCEPT \ + -m comment \ + --comment "${VPN}-forward-rule" + ${SUDO} ip6tables \ + -I FORWARD 2 \ + -s "${pivpnNETv6}/${subnetClassv6}" \ + -i "${pivpnDEV}" \ + -o "${IPv6dev}" \ + -j ACCEPT \ + -m comment \ + --comment "${VPN}-forward-rule" + fi + + FORWARD_CHAIN_EDITEDv6=1 + else + FORWARD_CHAIN_EDITEDv6=0 + fi + fi + + case "${PLAT}" in + Debian | Raspbian | Ubuntu) + ${SUDO} iptables-save \ + | ${SUDO} tee /etc/iptables/rules.v4 > /dev/null + ${SUDO} ip6tables-save \ + | ${SUDO} tee /etc/iptables/rules.v6 > /dev/null + ;; + esac + + { + echo "INPUT_CHAIN_EDITED=${INPUT_CHAIN_EDITED}" + echo "FORWARD_CHAIN_EDITED=${FORWARD_CHAIN_EDITED}" + echo "INPUT_CHAIN_EDITEDv6=${INPUT_CHAIN_EDITEDv6}" + echo "FORWARD_CHAIN_EDITEDv6=${FORWARD_CHAIN_EDITEDv6}" + } >> "${tempsetupVarsFile}" +} + +confLogging() { + # Pre-create rsyslog/logrotate config directories if missing, + # to assure logs are handled as expected when those are + # installed at a later time + ${SUDO} mkdir -p /etc/{rsyslog,logrotate}.d + + if [[ "${PLAT}" == 'Alpine' ]]; then + program_name='openvpn' + else + program_name='ovpn-server' + fi + + echo "if \$programname == '${program_name}' then /var/log/openvpn.log +if \$programname == '${program_name}' then stop" | ${SUDO} tee /etc/rsyslog.d/30-openvpn.conf > /dev/null + + echo "/var/log/openvpn.log +{ + rotate 4 + weekly + missingok + notifempty + compress + delaycompress + sharedscripts + postrotate + invoke-rc.d rsyslog rotate >/dev/null 2>&1 || true + endscript +}" | ${SUDO} tee /etc/logrotate.d/openvpn > /dev/null + + # Restart the logging service + case "${PLAT}" in + Debian | Raspbian | Ubuntu) + ${SUDO} systemctl -q is-active rsyslog.service \ + && ${SUDO} systemctl restart rsyslog.service + ;; + Alpine) + ${SUDO} rc-service -is rsyslog restart + ${SUDO} rc-service -iN rsyslog start + ;; + esac +} + +restartServices() { + # Start services + echo "::: Restarting services..." + + case "${PLAT}" in + Debian | Raspbian | Ubuntu) + if [[ "${VPN}" == "openvpn" ]]; then + ${SUDO} systemctl enable openvpn.service &> /dev/null + ${SUDO} systemctl restart openvpn.service + elif [[ "${VPN}" == "wireguard" ]]; then + ${SUDO} systemctl enable wg-quick@wg0.service &> /dev/null + ${SUDO} systemctl restart wg-quick@wg0.service + fi + + ;; + Alpine) + if [[ "${VPN}" == 'openvpn' ]]; then + ${SUDO} rc-update add openvpn default &> /dev/null + ${SUDO} rc-service -s openvpn restart + ${SUDO} rc-service -N openvpn start + elif [[ "${VPN}" == 'wireguard' ]]; then + ${SUDO} rc-update add wg-quick default &> /dev/null + ${SUDO} rc-service -s wg-quick restart + ${SUDO} rc-service -N wg-quick start + fi + + ;; + esac +} + +askUnattendedUpgrades() { + if [[ "${runUnattended}" == 'true' ]]; then + if [[ -z "${UNATTUPG}" ]]; then + UNATTUPG=1 + echo "::: No preference regarding unattended upgrades, assuming yes" + else + if [[ "${UNATTUPG}" -eq 1 ]]; then + echo "::: Enabling unattended upgrades" + else + echo "::: Skipping unattended upgrades" + fi + fi + + echo "UNATTUPG=${UNATTUPG}" >> "${tempsetupVarsFile}" + return + fi + + whiptail \ + --msgbox \ + --backtitle "Security Updates" \ + --title "Unattended Upgrades" \ + "Since this server will have at least one port open to the internet, \ +it is recommended you enable unattended-upgrades. +This feature will check daily for security package updates only and apply \ +them when necessary. +It will NOT automatically reboot the server so to fully apply some updates \ +you should periodically reboot." \ + "${r}" \ + "${c}" + + if whiptail \ + --backtitle "Security Updates" \ + --title "Unattended Upgrades" \ + --yesno \ + "Do you want to enable unattended upgrades \ +of security patches to this server?" \ + "${r}" \ + "${c}"; then + UNATTUPG=1 + else + UNATTUPG=0 + fi + + echo "UNATTUPG=${UNATTUPG}" >> "${tempsetupVarsFile}" +} + +confUnattendedUpgrades() { + local PIVPN_DEPS periodic_file + + if [[ "${PKG_MANAGER}" == 'apt-get' ]]; then + PIVPN_DEPS=(unattended-upgrades) + installDependentPackages PIVPN_DEPS[@] + aptConfDir="/etc/apt/apt.conf.d" + + # Raspbian's unattended-upgrades package downloads Debian's config, + # so we copy over the proper config + # https://github.com/mvo5/unattended-upgrades/blob/master/data/50unattended-upgrades.Raspbian + # Add the remaining settings for all other distributions + if [[ "${PLAT}" == "Raspbian" ]]; then + ${SUDO} install -m 644 \ + "${pivpnFilesDir}/files${aptConfDir}/50unattended-upgrades.Raspbian" \ + "${aptConfDir}/50unattended-upgrades" + fi + + if [[ "${PLAT}" == "Ubuntu" ]]; then + periodic_file="${aptConfDir}/10periodic" + else + periodic_file="${aptConfDir}/02periodic" + fi + + # Ubuntu 50unattended-upgrades should already just have security enabled + # so we just need to configure the 10periodic file + { + echo "APT::Periodic::Update-Package-Lists \"1\";" + echo "APT::Periodic::Download-Upgradeable-Packages \"1\";" + echo "APT::Periodic::Unattended-Upgrade \"1\";" + + if [[ "${PLAT}" == "Ubuntu" ]]; then + echo "APT::Periodic::AutocleanInterval \"5\";" + else + echo "APT::Periodic::Enable \"1\";" + echo "APT::Periodic::AutocleanInterval \"7\";" + echo "APT::Periodic::Verbose \"0\";" + fi + } | ${SUDO} tee "${periodic_file}" > /dev/null + + # Enable automatic updates via the bullseye repository + # when installing from debian package + if [[ "${VPN}" == "wireguard" ]]; then + if [[ -f /etc/apt/sources.list.d/pivpn-bullseye-repo.list ]]; then + if ! grep -q "\"o=${PLAT},n=bullseye\";" \ + "${aptConfDir}/50unattended-upgrades"; then + local sed_pattern + sed_pattern=" {/a\"o=${PLAT},n=bullseye\";" + sed_pattern="${sed_pattern} {/a\"o=${PLAT},n=bullseye\";" + ${SUDO} sed -i "${sed_pattern}" "${aptConfDir}/50unattended-upgrades" + fi + fi + fi + elif [[ "${PKG_MANAGER}" == 'apk' ]]; then + local down_dir + ## install dependencies + # shellcheck disable=SC2086 + ${SUDO} ${PKG_INSTALL} unzip asciidoctor + + if ! down_dir="$(mktemp -d)"; then + err "::: Failed to create download directory for apk-autoupdate!" + exit 1 + fi + + ## download binaries + curl -fLo "${down_dir}/master.zip" \ + https://github.com/jirutka/apk-autoupdate/archive/refs/heads/master.zip + unzip -qd "${down_dir}" "${down_dir}/master.zip" + + ( + cd "${down_dir}/apk-autoupdate-master" || exi + + ## personalize binaries + sed -i -E -e 's/^(prefix\s*:=).*/\1 \/usr/' Makefile + + ## install + ${SUDO} make install + + if ! command -v apk-autoupdate &> /dev/null; then + err "::: Failed to compile and install apk-autoupdate!" + exit + fi + ) || exit 1 + + ${SUDO} install -m 0755 \ + "${pivpnFilesDir}/files/etc/apk/personal_autoupdate.conf" \ + /etc/apk/personal_autoupdate.conf + ${SUDO} apk-autoupdate /etc/apk/personal_autoupdate.conf + fi +} + +writeConfigFiles() { + # Save installation setting to the final location + echo "INSTALLED_PACKAGES=(${INSTALLED_PACKAGES[*]})" >> "${tempsetupVarsFile}" + echo "::: Setupfiles copied to ${setupConfigDir}/${VPN}/${setupVarsFile}" + ${SUDO} mkdir -p "${setupConfigDir}/${VPN}/" + ${SUDO} cp "${tempsetupVarsFile}" "${setupConfigDir}/${VPN}/${setupVarsFile}" +} + +installScripts() { + # Ensure /opt exists (issue #607) + ${SUDO} mkdir -p /opt + + if [[ "${VPN}" == 'wireguard' ]]; then + othervpn='openvpn' + else + othervpn='wireguard' + fi + + # Symlink scripts from /usr/local/src/pivpn to their various locations + echo -e "::: Installing scripts to ${pivpnScriptDir}..." + + # if the other protocol file exists it has been installed + if [[ -r "${setupConfigDir}/${othervpn}/${setupVarsFile}" ]]; then + # Both are installed, no bash completion, unlink if already there + ${SUDO} unlink /etc/bash_completion.d/pivpn + + # Unlink the protocol specific pivpn script and symlink the common + # script to the location instead + ${SUDO} unlink /usr/local/bin/pivpn + ${SUDO} ln -sfT "${pivpnFilesDir}/scripts/pivpn" /usr/local/bin/pivpn + else + # Check if bash_completion scripts dir exists and creates it if not + ${SUDO} mkdir -p /etc/bash_completion.d + + # Only one protocol is installed, symlink bash completion, the pivpn script + # and the script directory + ${SUDO} ln -sfT \ + "${pivpnFilesDir}/scripts/${VPN}/bash-completion" \ + /etc/bash_completion.d/pivpn + ${SUDO} ln -sfT \ + "${pivpnFilesDir}/scripts/${VPN}/pivpn.sh" \ + /usr/local/bin/pivpn + ${SUDO} ln -sf "${pivpnFilesDir}/scripts/" "${pivpnScriptDir}" + # shellcheck disable=SC1091 + . /etc/bash_completion.d/pivpn + fi + + echo " done." +} + +displayFinalMessage() { + # Ensure that cached writes reach persistent storage + echo "::: Flushing writes to disk..." + + sync + + echo "::: done." + + if [[ "${runUnattended}" == 'true' ]]; then + echo "::: Installation Complete!" + echo "::: Now run 'pivpn add' to create the client profiles." + echo "::: Run 'pivpn help' to see what else you can do!" + echo + echo -n "::: If you run into any issue, please read all our documentation " + echo "carefully." + echo "::: All incomplete posts or bug reports will be ignored or deleted." + echo + echo "::: Thank you for using PiVPN." + echo "::: It is strongly recommended you reboot after installation." + return + fi + + # Final completion message to user + whiptail \ + --backtitle "Make it so." \ + --title "Installation Complete!" \ + --msgbox "Now run 'pivpn add' to create the client profiles. +Run 'pivpn help' to see what else you can do! + +If you run into any issue, please read all our documentation carefully. +All incomplete posts or bug reports will be ignored or deleted. + +Thank you for using PiVPN." "${r}" "${c}" + + if whiptail \ + --title "Reboot" \ + --defaultno \ + --yesno "It is strongly recommended you reboot after installation. \ +Would you like to reboot now?" "${r}" "${c}"; then + whiptail \ + --title "Rebooting" \ + --msgbox "The system will now reboot." "${r}" "${c}" + printf "\\nRebooting system...\\n" + ${SUDO} sleep 3 + + ${SUDO} reboot + fi +} + +main "$@" diff --git a/port_forwarding.sh b/port_forwarding.sh new file mode 100755 index 0000000..d7d2a0a --- /dev/null +++ b/port_forwarding.sh @@ -0,0 +1,203 @@ +#!/usr/bin/env bash +# Copyright (C) 2020 Private Internet Access, Inc. +# +# 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. + +# This function allows you to check if the required tools have been installed. +check_tool() { + cmd=$1 + if ! command -v "$cmd" >/dev/null; then + echo "$cmd could not be found" + echo "Please install $cmd" + exit 1 + fi +} + +# Now we call the function to make sure we can use curl and jq. +check_tool curl +check_tool jq +#username=$(cat /etc/openvpn/auth.txt | head -1) +#password=$(cat /etc/openvpn/auth.txt | tail -1) +#PF_GATEWAY=$(ip route | grep tun | awk '/^0.0.0.0*/ { print $3; }') +#PIA_TOKEN=$(curl -s -L -X POST 'https://www.privateinternetaccess.com/api/client/v2/token' --form "username=$username" --form "password=$password" | jq -r .token) +#PF_HOSTNAME=$(curl -s 'https://serverlist.piaservers.net/vpninfo/servers/v6' | head -1 | jq --arg REGION_ID 'uk_manchester' -r '.regions[] | select(.id==$REGION_ID)' | jq -r '.servers.ovpnudp[0].cn') +# Check if the mandatory environment variables are set. +if [[ -z $PF_GATEWAY || -z $PIA_TOKEN || -z $PF_HOSTNAME ]]; then + echo "This script requires 3 env vars:" + echo "PF_GATEWAY - the IP of your gateway" + echo "PF_HOSTNAME - name of the host used for SSL/TLS certificate verification" + echo "PIA_TOKEN - the token you use to connect to the vpn services" + echo + echo "An easy solution is to just run get_region_and_token.sh" + echo "as it will guide you through getting the best server and" + echo "also a token. Detailed information can be found here:" + echo "https://github.com/pia-foss/manual-connections" +exit 1 +fi + +# Check if terminal allows output, if yes, define colors for output +if [[ -t 1 ]]; then + ncolors=$(tput colors) + if [[ -n $ncolors && $ncolors -ge 8 ]]; then + red=$(tput setaf 1) # ANSI red + green=$(tput setaf 2) # ANSI green + nc=$(tput sgr0) # No Color + else + red='' + green='' + nc='' # No Color + fi +fi + +# The port forwarding system has required two variables: +# PAYLOAD: contains the token, the port and the expiration date +# SIGNATURE: certifies the payload originates from the PIA network. + +# Basically PAYLOAD+SIGNATURE=PORT. You can use the same PORT on all servers. +# The system has been designed to be completely decentralized, so that your +# privacy is protected even if you want to host services on your systems. + +# You can get your PAYLOAD+SIGNATURE with a simple curl request to any VPN +# gateway, no matter what protocol you are using. Considering WireGuard has +# already been automated in this repo, here is a command to help you get +# your gateway if you have an active OpenVPN connection: +# $ ip route | head -1 | grep tun | awk '{ print $3 }' +# This section will get updated as soon as we created the OpenVPN script. + +# Get the payload and the signature from the PF API. This will grant you +# access to a random port, which you can activate on any server you connect to. +# If you already have a signature, and you would like to re-use that port, +# save the payload_and_signature received from your previous request +# in the env var PAYLOAD_AND_SIGNATURE, and that will be used instead. +if [[ -z $PAYLOAD_AND_SIGNATURE ]]; then + echo + echo -n "Getting new signature... " + payload_and_signature="$(curl -s -m 5 \ + --connect-to "$PF_HOSTNAME::$PF_GATEWAY:" \ + --cacert "ca.rsa.4096.crt" \ + -G --data-urlencode "token=${PIA_TOKEN}" \ + "https://${PF_HOSTNAME}:19999/getSignature")" +else + payload_and_signature=$PAYLOAD_AND_SIGNATURE + echo -n "Checking the payload_and_signature from the env var... " +fi +export payload_and_signature + +# Check if the payload and the signature are OK. +# If they are not OK, just stop the script. +if [[ $(echo "$payload_and_signature" | jq -r '.status') != "OK" ]]; then + echo -e "${red}The payload_and_signature variable does not contain an OK status.${nc}" + exit 1 +fi +echo -e "${green}OK!${nc}" + +# We need to get the signature out of the previous response. +# The signature will allow the us to bind the port on the server. +signature=$(echo "$payload_and_signature" | jq -r '.signature') + +# The payload has a base64 format. We need to extract it from the +# previous response and also get the following information out: +# - port: This is the port you got access to +# - expires_at: this is the date+time when the port expires +payload=$(echo "$payload_and_signature" | jq -r '.payload') +port=$(echo "$payload" | base64 -d | jq -r '.port') + +# The port normally expires after 2 months. If you consider +# 2 months is not enough for your setup, please open a ticket. +expires_at=$(echo "$payload" | base64 -d | jq -r '.expires_at') + +echo -ne " +Signature ${green}$signature${nc} +Payload ${green}$payload${nc} + +--> The port is ${green}$port${nc} and it will expire on ${red}$expires_at${nc}. <-- + +Trying to bind the port... " + +export PF_PORT=$port +echo $port > /home/pi/pia-pivpn/pf_port +date --date="$expires_at" >> /home/pi/pia-pivpn/pf_port + +attempt-bind-port() { + bind_port_response="$(curl -Gs -m 5 \ + --connect-to "$PF_HOSTNAME::$PF_GATEWAY:" \ + --cacert "ca.rsa.4096.crt" \ + --data-urlencode "payload=${payload}" \ + --data-urlencode "signature=${signature}" \ + "https://${PF_HOSTNAME}:19999/bindPort")" + echo -e "${green}OK!${nc}" + + # If port did not bind, just exit the script. + # This script will exit in 2 months, since the port will expire. + export bind_port_response + i=1 + until [[ $i == 30 || $(echo "$bind_port_response" | jq -r '.status') == "OK" ]]; do + echo "Retrying connection..." + bind_port_response="$(curl -Gs -m 5 \ + --connect-to "$PF_HOSTNAME::$PF_GATEWAY:" \ + --cacert "ca.rsa.4096.crt" \ + --data-urlencode "payload=${payload}" \ + --data-urlencode "signature=${signature}" \ + "https://${PF_HOSTNAME}:19999/bindPort")" + echo -e "${green}OK!${nc}" + sleep 3 + + i=$((i+1)) + done + + if [[ $i == 30 && $(echo "$bind_port_response" | jq -r '.status') != "OK" ]]; then + echo -e "${red}The API did not return OK when trying to bind port... Exiting.${nc}" + exit 1 + fi + echo -e Forwarded port'\t'"${green}$port${nc}" + echo -e Refreshed on'\t'"${green}$(date)${nc}" + echo -e Expires on'\t'"${red}$(date --date="$expires_at")${nc}" + echo -e "\n${green}This script will need to remain active to use port forwarding, and will refresh every 15 minutes.${nc}\n" +} + +# Now we have all required data to create a request to bind the port. +# We will repeat this request every 15 minutes, in order to keep the port +# alive. The servers have no mechanism to track your activity, so they +# will just delete the port forwarding if you don't send keepalives. +while true; do +# bind_port_response="$(curl -Gs -m 5 \ +# --connect-to "$PF_HOSTNAME::$PF_GATEWAY:" \ +# --cacert "ca.rsa.4096.crt" \ +# --data-urlencode "payload=${payload}" \ +# --data-urlencode "signature=${signature}" \ +# "https://${PF_HOSTNAME}:19999/bindPort")" +# echo -e "${green}OK!${nc}" +# +# # If port did not bind, just exit the script. +# # This script will exit in 2 months, since the port will expire. +# export bind_port_response +# if [[ $(echo "$bind_port_response" | jq -r '.status') != "OK" ]]; then +# echo -e "${red}The API did not return OK when trying to bind port... Exiting.${nc}" +# exit 1 +# fi +# echo -e Forwarded port'\t'"${green}$port${nc}" +# echo -e Refreshed on'\t'"${green}$(date)${nc}" +# echo -e Expires on'\t'"${red}$(date --date="$expires_at")${nc}" +# echo -e "\n${green}This script will need to remain active to use port forwarding, and will refresh every 15 minutes.${nc}\n" +# + attempt-bind-port + + # sleep 15 minutes + sleep 900 +done diff --git a/run_setup.sh b/run_setup.sh new file mode 100755 index 0000000..83ac247 --- /dev/null +++ b/run_setup.sh @@ -0,0 +1,517 @@ +#!/usr/bin/env bash +# Copyright (C) 2020 Private Internet Access, Inc. +# +# 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. +export PREFERRED_REGION=uk_manchester +# Check if terminal allows output, if yes, define colors for output +if [[ -t 1 ]]; then + ncolors=$(tput colors) + if [[ -n $ncolors && $ncolors -ge 8 ]]; then + red=$(tput setaf 1) # ANSI red + green=$(tput setaf 2) # ANSI green + nc=$(tput sgr0) # No Color + else + red='' + green='' + nc='' # No Color + fi +fi + +# Variables to use for validating input +intCheck='^[0-9]+$' +floatCheck='^[0-9]+([.][0-9]+)?$' + +# Only allow script to run as root +if (( EUID != 0 )); then + echo -e "${red}This script needs to be run as root. Try again with 'sudo $0'${nc}" + exit 1 +fi + +# Erase previous authentication token if present +rm -f /opt/piavpn-manual/token /opt/piavpn-manual/latencyList + +# Retry login if no token is generated +while :; do + while :; do + # Check for in-line definition of $PIA_USER + if [[ -z $PIA_USER ]]; then +# echo +# read -r -p "PIA username (p#######): " PIA_USER + PIA_USER=$(cat /etc/openvpn/auth.txt | head -1) + fi + + # Confirm format of PIA_USER input + unPrefix=${PIA_USER:0:1} + unSuffix=${PIA_USER:1} + if [[ -z $PIA_USER ]]; then + echo -e "\n${red}You must provide input.${nc}" + elif [[ ${#PIA_USER} != 8 ]]; then + echo -e "\n${red}A PIA username is always 8 characters long.${nc}" + elif [[ $unPrefix != "P" ]] && [[ $unPrefix != "p" ]]; then + echo -e "\n${red}A PIA username must start with \"p\".${nc}" + elif ! [[ $unSuffix =~ $intCheck ]]; then + echo -e "\n${red}Username formatting is always p#######!${nc}" + else + echo -e "\n${green}PIA_USER=$PIA_USER${nc}" + break + fi + PIA_USER="" + done + export PIA_USER + + while :; do + # Check for in-line definition of $PIA_PASS + if [[ -z $PIA_PASS ]]; then +# echo +# echo -n "PIA password: " +# read -r -s PIA_PASS +# echo + PIA_PASS=$(cat /etc/openvpn/auth.txt | tail -1) + fi + + # Confirm format of PIA_PASS input + if [[ -z $PIA_PASS ]]; then + echo -e "\n${red}You must provide input.${nc}" + elif [[ ${#PIA_PASS} -lt 8 ]]; then + echo -e "\n${red}A PIA password is always a minimum of 8 characters long.${nc}" + else + echo -e "\n${green}PIA_PASS input received.${nc}" + echo + break + fi + PIA_PASS="" + done + export PIA_PASS + + # Confirm credentials and generate token + ./get_token.sh + + tokenLocation="/opt/piavpn-manual/token" + # If the script failed to generate an authentication token, the script will exit early. + if [[ ! -f $tokenLocation ]]; then + read -r -p "Do you want to try again ([N]o/[y]es): " tryAgain + if ! echo "${tryAgain:0:1}" | grep -iq y; then + exit 1 + fi + PIA_USER="" + PIA_PASS="" + else + PIA_TOKEN=$( awk 'NR == 1' /opt/piavpn-manual/token ) + export PIA_TOKEN + rm -f /opt/piavpn-manual/token + break + fi +done + +# Check for in-line definition of $DIP_TOKEN +if [[ -z $DIP_TOKEN ]]; then + # Check for dedicated IP + #echo -n "Do you want to use a dedicated IP token ([N]o/[y]es): " + #read useDIP + #echo + useDIP=n + pfOption="true" +else + if echo ${DIP_TOKEN:0:1} | grep -iq n; then + useDIP="no" + echo -e "${red}Not using a dedicated IP.${nc}" + echo + DIP_TOKEN="" + else + useDIP="yes" + fi +fi + +if echo ${useDIP:0:1} | grep -iq y; then + useDIP="true" + while :; do + while :; do + # Check for in-line definition of $DIP_TOKEN + if [[ -z $DIP_TOKEN ]]; then + read -p "Dedicated token (DIP#############################): " DIP_TOKEN + fi + # Confirm format of DIP_TOKEN input + dipPrefix=$( echo ${DIP_TOKEN:0:3} ) + if [[ -z "$DIP_TOKEN" ]]; then + echo -e "\n${red}You must provide input.${nc}" + elif [[ ${#DIP_TOKEN} != 32 ]]; then + echo -e "\n${red}A dedicated IP token is always 32 characters long.${nc}" + elif [[ $dipPrefix != "DIP" ]]; then + echo -e "\n${red}A dedicated IP token must start with \"DIP\".${nc}" + else + break + fi + echo + DIP_TOKEN="" + done + export DIP_TOKEN + # Confirm DIP_TOKEN and retrieve connection details + ./get_dip.sh + dipDetails="/opt/piavpn-manual/dipAddress" + # If the script failed to generate retrieve dedicated IP information, the script will exit early. + if [ ! -f "$dipDetails" ]; then + read -p "Do you want to try again ([N]o/[y]es): " tryAgain + echo + if ! echo ${tryAgain:0:1} | grep -iq y; then + exit 1 + fi + DIP_TOKEN="" + else + dipAddress=$( awk 'NR == 1' /opt/piavpn-manual/dipAddress ) + dipHostname=$( awk 'NR == 2' /opt/piavpn-manual/dipAddress) + dipKey=$( awk 'NR == 3' /opt/piavpn-manual/dipAddress ) + pfOption=$( awk 'NR == 5' /opt/piavpn-manual/dipAddress ) + rm -f /opt/piavpn-manual/dipAddress + break + fi + done +fi + +if [[ -z $DIP_TOKEN ]]; then + echo "${green}DIP_TOKEN=none${nc}" +else + echo "${green}DIP_TOKEN=$DIP_TOKEN${nc}" +fi +echo + +# Erase previous connection details if present +rm -f /opt/piavpn-manual/token /opt/piavpn-manual/latencyList + +# Prompt for port forwarding if no DIP or DIP allows it +if [[ $pfOption = "false" ]]; then + PIA_PF="false" +fi +# Check for in-line definition of PIA_PF and prompt for input +if [[ -z $PIA_PF ]]; then +# echo -n "Do you want a forwarding port assigned ([N]o/[y]es): " +# read -r portForwarding +# echo + portForwarding=y + if echo "${portForwarding:0:1}" | grep -iq y; then + PIA_PF="true" + fi +fi +if [[ $PIA_PF != "true" ]]; then + PIA_PF="false" +fi +export PIA_PF +echo -e "${green}PIA_PF=$PIA_PF${nc}" +echo + +# Check for in-line definition of DISABLE_IPV6 and prompt for input +if [[ -z $DISABLE_IPV6 ]]; then +# echo "Having active IPv6 connections might compromise security by allowing" +# echo "split tunnel connections that run outside the VPN tunnel." +# echo -n "Do you want to disable IPv6? (Y/n): " +# read -r DISABLE_IPV6 + echo +fi + +if echo "${DISABLE_IPV6:0:1}" | grep -iq n; then + echo -e "${red}IPv6 settings have not been altered. + ${nc}" +else + echo -e "The variable ${green}DISABLE_IPV6=$DISABLE_IPV6${nc}, does not start with 'n' for 'no'. +${green}Defaulting to yes.${nc} +" + sysctl -w net.ipv6.conf.all.disable_ipv6=1 + sysctl -w net.ipv6.conf.default.disable_ipv6=1 + echo + echo -e "${red}IPv6 has been disabled${nc}, you can ${green}enable it again with: " + echo "sysctl -w net.ipv6.conf.all.disable_ipv6=0" + echo "sysctl -w net.ipv6.conf.default.disable_ipv6=0" + echo -e "${nc}" +fi + +# Only prompt for server selection if no DIP has been specified +if [[ -z $DIP_TOKEN ]]; then + # Input validation and check for conflicting declarations of AUTOCONNECT and PREFERRED_REGION + # If both variables are set, AUTOCONNECT has superiority and PREFERRED_REGION is ignored + if [[ -z $AUTOCONNECT ]]; then + echo "AUTOCONNECT was not declared." + echo + selectServer="ask" + elif echo "${AUTOCONNECT:0:1}" | grep -iq f; then + if [[ $AUTOCONNECT != "false" ]]; then + echo -e "The variable ${green}AUTOCONNECT=$AUTOCONNECT${nc}, starts with 'f' for 'false'." + AUTOCONNECT="false" + echo -e "Updated ${green}AUTOCONNECT=$AUTOCONNECT${nc}" + echo + fi + selectServer="yes" + else + if [[ $AUTOCONNECT != "true" ]]; then + echo -e "The variable ${green}AUTOCONNECT=$AUTOCONNECT${nc}, does not start with 'f' for 'false'." + AUTOCONNECT="true" + echo -e "Updated ${green}AUTOCONNECT=$AUTOCONNECT${nc}" + echo + fi + if [[ -z $PREFERRED_REGION ]]; then + echo -e "${green}AUTOCONNECT=true${nc}" + echo + else + echo + echo "AUTOCONNECT supersedes in-line definitions of PREFERRED_REGION." + echo -e "${red}PREFERRED_REGION=$PREFERRED_REGION will be ignored.${nc} + " + PREFERRED_REGION="" + fi + selectServer="no" + fi + + # Prompt the user to specify a server or auto-connect to the lowest latency + while :; do + if [[ -z $PREFERRED_REGION ]]; then + # If autoconnect is not set, prompt the user to specify a server or auto-connect to the lowest latency + if [[ $selectServer == "ask" ]]; then + echo -n "Do you want to manually select a server, instead of auto-connecting to the + server with the lowest latency ([N]o/[y]es): " + read -r selectServer + echo + fi + + # Call the region script with input to create an ordered list based upon latency + # When $PREFERRED_REGION is set to none, get_region.sh will generate a list of servers + # that meet the latency requirements specified by $MAX_LATENCY. + # When $VPN_PROTOCOL is set to no, get_region.sh will sort that list of servers + # to allow for numeric selection, or an easy manual review of options. + if echo "${selectServer:0:1}" | grep -iq y; then + # This sets the maximum allowed latency in seconds. + # All servers that respond slower than this will be ignored. + if [[ -z $MAX_LATENCY ]]; then + echo -n "With no input, the maximum allowed latency will be set to 0.05s (50ms). + If your connection has high latency, you may need to increase this value. + For example, you can try 0.2 for 200ms allowed latency. + " + else + latencyInput=$MAX_LATENCY + fi + + # Assure that input is numeric and properly formatted. + MAX_LATENCY=0.05 # default + while :; do + if [[ -z $latencyInput ]]; then + read -r -p "Custom latency (no input required for 50ms): " latencyInput + echo + fi + customLatency=0 + customLatency+=$latencyInput + + if [[ -z $latencyInput ]]; then + break + elif [[ $latencyInput == 0 ]]; then + echo -e "${red}Latency input must not be zero.${nc}\n" + elif ! [[ $customLatency =~ $floatCheck ]]; then + echo -e "${red}Latency input must be numeric.${nc}\n" + elif [[ $latencyInput =~ $intCheck ]]; then + MAX_LATENCY=$latencyInput + break + else + MAX_LATENCY=$customLatency + break + fi + latencyInput="" + done + export MAX_LATENCY + echo -e "${green}MAX_LATENCY=$MAX_LATENCY${nc}" + + PREFERRED_REGION="none" + export PREFERRED_REGION + VPN_PROTOCOL="no" + export VPN_PROTOCOL + VPN_PROTOCOL=no ./get_region.sh + + if [[ -s /opt/piavpn-manual/latencyList ]]; then + # Output the ordered list of servers that meet the latency specification $MAX_LATENCY + echo -e "Ordered list of servers with latency less than ${green}$MAX_LATENCY${nc} seconds:" + i=0 + while read -r line; do + i=$((i+1)) + time=$( awk 'NR == '$i' {print $1}' /opt/piavpn-manual/latencyList ) + id=$( awk 'NR == '$i' {print $2}' /opt/piavpn-manual/latencyList ) + ip=$( awk 'NR == '$i' {print $3}' /opt/piavpn-manual/latencyList ) + location1=$( awk 'NR == '$i' {print $4}' /opt/piavpn-manual/latencyList ) + location2=$( awk 'NR == '$i' {print $5}' /opt/piavpn-manual/latencyList ) + location3=$( awk 'NR == '$i' {print $6}' /opt/piavpn-manual/latencyList ) + location4=$( awk 'NR == '$i' {print $7}' /opt/piavpn-manual/latencyList ) + location="$location1 $location2 $location3 $location4" + printf "%3s : %-8s %-15s %23s" $i "$time" "$ip" "$id" + echo " - $location" + done < /opt/piavpn-manual/latencyList + echo + + # Receive input to specify the server to connect to manually + while :; do + read -r -p "Input the number of the server you want to connect to ([1]-[$i]) : " serverSelection + if [[ -z $serverSelection ]]; then + echo -e "\n${red}You must provide input.${nc}\n" + elif ! [[ $serverSelection =~ $intCheck ]]; then + echo -e "\n${red}You must enter a number.${nc}\n" + elif [[ $serverSelection -lt 1 ]]; then + echo -e "\n${red}You must enter a number greater than 1.${nc}\n" + elif [[ $serverSelection -gt $i ]]; then + echo -e "\n${red}You must enter a number between 1 and $i.${nc}\n" + else + PREFERRED_REGION=$( awk 'NR == '"$serverSelection"' {print $2}' /opt/piavpn-manual/latencyList ) + echo + echo -e "${green}PREFERRED_REGION=$PREFERRED_REGION${nc}" + break + fi + done + + # Write the serverID for use when connecting, and display the serverName for user confirmation + export PREFERRED_REGION + echo + break + else + exit 1 + fi + else + echo -e "${green}You will auto-connect to the server with the lowest latency.${nc}" + echo + break + fi + else + # Validate in-line declaration of PREFERRED_REGION; if invalid remove input to initiate prompts + echo "Region input is : $PREFERRED_REGION" + export PREFERRED_REGION + VPN_PROTOCOL=no ./get_region.sh + if [[ $? != 1 ]]; then + break + fi + PREFERRED_REGION="" + fi + done +fi + +if [[ -z $VPN_PROTOCOL ]]; then + VPN_PROTOCOL="none" +fi +# This section asks for user connection preferences +case $VPN_PROTOCOL in + openvpn) + VPN_PROTOCOL="openvpn_udp_standard" + ;; + wireguard | openvpn_udp_standard | openvpn_udp_strong | openvpn_tcp_standard | openvpn_tcp_strong) + ;; + none | *) +# echo -n "Connection method ([W]ireguard/[o]penvpn): " +# read -r connection_method +# echo + connection_method=o + + VPN_PROTOCOL="wireguard" + if echo "${connection_method:0:1}" | grep -iq o; then +# echo -n "Connection method ([U]dp/[t]cp): " +# read -r protocolInput +# echo + protocolInput=u + + protocol="udp" + if echo "${protocolInput:0:1}" | grep -iq t; then + protocol="tcp" + fi + + echo "Higher levels of encryption trade performance for security. " + echo -n "Do you want to use strong encryption ([N]o/[y]es): " +# read -r strongEncryption + strongEncryption=n + echo + + encryption="standard" + if echo "${strongEncryption:0:1}" | grep -iq y; then + encryption="strong" + fi + + VPN_PROTOCOL="openvpn_${protocol}_${encryption}" + fi + ;; +esac +export VPN_PROTOCOL +echo -e "${green}VPN_PROTOCOL=$VPN_PROTOCOL +${nc}" + +# Check for the required presence of resolvconf for setting DNS on wireguard connections +setDNS="yes" +if ! command -v resolvconf &>/dev/null && [[ $VPN_PROTOCOL == "wireguard" ]]; then + echo -e "${red}The resolvconf package could not be found." + echo "This script can not set DNS for you and you will" + echo -e "need to invoke DNS protection some other way.${nc}" + echo + setDNS="no" +fi + +# Check for in-line definition of PIA_DNS and prompt for input +if [[ $setDNS == "yes" ]]; then + if [[ -z $PIA_DNS ]]; then + echo "Using third party DNS could allow DNS monitoring." + echo -n "Do you want to force PIA DNS ([Y]es/[n]o): " +# read -r setDNS + setDNS=n + echo + PIA_DNS="true" + if echo "${setDNS:0:1}" | grep -iq n; then + PIA_DNS="false" + fi + fi +elif [[ $PIA_DNS != "true" || $setDNS == "no" ]]; then + PIA_DNS="false" +fi +export PIA_DNS +echo -e "${green}PIA_DNS=$PIA_DNS${nc}" + +CONNECTION_READY="true" +export CONNECTION_READY + +if [[ -z $DIP_TOKEN ]]; then + ./get_region.sh +elif [[ $VPN_PROTOCOL == wireguard ]]; then + echo + echo -e "You will be connecting with ${green}WG_SERVER_IP=$dipAddress${nc} using" + echo -e "${green}VPN_PROTOCOL=wireguard${nc}, so we will automatically connect to WireGuard," + echo "by running this command:" + echo -e "$ ${green}PIA_PF=$PIA_PF PIA_TOKEN=$PIA_TOKEN" \\ + echo "DIP_TOKEN=$DIP_TOKEN" \\ + echo "WG_SERVER_IP=$dipAddress WG_HOSTNAME=$dipHostname" \\ + echo -e "./connect_to_wireguard_with_token.sh${nc}" + echo + PIA_PF=$PIA_PF PIA_TOKEN=$PIA_TOKEN DIP_TOKEN=$DIP_TOKEN \ + WG_SERVER_IP=$dipAddress WG_HOSTNAME=$dipHostname \ + ./connect_to_wireguard_with_token.sh + rm -f /opt/piavpn-manual/latencyList + exit 0 +elif [[ $VPN_PROTOCOL == openvpn* ]]; then + echo + echo "The dedicated IP connection will be started with" + echo -e "${green}VPN_PROTOCOL=$VPN_PROTOCOL${nc}, so we will automatically" + echo "connect to OpenVPN, by running this command:" + echo -e "$ ${green}PIA_PF=$PIA_PF PIA_TOKEN=$PIA_TOKEN" \\ + echo "DIP_TOKEN=$DIP_TOKEN OVPN_SERVER_IP=$dipAddress" \\ + echo "OVPN_HOSTNAME=$dipHostname" \\ + echo "CONNECTION_SETTINGS=$VPN_PROTOCOL" \\ + echo -e "./connect_to_openvpn_with_token.sh${nc}" + echo + PIA_PF=$PIA_PF PIA_TOKEN=$PIA_TOKEN \ + DIP_TOKEN=$DIP_TOKEN OVPN_SERVER_IP=$dipAddress \ + OVPN_HOSTNAME=$dipHostname \ + CONNECTION_SETTINGS=$VPN_PROTOCOL \ + ./connect_to_openvpn_with_token.sh + rm -f /opt/piavpn-manual/latencyList + exit 0 +fi diff --git a/setupVars.conf b/setupVars.conf new file mode 100644 index 0000000..1db6663 --- /dev/null +++ b/setupVars.conf @@ -0,0 +1,27 @@ +PLAT=Raspbian +OSCN=buster +USING_UFW=0 +pivpnforceipv6route=0 +IPv4dev=eth0 +IPv4addr=192.168.0.25/24 +IPv4gw=10.18.112.1 +install_user=pi +install_home=/home/pi/pia-pivpn +VPN=wireguard +pivpnPORT=35377 +pivpnDNS1=172.16.17.1 +pivpnDNS2= +pivpnHOST=45.133.172.212 +INPUT_CHAIN_EDITED=1 +FORWARD_CHAIN_EDITED=1 +INPUT_CHAIN_EDITEDv6= +FORWARD_CHAIN_EDITEDv6= +pivpnPROTO=udp +pivpnMTU=1420 +pivpnDEV=wg0 +pivpnNET=172.16.17.0 +subnetClass=24 +pivpnenableipv6=0 +ALLOWED_IPS="0.0.0.0/0" +UNATTUPG=1 +INSTALLED_PACKAGES=() diff --git a/setup_vpns.sh b/setup_vpns.sh new file mode 100644 index 0000000..fee1cd8 --- /dev/null +++ b/setup_vpns.sh @@ -0,0 +1,63 @@ +#!/bin/bash + +if (( EUID != 0 )); then + echo "This script needs to be run as sudo" + exit 1 +fi + +TERMINAL_HEIGHT=$(tput lines) +BOX_HEIGHT=$(printf "%.0f" "$(echo "scale=2; $TERMINAL_HEIGHT * .5" | bc)") + +TERMINAL_WIDTH=$(tput cols) +BOX_WIDTH=$(printf "%.0f" "$(echo "scale=2; $TERMINAL_WIDTH * .75" | bc)") +declare piaCredsFile=/etc/openvpn/auth.txt + +if ! (command -v whiptail 2> /dev/null); then + apt install whiptail +fi + +if [[ ! -d /etc/openvpn ]]; then + mkdir /etc/openvpn +fi + +checkPiaCredentials() { + if [[ ! -f $piaCredsFile ]]; then + PIA_PASS=$(whiptail --passwordbox "Please enter your PIA password" "$BOX_HEIGHT" "$BOX_WIDTH" 3>&2 2>&1 1>&3) + cat <<-EOF > /etc/openvpn/auth.txt + p0486245 + $PIA_PASS + EOF + fi +} + +createVpnService() { + declare serviceFile=/lib/systemd/system/vpn.service + if [[ ! -f $serviceFile ]]; then + cat <<-EOF > $serviceFile + [Unit] + Description=Startup for PIA VPN with port forwarding and PiVPN + After=network.target + + [Service] + User=root + Group=root + RemainAfterExit=1 + Type=simple + ExecStart=/home/pi/pia-pivpn/startup_vpn.sh + Restart=on-failure + + [Install] + WantedBy=multi-user.target + EOF + fi +} + +installVpn() { + checkPiaCredentials + createVpnService + systemctl daemon-reload + systemctl enable vpn.service + service vpn start +} + +installVpn diff --git a/startup_vpn.sh b/startup_vpn.sh new file mode 100755 index 0000000..6e310bd --- /dev/null +++ b/startup_vpn.sh @@ -0,0 +1,37 @@ +#!/bin/bash +declare ntfyTopic=atusa_061796_pihole +curl -H "t: Resetting VPN" -H "p:5" -H "ta:warning" -d "PiHole VPN connection expired. Resetting both VPNs. PiVPN reconfigure is required." ntfy.sh/$ntfyTopic + +pkill -f openvpn > /dev/null 2>&1 +pkill -f port_forwarding > /dev/null 2>&1 + +declare portFile=/home/pi/pf_port +[[ -f $portFile ]] && rm $portFile + +./run_setup.sh + +until [[ -f $portFile ]]; do + echo "Waiting for port forwarding to complete..." + sleep 3 +done + +declare port=$(cat $portFile | head -1) +declare gateway=$(ip route | grep tun | awk '/^0.0.0.0*/ { print $3;}') +declare host=$(curl -s api.ipify.org) +declare expiration=$(cat $portFile | tail -1) +declare expirationDayEpoch=$(echo $expiration | xargs -i date -d {} +%m/%d/%Y | xargs -i date -d {} +%s) +declare setupVarsFile=/home/pi/pia-pivpn/setupVars.conf + +curl -H "t: Port Forwarding Expires Today" -H "p:5" -H "ta:warning" -H "Delay: $expirationDayEpoch" -d "PiHole port forwarding expires at $expiration. The connection will be reset at this time and you'll need to ssh into your pihole to connect devices to PiVPN again." ntfy.sh/$ntfyTopic + +sed -i "/pivpnPORT=/c\pivpnPORT=$port" $setupVarsFile +sed -i "/IPv4gw=/c\IPv4gw=$gateway" $setupVarsFile +sed -i "/pivpnHOST=/c\pivpnHOST=$host" $setupVarsFile + +declare -a users=( $(pivpn -l | grep -v ':::\|Client' | awk '{print $1;}') ) + +/home/pi/pivpn/pivpn_install.sh --reconfigure --unattended /home/pi/pia-pivpn/setupVars.conf + +for user in ${users[@]}; do + pivpn -a -n $user +done