gunix 2 years ago
commit fc2dd73a1c
  1. 92
  2. 43
  3. 137
  4. 195

@ -0,0 +1,92 @@
# Manual PIA VPN Connections
__This project is "Work in Progress"__
This repository contains documentation on how to create native WireGuard connections to our NextGen network, and also on how to enable Port Forwarding in case you require this feature. Documentation on OpenVPN will follow soon enough.
You will find a lot of information bellow. However if you prefer a hands-on approach, here is the __TL/DR__:
* clone this repo: `git clone https://github.com/pia-foss/manual-connections.git`
* use `get_region_and_token.sh` to get the best region and a token
* use `wireguard_and_pf.sh` to create a WireGuard connection with/without PF
### Dependencies
In order for the scripts to work (probably even if you do a manual setup), you will need the following packages:
* `curl`
* `jq`
* `wireguard-tools` (which give you the `wg-quick` utility)
## 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 aplications. 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 of VPN and/or PF
In order to help you use VPN services and PF on any device, we have prepare 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.
Here is a list of scripts you could find useful:
* [region and token script](get_region_and_token.sh): This script helps you to get the best region and also to get a token for VPN authentication. The script will extend it's functionality if you add extra environment variables. Adding your PIA credentials will allow the script to also get a VPN token. The script can also trigger the WireGuard script to create a connection, if you specify `WG_AUTOCONNECT=true`.
* [wireguard and pf script](wireguard_and_pf.sh): This script allow you to connect to the VPN server via WireGuard. You can specify `PIA_PF=true` if you also wish to get Port Forwarding for your connection.
* openvpn script: allows you to connect and to bind a port // TODO: Add Link
## Manual setup of PF
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 proove you actually have an active account.
bash-5.0# curl -k "$TOKEN"
"status": "OK",
"payload": "eyJ0b2tlbiI6Inh4eHh4eHh4eCIsInBvcnQiOjQ3MDQ3LCJjcmVhdGVkX2F0IjoiMjAyMC0wNC0zMFQyMjozMzo0NC4xMTQzNjk5MDZaIn0=",
"signature": "a40Tf4OrVECzEpi5kkr1x5vR0DEimjCYJU9QwREDpLM+cdaJMBUcwFoemSuJlxjksncsrvIgRdZc0te4BUL6BA=="
The payload can be decoded with base64 to see your information:
$ 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-5.0# curl -sGk --data-urlencode "payload=${payload}" --data-urlencode "signature=${signature}"
"status": "OK",
"message": "port scheduled for add"
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 on the IP of the traffic server and the port specified in the payload which in our case is `47047`:
$ curl ""
and 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 > 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 > Flags [R.], seq 0, ack 906854497, win 0, length 0

@ -0,0 +1,43 @@

@ -0,0 +1,137 @@
# Set this to the maximum allowed latency in seconds.
# All servers that repond slower than this will be ignore.
# The value is currently set to 50 milliseconds.
export maximum_allowed_latency
# 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() {
time=$(curl -o /dev/null -s \
--connect-timeout $maximum_allowed_latency \
--write-out "%{time_connect}" \
if [ $? -eq 0 ]; then
>&2 echo The region \"$regionName\" responded in $time seconds
echo $time $regionID $serverIP
export -f printServerLatency
echo -n "Getting the server list... "
# Get all region data since we will need this on multiple ocasions
all_region_data=$(curl -s "$serverlist_url" | head -1)
# If the server list has less than 1000 characters, it means curl failed.
if [[ ${#all_region_data} < 1000 ]]; then
echo "Could not get correct region data. To debug this, run:"
echo "$ curl -v $serverlist_url"
echo "If it works, you will get a huge JSON as a response."
exit 1
# Notify the user that we got the server list.
echo "OK!"
# Test one server from each region to get the closest region:
echo Testing servers that respond \
faster than $maximum_allowed_latency seconds:
region_latency_report="$( echo $all_region_data |
jq -r '.regions[] | .servers.meta[0].ip + " " + .id + " " + .name' )"
# Get the best region
bestRegion="$(echo "$region_latency_report" |
xargs -i bash -c 'printServerLatency {}' |
sort | head -1 | awk '{ print $2 }')"
# Get all data for the best region
regionData="$( echo $all_region_data |
jq --arg REGION_ID "$bestRegion" -r \
'.regions[] | select(.id==$REGION_ID)')"
echo The closest region is "$(echo $regionData | jq -r '.name')".
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')"
echo "The script found the best servers from the region closes to you.
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 bellow the list of best IPs and matching
hostnames for each protocol:
Meta Services: $bestServer_meta_IP // $bestServer_meta_hostname
WireGuard: $bestServer_WG_IP // $bestServer_WG_hostname
OpenVPN TCP: $bestServer_OT_IP // $bestServer_OT_hostname
OpenVPN UDP: $bestServer_OU_IP // $bestServer_OU_hostname
if [[ ! $PIA_USER || ! $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_region_and_token.sh
exit 1
echo "The ./get_region_and_token.sh script got started with PIA_USER and PIA_PASS,
so we will also use a meta service to get a new VPN token."
echo "Trying to get a new token by authenticating with the meta service..."
generateTokenResponse=$(curl -s -u "$PIA_USER:$PIA_PASS" \
--connect-to "$bestServer_meta_hostname::$bestServer_meta_IP:" \
--cacert "ca.rsa.4096.crt" \
echo "$generateTokenResponse"
if [ "$(echo "$generateTokenResponse" | jq -r '.status')" != "OK" ]; then
echo "Could not get a token. Please check your account credentials."
echo "You can also try debugging by manually running the curl command:"
echo $ curl -vs -u "$PIA_USER:$PIA_PASS" --cacert ca.rsa.4096.crt \
--connect-to "$bestServer_meta_hostname::$bestServer_meta_IP:" \
exit 1
token="$(echo "$generateTokenResponse" | jq -r '.token')"
echo "This token will expire in 24 hours.
if [ "$WG_AUTOCONNECT" != true ]; then
echo If you wish to automatically connect to WireGuard after detecting the best
echo region, please run the script with the env var WG_AUTOCONNECT=true. You can
echo also specify the env var PIA_PF=true to get port forwarding. Example:
echo $ PIA_USER=p0123456 PIA_PASS=xxx \
WG_AUTOCONNECT=true PIA_PF=true ./sort_regions_by_latency.sh
echo You can connect by running:
echo WG_TOKEN=\"$token\" WG_SERVER_IP=$bestServer_WG_IP \
WG_HOSTNAME=$bestServer_WG_hostname ./wireguard_and_pf.sh
if [ "$PIA_PF" != true ]; then
echo "The ./get_region_and_token.sh script got started with WG_AUTOCONNECT=true,
so we will automatically connect to WireGuard, by running this command:
$ WG_TOKEN=\"$token\" \\
WG_SERVER_IP=$bestServer_WG_IP WG_HOSTNAME=$bestServer_WG_hostname \\
PIA_PF=$PIA_PF ./wireguard_port_forwarding.sh
PIA_PF=$PIA_PF WG_TOKEN="$token" WG_SERVER_IP=$bestServer_WG_IP \
WG_HOSTNAME=$bestServer_WG_hostname ./wireguard_and_pf.sh

@ -0,0 +1,195 @@
# PIA currently does not support IPv6. In order to be sure your VPN
# connection does not leak, it is best to disabled IPv6 altogether.
echo 'You should consider disabling IPv6 by running:
sysctl -w net.ipv6.conf.all.disable_ipv6=1
sysctl -w net.ipv6.conf.default.disable_ipv6=1
# check if the wireguard tools have been installed
if ! command -v wg-quick &> /dev/null
echo "wg-quick could not be found."
echo "Please install wireguard-tools"
exit 1
# Check if the mandatory environment variables are set.
if [[ ! $WG_SERVER_IP || ! $WG_HOSTNAME || ! $WG_TOKEN ]]; then
echo 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 WG_TOKEN - your authentication token
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 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
# 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...
wireguard_json="$(curl -s -G \
--connect-to "$WG_HOSTNAME::$WG_SERVER_IP:" \
--cacert "ca.rsa.4096.crt" \
--data-urlencode "pt=${WG_TOKEN}" \
--data-urlencode "pubkey=$pubKey" \
"https://${WG_HOSTNAME}:1337/addKey" )"
export wireguard_json
echo "$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 "Server did not return OK. Stopping now."
exit 1
# Create the WireGuard config based on the JSON received from the API
# The config does not contain a DNS entry, since some servers do not
# have resolvconf, which will result in the script failing.
# We will enforce the DNS after the connection gets established.
echo -n "Trying to write /etc/wireguard/pia.conf... "
echo "
Address = $(echo "$wireguard_json" | jq -r '.peer_ip')
PrivateKey = $privKey
## If you want wg-quick to also set up your DNS, uncomment the line below.
# DNS = $(echo "$json" | jq -r '.dns_servers[0]')
PublicKey = $(echo "$wireguard_json" | jq -r '.server_key')
AllowedIPs =
Endpoint = ${WG_SERVER_IP}:$(echo "$wireguard_json" | jq -r '.server_port')
" > /etc/wireguard/pia.conf || exit 1
echo OK!
# Start the WireGuard interface.
# If something failed, stop this script.
# If you get DNS errors because you miss some packages,
# just can hardcode /etc/resolv.conf to "nameserver".
echo Trying to create the wireguard interface...
wg-quick up pia || exit 1
echo The WireGuard interface got created.
echo At this point, internet should work via VPN.
# 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, please start the script
echo with the env var PIA_PF=true. Example:
echo $ WG_SERVER= WG_HOSTNAME=piaserver401 \
WG_TOKEN=\"\$token\" PIA_PF=true \
echo "
This script got started with PIA_PF=true.
Starting procedure to enable port forwarding."
# 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.
# Since this is the script for wireguard, you can just get the gateway
# from the JSON response you got at the previous step from the Wireguard API.
gateway="$(echo "$wireguard_json" | jq -r '.server_vip')"
# 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 [[ ! $PAYLOAD_AND_SIGNATURE ]]; then
echo "Getting new signature..."
payload_and_signature="$(curl -s -m 5 \
--connect-to "$WG_HOSTNAME::$gateway:" \
--cacert "ca.rsa.4096.crt" \
-G --data-urlencode "token=${WG_TOKEN}" \
echo "Using the following payload_and_signature from the env var:"
echo "$payload_and_signature"
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 "The payload_and_signature variable does not contain an OK status."
exit 1
# 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 reponse 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')"
# Display some information on the screen for the user.
echo "The signature is OK.
--> The port is $port and it will expire on $expires_at. <--
Trying to bind the port..."
# 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 "$WG_HOSTNAME::$gateway:" \
--cacert "ca.rsa.4096.crt" \
--data-urlencode "payload=${payload}" \
--data-urlencode "signature=${signature}" \
echo "$bind_port_response"
# 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 "The API did not return OK when trying to bind port. Exiting."
exit 1
echo Port $port refreshed on $(date). \
This port will expire on $(date --date="$expires_at")
# sleep 15 minutes
sleep 900