├── .github
└── workflows
│ ├── docker-build-push.yml
│ ├── dockerhub-description.yml
│ └── shellcheck.yml
├── Dockerfile
├── README.md
├── RegionsListPubKey.pem
├── docker-compose.yml
├── extra
├── README.md
├── pf.sh
├── pia-auth.sh
└── wg-gen.sh
├── healthcheck.sh
├── pf_success.sh
└── run
/.github/workflows/docker-build-push.yml:
--------------------------------------------------------------------------------
1 | name: Docker Build & Push
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'master'
7 | - 'testing'
8 | paths-ignore:
9 | - 'README.md'
10 | - 'docker-compose.yml'
11 | # Rebuild monthly to keep packages up to date
12 | schedule:
13 | - cron: '0 0 1 * *'
14 |
15 | jobs:
16 | docker:
17 | runs-on: ubuntu-latest
18 | permissions:
19 | contents: read
20 | packages: write
21 | steps:
22 | -
23 | uses: actions/checkout@v4
24 | -
25 | name: Set up QEMU
26 | uses: docker/setup-qemu-action@v3
27 | -
28 | name: Set up Docker Buildx
29 | uses: docker/setup-buildx-action@v3
30 | -
31 | name: Login to DockerHub
32 | uses: docker/login-action@v3
33 | if: github.repository_owner == 'thrnz'
34 | with:
35 | username: ${{ secrets.DOCKERHUB_USERNAME }}
36 | password: ${{ secrets.DOCKERHUB_TOKEN }}
37 | -
38 | name: Log in to the Container registry
39 | uses: docker/login-action@v3
40 | if: github.repository_owner == 'thrnz'
41 | with:
42 | registry: ghcr.io
43 | username: ${{ github.repository_owner }}
44 | password: ${{ secrets.GITHUB_TOKEN }}
45 | -
46 | name: Docker Metadata
47 | id: docker-meta
48 | uses: docker/metadata-action@v5
49 | with:
50 | images: |
51 | thrnz/docker-wireguard-pia
52 | ghcr.io/${{ github.repository }}
53 | tags: |
54 | # Tag branches with branch name, but disable for default branch
55 | type=ref,event=branch,enable=${{ github.ref_name != 'master' }}
56 | # set latest tag for default branch
57 | type=raw,value=latest,enable=${{ github.ref_name == 'master' }}
58 | type=raw,value={{date 'YYYYMMDD'}}_{{branch}}_{{sha}}
59 | - name: Note build time
60 | run: |
61 | echo "BUILD_DATE=$(date)" >> ${GITHUB_ENV}
62 | echo "SHA_TRIMMED=${GITHUB_SHA::7}" >> ${GITHUB_ENV}
63 | -
64 | name: Build and push
65 | id: docker_build
66 | uses: docker/build-push-action@v5
67 | with:
68 | platforms: linux/amd64,linux/arm,linux/arm64
69 | push: ${{ github.repository_owner == 'thrnz' }}
70 | tags: ${{ steps.docker-meta.outputs.tags }}
71 | build-args: |
72 | BUILDINFO="${{ github.ref_name }} ${{ env.SHA_TRIMMED }} ${{ env.BUILD_DATE }}"
73 | labels: ${{ steps.docker-meta.outputs.labels }}
74 | -
75 | name: Image digest
76 | run: echo ${{ steps.docker_build.outputs.digest }}
77 |
--------------------------------------------------------------------------------
/.github/workflows/dockerhub-description.yml:
--------------------------------------------------------------------------------
1 | name: Update DockerHub description
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | paths:
8 | - README.md
9 | - .github/workflows/dockerhub-description.yml
10 |
11 | jobs:
12 | dockerHubDescription:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v4
16 | - name: Docker Hub Description
17 | uses: peter-evans/dockerhub-description@v3
18 | with:
19 | username: ${{ secrets.DOCKERHUB_USERNAME }}
20 | password: ${{ secrets.DOCKERHUB_TOKEN }}
21 | repository: thrnz/docker-wireguard-pia
22 |
--------------------------------------------------------------------------------
/.github/workflows/shellcheck.yml:
--------------------------------------------------------------------------------
1 | name: "ShellCheck"
2 | on: [push, pull_request]
3 |
4 | jobs:
5 | shellcheck:
6 | name: ShellCheck
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v4
10 | - name: Run ShellCheck
11 | run: shellcheck run ./*.sh ./extra/*.sh
12 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:latest
2 |
3 | RUN apk add --no-cache \
4 | bash \
5 | ca-certificates \
6 | curl \
7 | grepcidr3 \
8 | iptables \
9 | iptables-legacy \
10 | libcap-utils \
11 | jq \
12 | openssl \
13 | wireguard-tools
14 |
15 | # Modify wg-quick so it doesn't die without --privileged
16 | # Set net.ipv4.conf.all.src_valid_mark=1 on container creation using --sysctl if required instead
17 | # To avoid confusion, also suppress the error message that displays even when pre-set to 1 on container creation
18 | RUN sed -i 's/cmd sysctl.*/set +e \&\& sysctl -q net.ipv4.conf.all.src_valid_mark=1 \&> \/dev\/null \&\& set -e/' /usr/bin/wg-quick
19 |
20 | # Install wireguard-go as a fallback if wireguard is not supported by the host OS or Linux kernel
21 | RUN apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community wireguard-go
22 |
23 | # Get the PIA CA cert
24 | ADD https://raw.githubusercontent.com/pia-foss/desktop/master/daemon/res/ca/rsa_4096.crt /rsa_4096.crt
25 |
26 | # The PIA desktop app uses this public key to verify server list downloads
27 | # https://github.com/pia-foss/desktop/blob/master/daemon/src/environment.cpp#L30
28 | COPY ./RegionsListPubKey.pem /RegionsListPubKey.pem
29 |
30 | # Add main work dir to PATH
31 | WORKDIR /scripts
32 |
33 | # Copy scripts to containers
34 | COPY run healthcheck.sh pf_success.sh ./extra/pf.sh ./extra/pia-auth.sh ./extra/wg-gen.sh /scripts/
35 | RUN chmod 755 /scripts/*
36 |
37 | # Store persistent PIA stuff here (auth token, server list)
38 | VOLUME /pia
39 |
40 | # Store stuff that might be shared with another container here (eg forwarded port)
41 | VOLUME /pia-shared
42 |
43 | HEALTHCHECK --interval=1m --timeout=3s --start-period=30s --start-interval=1s --retries=3 \
44 | CMD /scripts/healthcheck.sh || exit 1
45 |
46 | ARG BUILDINFO=manual
47 | ENV BUILDINFO=${BUILDINFO}
48 |
49 | CMD ["/scripts/run"]
50 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # docker-wireguard-pia
2 |
3 | A Docker container for using WireGuard with PIA.
4 |
5 | ## Requirements
6 | * Ideally the host should already support WireGuard. Pre 5.6 kernels may need to have the module manually installed. `wg-quick` should automatically fall back to a userspace implementation (`wireguard-go`) if needed, however the container may need access to the `/dev/net/tun` device for this to work.
7 | * The container requires the `NET_ADMIN` [capability](https://docs.docker.com/compose/compose-file/05-services/#cap_add). `SYS_MODULE` may also be needed in some cases, especially when WireGuard support is provided via kernel module.
8 | * An active [PIA](https://www.privateinternetaccess.com) subscription.
9 |
10 | ## Examples
11 | An example [docker-compose.yml](https://github.com/thrnz/docker-wireguard-pia/blob/master/docker-compose.yml) is available. Some more working examples can be found [here](https://github.com/thrnz/docker-wireguard-pia/wiki/Examples).
12 |
13 | ## Config
14 | The following ENV vars are required:
15 |
16 | | ENV Var | Function |
17 | |-------|------|
18 | |```LOC=swiss```|Location id to connect to. Available server location ids are listed [here](https://serverlist.piaservers.net/vpninfo/servers/v6). Example values include ```us_california```, ```ca_ontario```, and ```swiss```. If left empty the container will print out all currently available location ids and exit.
Multiple ids can be listed, separated by either a space or a comma, and are used as fallback if the initial endpoint registration fails.
19 | |```USER=xxxxxxxx```|PIA username
20 | |```PASS=xxxxxxxx```|PIA password
21 |
22 | The rest are optional:
23 |
24 | | ENV Var | Function |
25 | |-------|------|
26 | |```LOCAL_NETWORK=192.168.1.0/24```|Whether to route and allow input/output traffic to the LAN. LAN access will be unavailable if not specified. Multiple ranges can be specified, separated by a comma or space. Note that there may be DNS issues if this overlaps with PIA's default DNS servers (`10.0.0.243` and `10.0.0.242` as of July 2022). Custom DNS servers can be defined using `VPNDNS` (see below) if this is an issue.
27 | |```KEEPALIVE=25```|If defined, PersistentKeepalive will be set to this in the WireGuard config. This can be used to ensure incoming packets on an idle link aren't lost when behind NAT. The [WireGuard QuickStart guide](https://www.wireguard.com/quickstart/) suggests a value of 25 if needed. By default this remains unset.
28 | |```MTU=1420```|This can be used to override ```wg-quick```'s automatic MTU setting on the Wireguard interface if needed. By default this remains unset (ie. let ```wg-quick``` choose).
29 | |```VPNDNS=8.8.8.8, 8.8.4.4```|Use these DNS servers in the WireGuard config. PIA's DNS servers will be used if not specified. Set to `0` to disable making any changes to the default container DNS settings. When set to `0`, and Docker's embedded DNS server is used, there may be the potential for DNS leaks depending on how Docker internally routes the queries.
30 | |```PORT_FORWARDING=0/1```|Whether to enable port forwarding. Requires a supported server. Defaults to 0 if not specified.
31 | |```PORT_FILE=/pia-shared/port.dat```|The forwarded port number is dumped here for possible access by scripts in other containers. By default this is ```/pia-shared/port.dat```.
32 | |```PORT_FILE_CLEANUP=0/1```|Remove the file containing the forwarded port number on exit. Defaults to 0 if not specified.
33 | |```PORT_PERSIST=0/1```|Set to 1 to attempt to keep the same port forwarded when the container is restarted. The port number may persist for up to two months. Defaults to 0 (always acquire a new port number) if not specified.
34 | |`PORT_FATAL=0/1`|Whether to consider port forwarding errors as fatal or not. May be useful when combined with `EXIT_ON_FATAL` if needed. Defaults to 0 if not specified.
35 | |```PORT_SCRIPT=/path/to/script.sh```|A mounted custom script can be run inside the container once a port is successfully forwarded if needed. The forwarded port number is passed as the first command line argument. By default this remains unset. See [issue #26](https://github.com/thrnz/docker-wireguard-pia/issues/26) for more info.
36 | |```FIREWALL=0/1```|Whether to block non-WireGuard traffic. Defaults to 1 if not specified.
37 | |```EXIT_ON_FATAL=0/1```|By default the container will continue running until manually stopped. Set this to 1 to force the container to exit when an error occurs. Exiting on an error may not be desirable behaviour if other containers are sharing the connection.
38 | |```FATAL_SCRIPT=/path/to/script.sh```|A mounted custom script can be run inside the container if a fatal error occurs. By default this remains unset.
39 | |```USER_FILE=/run/secrets/pia-username``` ```PASS_FILE=/run/secrets/pia-password```|PIA credentials can also be read in from existing files (eg for use with Docker secrets)
40 | |```PIA_IP=x.x.x.x``` ```PIA_CN=hostname401``` ```PIA_PORT=1337```|Connect to a specific server by manually setting all three of these. This will override whatever ```LOC``` is set to.
41 | |```FWD_IFACE``` ```PF_DEST_IP```|If needed, the container can be used as a gateway for other containers or devices by setting these. See [issue #20](https://github.com/thrnz/docker-wireguard-pia/issues/20) for more info. Note that these are for a specific use case, and in many cases using Docker's ```--net=container:xyz``` or docker-compose's ```network_mode: service:xyz``` instead, and leaving these vars unset, would be an easier way of accessing the VPN and forwarded port from other containers.
42 | |`PRE_UP` `POST_UP` `PRE_DOWN` `POST_DOWN` `PRE_RECONNECT` `POST_RECONNECT`|Custom commands and/or scripts can be run at certain stages if needed. See [below](#scripting) for more info.
43 | |`PIA_DIP_TOKEN`|A dedicated ip token can be used by setting this. When set, `LOC` is not used.
44 | |`META_IP=x.x.x.x` `META_CN=hostname401` `META_PORT=443`|On startup, the container needs untunnelled access to PIA's API in order to download the server list and to generate a persistent auth token if needed. Optionally, PIA's 'meta' servers (found in PIA's [server list](https://serverlist.piaservers.net/vpninfo/servers/v6)) can be used instead of the default API endpoints by setting `META_IP` and `META_CN`. These can be set to a different location than `LOC`. `META_PORT` is optional and defaults to 443, although 8080 also appears to be available. See [issue #109](https://github.com/thrnz/docker-wireguard-pia/issues/109) for more info.
45 | |`ACTIVE_HEALTHCHECKS=0/1`|The container contains a very basic Docker [healthcheck](https://docs.docker.com/reference/dockerfile/#healthcheck) script that can be used to ensure the VPN is up before starting other services. By default only passive checks that don't generate any traffic are run. Set this to 1 to also allow checks that generate traffic, such as `ping`, in order to detect if the remote endpoint is responding.
46 | |`HEALTHCHECK_PING_TARGET`|When active healthchecks are enabled or reconnect logic is used, this can be used to override the target/s that gets pinged when testing that the endpoint is still responding. Defaults to `www.privateinternetaccess.com`. Can be set to space or comma separated list of multiple targets, in which case all need to fail for the endpoint to be considered unresponsive.
47 | |`HEALTHCHECK_PING_TIMEOUT`|Can be used to override the number of seconds to wait for a reply when pinging a target. Defaults to 3.
48 | `NFTABLES=0/1`|Alpine uses the `nf_tables` iptables backend by default. The container should automatically fall back to the legacy backend if needed. Set this to `0` to force the use of the legacy backend, or to `1` to force the use of the `nf_tables` backend if desired.
49 | `RECONNECT=0/1`|The container can optionally attempt to detect and recover from an unresponsive endpoint. This is done without the WireGuard interface being brought down. `HEALTHCHECK_PING_TARGET` can be used to set the target used to detect if the remote endpoint is responding. Defaults to 0 if not specified. See [below](#networking) for more info.
50 | `MONITOR_INTERVAL=60` `MONITOR_RETRIES=3`|These are used by the `RECONNECT` logic, and can be used to tweak the probe frequency and the number of retries made before considering an endpoint unresponsive.
51 |
52 | ## Scripting
53 | Custom commands and/or scripts can be run at certain stages of the container's life-cycle by setting the `PRE_UP`, `POST_UP`, `PRE_DOWN`, `POST_DOWN`, `PRE_RECONNECT`, and `POST_RECONNECT` env vars. `PRE_UP` is run prior to generating the WireGuard config, `POST_UP` is run after the WireGuard interface is brought up, `PRE_DOWN` and `POST_DOWN` are run before and after the interface is brought down again when the container exits, and `PRE_RECONNECT` and `POST_RECONNECT` are run before and after an attempt is made to reconnect after an unresponsive endpoint is detected (assuming `RECONNECT=1` is set).
54 |
55 | In addition, scripts mounted in `/pia/scripts` named `pre-up.sh`, `post-up.sh`, `pre-down.sh` and `post-down.sh` will be run at the appropriate stage if present. See [issue #33](https://github.com/thrnz/docker-wireguard-pia/issues/33) for more info.
56 |
57 | ## Networking
58 | To keep things simple, network setup is mostly handled by `wg-quick`. All traffic is routed down the WireGuard tunnel, with exceptions added for any ranges manually defined by `LOCAL_NETWORK`. Note that `LOCAL_NETWORK` must be set correctly if LAN access is needed. More information on `wg-quick`'s routing is available [here](https://www.wireguard.com/netns/) under the 'Improved Rule-based Routing' heading.
59 |
60 | Firewall rules are added dropping all traffic by default, and only encrypted/tunneled traffic, attached Docker network traffic, and `LOCAL_NETWORK` traffic is explicitly allowed. This can be disabled by setting the `FIREWALL=0` env var if desired.
61 |
62 | Other containers can access the VPN connection using Docker's [`--net=container:xyz`](https://docs.docker.com/engine/network/#container-networks) or docker-compose's [`network_mode: service:xyz`](https://docs.docker.com/reference/compose-file/services/#network_mode). Note that network related settings for other containers (such as exposing ports) need to be set on the VPN container itself. When the VPN container is brought down or recreated, Docker appears to bring down the shared network with it, so other containers may also require recreating to regain network access.
63 |
64 | The container doesn't support IPv6. Any IPv6 traffic is dropped unless using `FIREWALL=0`, though it might be worth disabling IPv6 on container creation anyway.
65 |
66 | WireGuard keys seem to expire at PIA's end after several hours of inactivity. Setting the `KEEPALIVE` env var may be enough to prevent this from happening if needed.
67 |
68 | The container has optional recovery logic if the remote endpoint permanently stops responding. In order to register new WireGuard keys, temporary routes and firewall rules are added to allow some requests to bypass the tunnel. To prevent potential leaks while this happens, these requests are limited to a certain interface, ports, and root UID. If successful, new WireGuard settings are then applied without needing to bring the existing interface down. Apart from a change in address (and possibly forwarded port number if used) the process should be mostly transparent to other containers sharing the connection. The recovery logic is disabled by default, and is enabled by setting the `RECONNECT=1` env var.
69 |
70 | ## Notes
71 | * WireGuard config generation and port forwarding was based on what was found in the source code to the PIA desktop app. The standalone [Bash scripts](https://github.com/thrnz/docker-wireguard-pia/tree/master/extra) used by the container are available for use outside of Docker.
72 | * As of Sep 2020, PIA have released their own [scripts](https://github.com/pia-foss/manual-connections) for using WireGuard and port forwarding outside of their app.
73 | * Persistent data is stored in ```/pia```.
74 | * If strict reverse path filtering is used, then the `net.ipv4.conf.all.src_valid_mark=1` sysctl should be set on container creation to prevent incoming packets being dropped. See [issue #96](https://github.com/thrnz/docker-wireguard-pia/issues/96) for more info.
75 | * The userspace implementation through wireguard-go is very stable but lacks in performance. Looking into supporting ([boringtun](https://github.com/cloudflare/boringtun)) might be beneficial.
76 | * Container images are available on both Docker Hub (`thrnz/docker-wireguard-pia`) and GitHub's Container Registry (`ghcr.io/thrnz/docker-wireguard-pia`). Images are rebuilt monthly to keep Alpine packages up to date.
77 |
78 | ## Credits
79 | Some bits and pieces and ideas have been borrowed from the following:
80 | * https://github.com/activeeos/wireguard-docker
81 | * https://github.com/cmulk/wireguard-docker
82 | * https://github.com/dperson/openvpn-client
83 | * https://github.com/pia-foss/desktop
84 | * https://gist.github.com/triffid/da48f3c99f1ff334571ae49be80d591b
85 |
--------------------------------------------------------------------------------
/RegionsListPubKey.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN PUBLIC KEY-----
2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzLYHwX5Ug/oUObZ5eH5P
3 | rEwmfj4E/YEfSKLgFSsyRGGsVmmjiXBmSbX2s3xbj/ofuvYtkMkP/VPFHy9E/8ox
4 | Y+cRjPzydxz46LPY7jpEw1NHZjOyTeUero5e1nkLhiQqO/cMVYmUnuVcuFfZyZvc
5 | 8Apx5fBrIp2oWpF/G9tpUZfUUJaaHiXDtuYP8o8VhYtyjuUu3h7rkQFoMxvuoOFH
6 | 6nkc0VQmBsHvCfq4T9v8gyiBtQRy543leapTBMT34mxVIQ4ReGLPVit/6sNLoGLb
7 | gSnGe9Bk/a5V/5vlqeemWF0hgoRtUxMtU1hFbe7e8tSq1j+mu0SHMyKHiHd+OsmU
8 | IQIDAQAB
9 | -----END PUBLIC KEY-----
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | vpn:
3 | image: thrnz/docker-wireguard-pia
4 | # Alternatively you may use ghcr.io
5 | #image: ghcr.io/thrnz/docker-wireguard-pia
6 | container_name: docker-wireguard-pia
7 | volumes:
8 | #Auth token is stored here
9 | - pia:/pia
10 | # If enabled, the forwarded port is dumped to /pia-shared/port.dat for potential use in other containers
11 | - pia-shared:/pia-shared
12 | cap_add:
13 | - NET_ADMIN
14 | # SYS_MODULE might not be needed with a 5.6+ kernel?
15 | #- SYS_MODULE
16 | # If the kernel module isn't available, mounting the tun device may be necessary for userspace implementations
17 | #devices:
18 | #- /dev/net/tun:/dev/net/tun
19 | environment:
20 | # The following env vars are required:
21 | - LOC=swiss
22 | - USER=xxxxxxxxxxxxxxxx
23 | - PASS=xxxxxxxxxxxxxxxx
24 | #The rest are optional:
25 | #- LOCAL_NETWORK=192.168.1.0/24
26 | #- RECONNECT=1
27 | #- KEEPALIVE=25
28 | #- VPNDNS=8.8.8.8,8.8.4.4
29 | #- PORT_FORWARDING=1
30 | sysctls:
31 | # The wg-quick script tries to set this when setting up routing, however this requires running the container
32 | # with the --privileged flag set. Setting it here instead if needed means the container can be run with lower
33 | # privileges. This only needs setting if strict reverse path filtering (rp_filter=1) is used.
34 | - net.ipv4.conf.all.src_valid_mark=1
35 | # May as well disable ipv6. Should be blocked anyway.
36 | - net.ipv6.conf.default.disable_ipv6=1
37 | - net.ipv6.conf.all.disable_ipv6=1
38 | - net.ipv6.conf.lo.disable_ipv6=1
39 |
40 | # Example of another service sharing the VPN
41 | # If this service needs LAN access then LOCAL_NETWORK must be set appropriately on the vpn container
42 | # Forwarded ports should also be set on the vpn container if needed rather than this one in
43 | # order to access from the LAN
44 | # It may be preferable to use a reverse proxy connected via the docker bridge network instead
45 | # to keep the vpn isolated from the LAN
46 | #other-service:
47 | #image: some-other-image
48 | # Other services can share the VPN using 'network_mode: "service:[service name]"'
49 | # See https://docs.docker.com/engine/network/#container-networks and
50 | # https://docs.docker.com/reference/compose-file/services/#network_mode
51 | #network_mode: "service:vpn"
52 | # The following can be used to ensure the vpn is up and functional before the dependant service is started
53 | #depends_on:
54 | #vpn:
55 | #condition: service_healthy
56 |
57 |
58 | # Other containers can access the forwarded port number via /pia-shared/port.dat
59 | # Here's an example of a bare-bones 'helper' container that passes the forwarded port to Deluge
60 | # See https://gist.github.com/thrnz/dcbaa0af66c70af8e302a1c7eb75484a
61 | #deluge-port-helper:
62 | #build: /path/to/deluge-port-helper
63 | #volumes:
64 | #- pia-shared:/pia-shared:ro
65 | #- /path/to/deluge/conf:/deluge/conf
66 | #network_mode: "service:vpn"
67 | #depends_on:
68 | #- vpn
69 | #- other-service
70 |
71 | volumes:
72 | pia:
73 | pia-shared:
74 |
--------------------------------------------------------------------------------
/extra/README.md:
--------------------------------------------------------------------------------
1 | # Standalone Stuff
2 |
3 | Here's some standalone stuff for use outside of Docker.
4 |
--------------------------------------------------------------------------------
/extra/pf.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Bash script for port-forwarding on the PIA 'next-gen' network.
4 | #
5 | # Requires jq and curl.
6 | #
7 | # Options:
8 | # -t Path to a valid PIA auth token
9 | # -i (Optional) IP to send port-forward API requests to.
10 | # An 'educated guess' is made if not specified.
11 | # -n (Optional) Common name of the VPN server (eg. "london411")
12 | # An 'educated guess' is made if not specified.
13 | # -p (Optional) Dump forwarded port here for access by other scripts
14 | # -f (Optional) Network interface to use for requests
15 | # -s (Optional) Run a script on success.
16 | # The forwarded port is passed as an argument.
17 | # -r (Optional) In order to re-use the same forwarded port number between
18 | # sessions, the port forwarding token can be stored here. Tokens can last
19 | # up to 2 months after which a new token will be retrieved and the forwarded
20 | # port will change
21 | #
22 | # Examples:
23 | # pf.sh -t ~/.pia-token
24 | # pf.sh -t ~/.pia-token -n sydney402
25 | # pf.sh -t ~/.pia-token -i 10.13.14.1 -n london416 -p /port.dat -f wg0
26 | #
27 | # For port forwarding on the next-gen network, we need a valid PIA auth token (see pia-auth.sh) and to know the address to send API requests to.
28 | #
29 | # With Wireguard, the PIA app uses the 'server_vip' address found in the 'addKey' response (eg 10.x.x.1), although 'server_ip' also appears to work.
30 | # With OpenVPN, the PIA app uses the gateway IP (also 10.x.x.1)
31 | #
32 | # Optionally, if we know the common name of the server we're connected to we can verify our HTTPS requests.
33 | #
34 | # Port forwarding appears to involve two api calls. The first (getSignature) is done once on startup and again when the returned pf 'token' will soon expire.
35 | # Port forwarding tokens seem to last ~2 months, so the chances of needing to call it again are low but we may as well do what the app does.
36 | # The second (bindPort) is done once at startup and again every 15 mins to 'keep the forwarded port alive'.
37 | #
38 | # This script has been tested with Wireguard and briefly with OpenVPN
39 | #
40 | # This script is based on what was found in the source code to their desktop app (v.2.2.0):
41 | # https://github.com/pia-foss/desktop/blob/2.2.0/daemon/src/portforwardrequest.cpp
42 | # Use at your own risk!
43 | #
44 | # As of Sep 2020, PIA have released their own standalone scripts for use outside of their app:
45 | # https://github.com/pia-foss/manual-connections
46 | #
47 | # Feel free to take apart and use in your own projects. A link back to the original might be nice though.
48 |
49 | # An error with no recovery logic occured
50 | fatal_error () {
51 | cleanup
52 | echo "$(date): Fatal error"
53 | exit 1
54 | }
55 |
56 | cleanup(){
57 | [ "$cacert_istemp" == "1" ] && [ -w "$cacert" ] && rm "$cacert"
58 | }
59 |
60 | # Handle shutdown behavior
61 | finish () {
62 | cleanup
63 | echo "$(date): Port forward rebinding stopped. The port will likely close soon."
64 | exit 0
65 | }
66 | trap finish SIGTERM SIGINT SIGQUIT
67 |
68 | usage() {
69 | echo "Options:
70 | -t Path to a valid PIA auth token
71 | -i (Optional) IP to send port-forward API requests to.
72 | An 'educated guess' is made if not specified.
73 | -n (Optional) Common name of the VPN server (eg. \"london411\")
74 | An 'educated guess' is made if not specified.
75 | -p (Optional) Dump forwarded port here for access by other scripts
76 | -f (Optional) Network interface to use for requests
77 | -s (Optional) Run a script on success.
78 | The forwarded port is passed as an argument.
79 | -r (Optional) In order to re-use the same forwarded port number between
80 | sessions, the port forwarding token can be stored here. Tokens can last
81 | up to 2 months after which a new token will be retrieved and the forwarded
82 | port will change"
83 | }
84 |
85 | while getopts ":t:i:n:c:p:f:s:r:" args; do
86 | case ${args} in
87 | t)
88 | tokenfile=$OPTARG
89 | ;;
90 | i)
91 | api_ip=$OPTARG
92 | ;;
93 | n)
94 | vpn_cn=$OPTARG
95 | ;;
96 | c)
97 | cacert=$OPTARG
98 | ;;
99 | p)
100 | portfile=$OPTARG
101 | ;;
102 | f)
103 | iface_tr="-i $OPTARG"
104 | iface_curl="--interface $OPTARG"
105 | ;;
106 | s)
107 | post_script=$OPTARG
108 | ;;
109 | r)
110 | persist_file=$OPTARG
111 | ;;
112 | *)
113 | echo "Unknown option"
114 | exit 1
115 | ;;
116 | esac
117 | done
118 |
119 | bind_port () {
120 | # Store transient errors here. Only display on fail.
121 | local stderr_tmp
122 | stderr_tmp=$(mktemp)
123 | # shellcheck disable=SC2086
124 | pf_bind=$(curl --get --silent --show-error $iface_curl --connect-timeout "$curl_connection_timeout" \
125 | --retry "$curl_retry" --retry-delay "$curl_retry_delay" --max-time "$curl_max_time" \
126 | --data-urlencode "payload=$pf_payload" \
127 | --data-urlencode "signature=$pf_getsignature" \
128 | $verify \
129 | "https://$pf_host:19999/bindPort" 2> "$stderr_tmp")
130 | if [ "$(jq -r .status <<< "$pf_bind ")" != "OK" ]; then
131 | echo "$(date): bindPort error"
132 | echo "$(date): Curl error/s:"
133 | cat "$stderr_tmp"
134 | rm "$stderr_tmp"
135 | echo "$(date): API response:"
136 | echo "$pf_bind"
137 | return 1
138 | fi
139 | rm "$stderr_tmp"
140 | return 0
141 | }
142 |
143 | get_sig () {
144 | # Attempt to reuse our previous port if requested
145 | if [ -n "$persist_file" ] && [ -r "$persist_file" ]; then
146 | echo "$(date): Reusing previous PF token"
147 | pf_getsig=$(cat "$persist_file")
148 | else
149 | # shellcheck disable=SC2086
150 | pf_getsig=$(curl --get --silent --show-error $iface_curl --connect-timeout "$curl_connection_timeout" \
151 | --retry "$curl_retry" --retry-delay "$curl_retry_delay" --max-time "$curl_max_time" \
152 | --data-urlencode "token=$(cat "$tokenfile")" \
153 | $verify \
154 | "https://$pf_host:19999/getSignature")
155 | fi
156 | if [ "$(jq -r .status <<< "$pf_getsig")" != "OK" ]; then
157 | echo "$(date): getSignature error"
158 | echo "$pf_getsig"
159 | fatal_error
160 | fi
161 | # Save response for re-use if requested
162 | [ -n "$persist_file" ] && echo "$pf_getsig" > "$persist_file"
163 | pf_payload=$(jq -r .payload <<< "$pf_getsig")
164 | pf_getsignature=$(jq -r .signature <<< "$pf_getsig")
165 | pf_port=$(base64 -d <<< "$pf_payload" | jq -r .port)
166 | pf_token_expiry_raw=$(base64 -d <<< "$pf_payload " | jq -r .expires_at)
167 | # Coreutils date doesn't need format specified (-D), whereas BusyBox does
168 | if date --help 2>&1 /dev/null | grep -iq 'busybox'; then
169 | pf_token_expiry=$(date -D %Y-%m-%dT%H:%M:%S --date="$pf_token_expiry_raw" +%s)
170 | else
171 | pf_token_expiry=$(date --date="$pf_token_expiry_raw" +%s)
172 | fi
173 | }
174 |
175 | # We don't use any error handling or retry logic beyond what curl provides
176 | curl_max_time=60
177 | curl_retry=10
178 | curl_retry_delay=30
179 | curl_connection_timeout=30
180 |
181 | # Rebind every 15 mins (same as desktop app)
182 | pf_bindinterval=$(( 15 * 60))
183 | # Get a new token when the current one has less than this remaining
184 | # Defaults to 7 days (same as desktop app)
185 | pf_minreuse=$(( 60 * 60 * 24 * 7 ))
186 |
187 | pf_remaining=0
188 | pf_firstrun=1
189 |
190 | # Minimum args needed to run
191 | if [ -z "$tokenfile" ]; then
192 | usage && exit 0
193 | fi
194 |
195 | # Hacky way to try to automatically get the API IP: use the first hop of a traceroute.
196 | # This seems to work for both Wireguard and OpenVPN.
197 | # Ideally we'd have been provided a cn, in case we 'guess' the wrong IP.
198 | # Must be a better way to do this.
199 | if [ -z "$api_ip" ]; then
200 | # shellcheck disable=SC2086
201 | api_ip=$(traceroute -4 -m 1 $iface_tr privateinternetaccess.com | tail -n 1 | awk '{print $2}')
202 | # Very basic sanity check - make sure it matches 10.x.x.1
203 | if ! grep -q '10\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.1' <<< "$api_ip"; then
204 | echo "$(date): Automatically getting API IP failed."
205 | fatal_error
206 | fi
207 | echo "$(date): Using $api_ip as API endpoint"
208 | fi
209 |
210 | # If we haven't been passed a cn, then use the cn the server is claiming
211 | if [ -z "$vpn_cn" ]; then
212 | # shellcheck disable=SC2086
213 | possible_cn=$(curl $iface_curl --insecure --verbose --head "https://$api_ip:19999" 2>&1 | grep '\\* subject' | sed 's/.*CN=\(.*\)\;.*/\1/')
214 | # Sanity check - match 'lowercase123'
215 | if grep -q '[a-z]*[0-9]\{3\}' <<< "$possible_cn"; then
216 | echo "$(date): Using $possible_cn as cn"
217 | vpn_cn="$possible_cn"
218 | fi
219 | fi
220 |
221 | # If we've been provided a cn, we can verify using the PIA ca cert
222 | if [ -n "$vpn_cn" ]; then
223 | # Get the PIA ca crt if we weren't given it
224 | if [ -z "$cacert" ]; then
225 | echo "$(date): Getting PIA ca cert"
226 | cacert=$(mktemp)
227 | cacert_istemp=1
228 | # shellcheck disable=SC2086
229 | if ! curl $iface_curl --get --silent --max-time "$curl_max_time" --output "$cacert" --connect-timeout "$curl_connection_timeout" \
230 | --retry "$curl_retry" --retry-delay "$curl_retry_delay" --max-time "$curl_max_time" \
231 | "https://raw.githubusercontent.com/pia-foss/desktop/master/daemon/res/ca/rsa_4096.crt"; then
232 | echo "(date): Failed to download PIA ca cert"
233 | fatal_error
234 | fi
235 | fi
236 | verify="--cacert $cacert --resolve $vpn_cn:19999:$api_ip"
237 | pf_host="$vpn_cn"
238 | echo "$(date): Verifying API requests. CN: $vpn_cn"
239 | else
240 | # For simplicity, use '--insecure' by default, though show a warning
241 | echo "$(date): API requests may be insecure. Specify a common name using -n."
242 | verify="--insecure"
243 | pf_host="$api_ip"
244 | fi
245 |
246 | # Main loop
247 | while true; do
248 | pf_remaining=$(( pf_token_expiry - $(date +%s) ))
249 | # Get a new pf token as the previous one will expire soon
250 | if [ $pf_remaining -lt $pf_minreuse ]; then
251 | if [ $pf_firstrun -ne 1 ]; then
252 | echo "$(date): PF token will expire soon. Getting new one."
253 | [ -n "$persist_file" ] && [ -w "$persist_file" ] && rm "$persist_file"
254 | else
255 | echo "$(date): Getting PF token"
256 | pf_firstrun=0
257 | fi
258 | get_sig
259 | if ! bind_port; then
260 | # If we attempted to use a previous port and binding failed then discard it and retry
261 | if [ -n "$persist_file" ] && [ -w "$persist_file" ]; then
262 | echo "$(date): Discarding previous PF token and trying again"
263 | rm "$persist_file"
264 | get_sig
265 | bind_port || fatal_error
266 | else
267 | fatal_error
268 | fi
269 | fi
270 | echo "$(date): Obtained PF token. Expires at $pf_token_expiry_raw"
271 | echo "$(date): Server accepted PF bind"
272 | echo "$(date): Forwarding on port $pf_port"
273 | echo "$(date): Rebind interval: $pf_bindinterval seconds"
274 | # Dump port here if requested
275 | [ -n "$portfile" ] && echo "$(date): Port dumped to $portfile" && echo "$pf_port" > "$portfile"
276 | echo "$(date): This script should remain running to keep the forwarded port alive"
277 | echo "$(date): Press Ctrl+C to exit"
278 | # Run another script if requested
279 | [ -n "$post_script" ] && echo "$(date): Running $post_script" && eval "$post_script $pf_port" &
280 | fi
281 | # Rebind at a specific time instead of simply sleeping in case the system itself goes to sleep
282 | # This prevents a delayed rebind after waking up
283 | nextbind=$(( $(date +%s) + pf_bindinterval ))
284 | while [[ $(date +%s) -lt $nextbind ]]; do
285 | sleep 5 &
286 | wait $!
287 | done
288 | bind_port || fatal_error
289 | done
290 |
291 |
--------------------------------------------------------------------------------
/extra/pia-auth.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Generate and output a PIA auth token
4 | #
5 | # Requires jq and curl
6 | #
7 | # Options:
8 | # -u
9 | # -p
10 | # -i (Optional)
11 | # -o (Optional)
12 | # -n (Optional)
13 | # -c (Optional) Path to ca cert used to secure communication with "meta" servers
14 | #
15 | # Examples:
16 | # ./pia-auth.sh -u myusername -p mypassword > ~/.pia-token
17 | # ./pia-auth.sh -u myusername -p mypassword -i 12.34.56.78 -n location401 -p 443 -c /path/to/ca.crt > ~/.pia-token
18 | #
19 | # By default, the www.privateinternetaccess.com API endpoint is used.
20 | # If needed, 'meta' services on the VPN servers themselves can be used instead.
21 | #
22 | # deauth using:
23 | # curl --silent --show-error --request POST \
24 | # --header "Content-Type: application/json" \
25 | # --header "Authorization: Token $(cat ~/.pia-token)" \
26 | # --data "{}" \
27 | # "https://www.privateinternetaccess.com/api/client/v2/expire_token"
28 |
29 | [ -n "$DEBUG" ] && set -o xtrace
30 |
31 | while getopts ":u:p:i:c:o:n:" args; do
32 | case ${args} in
33 | u)
34 | user="$OPTARG"
35 | ;;
36 | p)
37 | pass="$OPTARG"
38 | ;;
39 | i)
40 | meta_ip="$OPTARG"
41 | ;;
42 | c)
43 | cacert="$OPTARG"
44 | ;;
45 | o)
46 | meta_port="$OPTARG"
47 | ;;
48 | n)
49 | meta_cn="$OPTARG"
50 | ;;
51 | *)
52 | echo "Unknown option"
53 | exit 1
54 | ;;
55 | esac
56 | done
57 |
58 | usage() {
59 | echo 'Options:
60 | -u
61 | -p
62 | -i (Optional)
63 | -o (Optional)
64 | -n (Optional)
65 | -c (Optional) Path to ca cert used to secure communication with "meta" servers'
66 | exit 1
67 | }
68 |
69 | get_auth_token () {
70 | if [ -n "$meta_port" ] && [ -n "$meta_ip" ] && [ -n "$meta_cn" ] && [ -n "$cacert" ]; then
71 | # https://github.com/pia-foss/desktop/blob/master/daemon/src/metaserviceapibase.h
72 | # shellcheck disable=SC2086
73 | token_response=$(curl $CURL_OVERRIDE_PARAMS --silent --location --show-error --request POST --max-time "$curl_max_time" \
74 | --resolve "$meta_cn:$meta_port:$meta_ip" \
75 | --data-urlencode "username=$user" \
76 | --data-urlencode "password=$pass" \
77 | --cacert "$cacert" \
78 | "https://$meta_cn:$meta_port/api/client/v2/token")
79 | else
80 | # shellcheck disable=SC2086
81 | token_response=$(curl $CURL_OVERRIDE_PARAMS --silent --location --show-error --request POST --max-time "$curl_max_time" \
82 | 'https://www.privateinternetaccess.com/api/client/v2/token' \
83 | --data-urlencode "username=$user" \
84 | --data-urlencode "password=$pass")
85 | fi
86 | TOK=$(jq -r .'token' <<< "$token_response")
87 | if [ -z "$TOK" ] || [ "$TOK" == "null" ]; then
88 | echo "Failed to acquire new auth token. Response:" >&2
89 | echo "$token_response" >&2
90 | exit 1
91 | fi
92 | echo "$TOK"
93 | }
94 |
95 | if [ -z "$pass" ] || [ -z "$user" ]; then
96 | usage
97 | fi
98 |
99 | curl_max_time=15
100 | get_auth_token
101 | exit 0
--------------------------------------------------------------------------------
/extra/wg-gen.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Bash script for generating basic Wireguard config files (for use with wg-quick) for the PIA 'next-gen' network
4 | #
5 | # Requires Wireguard (for generating a key pair), jq and curl
6 | # Optionally requires OpenSSL for verifying server list
7 | #
8 | # Options:
9 | # -t Path to a valid PIA auth token
10 | # -l id of the location to connect to (eg. "swiss")
11 | # -o (Optional) Verify the server list using this public key. Requires OpenSSL.
13 | # -d (Optional) Use these DNS servers in the generated WG config. Defaults to PIA's DNS.
14 | # -m (Optional) Use this as the interface's mtu value in the generated config
15 | # -c (Optional) Path to PIA ca cert. Fetched from the PIA Github repository if not provided.
16 | # -a List available locations and whether they support port forwarding
17 | #
18 | # Examples:
19 | # wg-gen.sh -a
20 | # wg-gen.sh -l swiss -t ~/.token -o ~/wg.conf
21 | # wg-gen.sh -l swiss -t ~/.token -o ~/wg.conf -k ~/pubkey.pem -d 8.8.8.8,8.8.4.4
22 | #
23 | # To force the use of a specific server, the PIA_IP PIA_PORT and PIA_CN env vars must all be set
24 | # eg: $ PIA_IP=1.2.3.4 PIA_PORT=1337 PIA_CN=hostname401 wg-gen.sh -t ~/.token -o ~/wg.conf
25 | #
26 | # To use a dedicated ip, the PIA_DIP_TOKEN env var must be set
27 | # eg: $ PIA_DIP_TOKEN=DIPabc123 wg-gen.sh -t ~/.token -o ~/wg.conf
28 | #
29 | # API requests can be sent via PIA's 'meta' servers by setting the META_IP META_CN and META_PORT env vars
30 | # eg: $ META_IP=123.45.67.89 META_CN=hostname401 META_PORT=443 ./wg-gen.sh -t ~/.token ~/wg.conf
31 | #
32 | # Available servers can be found here:
33 | # https://serverlist.piaservers.net/vpninfo/servers/v6
34 | # The public key for verifying the server list can be found here:
35 | # https://github.com/pia-foss/desktop/blob/122710c6ada5db83620c63faff2d805ea52d7f40/daemon/src/environment.cpp#L30
36 | # The PIA ca cert can be found here:
37 | # https://github.com/pia-foss/desktop/blob/master/daemon/res/ca/rsa_4096.crt
38 | #
39 | # As of Sep 2020, PIA have released their own standalone scripts for use outside of their app:
40 | # https://github.com/pia-foss/manual-connections
41 |
42 | # Exit codes:
43 | # 0: Success
44 | # 1: Anything else
45 | # 2: Auth error
46 | # 3: Invalid server location
47 | # 4: Registration failed
48 |
49 | [ -n "$DEBUG" ] && set -o xtrace
50 |
51 | fatal_error () {
52 | cleanup
53 | [ -n "$1" ] && exit "$1"
54 | exit 1
55 | }
56 |
57 | cleanup(){
58 | [ -w "$servers_raw" ] && rm "$servers_raw"
59 | [ -w "$servers_json" ] && rm "$servers_json"
60 | [ -w "$servers_sig" ] && rm "$servers_sig"
61 | [ -w "$addkey_response" ] && rm "$addkey_response"
62 | [ -w "$pia_cacert_tmp" ] && rm "$pia_cacert_tmp"
63 | return 0
64 | }
65 |
66 | usage() {
67 | echo "Options:"
68 | echo " -t Path to a valid PIA auth token"
69 | echo " -l id of the location to connect to (eg. \"swiss\")"
70 | echo " -o (Optional) Verify the server list using this public key. Requires OpenSSL."
72 | echo " -d (Optional) Use these DNS servers in the generated WG config. Defaults to PIA's DNS."
73 | echo " -m (Optional) Use this as the interface's mtu value in the generated config"
74 | echo " -c (Optional) Path to PIA ca cert. Fetched from the PIA Github repository if not provided."
75 | echo " -a List available locations and whether they support port forwarding"
76 | }
77 |
78 | parse_args() {
79 | while getopts ":t:l:o:k:c:d:m:a" args; do
80 | case ${args} in
81 | t)
82 | tokenfile="$OPTARG"
83 | ;;
84 | l)
85 | location="$OPTARG"
86 | ;;
87 | o)
88 | wg_out="$OPTARG"
89 | ;;
90 | k)
91 | pia_pubkey="$OPTARG"
92 | ;;
93 | c)
94 | pia_cacert="$OPTARG"
95 | ;;
96 | d)
97 | dns="$OPTARG"
98 | ;;
99 | m)
100 | mtu="$OPTARG"
101 | ;;
102 | a)
103 | list_and_exit=1
104 | ;;
105 | *)
106 | echo "Unknown option"
107 | exit 1
108 | ;;
109 | esac
110 | done
111 | }
112 |
113 | # The PIA desktop app uses a public key to verify server list downloads.
114 | # https://github.com/pia-foss/desktop/blob/b701601bfa806621a41039514bbb507e250466ec/common/src/jsonrefresher.cpp#L93
115 | verify_serverlist ()
116 | {
117 | if openssl dgst -sha256 -verify "$pia_pubkey" -signature "$servers_sig" "$servers_json"; then
118 | echo "Verified server list"
119 | else
120 | echo "Failed to verify server list"
121 | fatal_error
122 | fi
123 | }
124 |
125 | get_dip_serverinfo ()
126 | {
127 | if [ -n "$META_IP" ] && [ -n "$META_CN" ] && [ -n "$META_PORT" ]; then
128 | echo "$(date): Fetching dedicated ip server info via meta server: ip: $META_IP, cn: $META_CN, port: $META_PORT"
129 | # shellcheck disable=SC2086
130 | dip_response=$(curl --silent --show-error $curl_params --location --request POST \
131 | "https://$META_CN:$META_PORT/api/client/v2/dedicated_ip" \
132 | --header 'Content-Type: application/json' \
133 | --header "Authorization: Token $(cat "$tokenfile")" \
134 | --cacert "$pia_cacert" --resolve "$META_CN:$META_PORT:$META_IP" \
135 | --data-raw '{
136 | "tokens":["'"$PIA_DIP_TOKEN"'"]
137 | }')
138 | else
139 | echo "$(date): Fetching dedicated ip server info"
140 | # shellcheck disable=SC2086
141 | dip_response=$(curl --silent --show-error $curl_params --location --request POST \
142 | 'https://www.privateinternetaccess.com/api/client/v2/dedicated_ip' \
143 | --header 'Content-Type: application/json' \
144 | --header "Authorization: Token $(cat "$tokenfile")" \
145 | --data-raw '{
146 | "tokens":["'"$PIA_DIP_TOKEN"'"]
147 | }')
148 | fi
149 |
150 | [ "$dip_response" == "HTTP Token: Access denied." ] && echo "Auth failed" && fatal_error 2
151 |
152 | if [ "$(jq -r '.[0].status' <<< "$dip_response")" != "active" ]; then
153 | echo "$(date): Failed to fetch dedicated ip server info. Response:"
154 | echo "$dip_response"
155 | fatal_error
156 | fi
157 |
158 | wg_port=1337
159 | wg_cn=$(jq -r '.[0].cn' <<< "$dip_response")
160 | wg_ip=$(jq -r '.[0].ip' <<< "$dip_response")
161 |
162 | echo "$(date): Dedicated ip: $wg_ip, cn: $wg_cn"
163 |
164 | # PIA's standalone scripts seem to assume port forwarding is available everywhere apart from the us
165 | [[ $(jq -r '.[0].id' <<< "$dip_response") != us_* ]] && port_forward_avail=1
166 | }
167 |
168 | get_servers() {
169 | if [ -n "$META_IP" ] && [ -n "$META_CN" ] && [ -n "$META_PORT" ]; then
170 | echo "Fetching next-gen PIA server list via meta server: ip: $META_IP, cn: $META_CN, port: $META_PORT"
171 | # shellcheck disable=SC2086
172 | curl --silent --show-error $curl_params --cacert "$pia_cacert" --resolve "$META_CN:$META_PORT:$META_IP" \
173 | "https://$META_CN:$META_PORT/vpninfo/servers/v6" > "$servers_raw"
174 | else
175 | echo "Fetching next-gen PIA server list"
176 | # shellcheck disable=SC2086
177 | curl --silent --show-error $curl_params \
178 | "https://serverlist.piaservers.net/vpninfo/servers/v6" > "$servers_raw"
179 | fi
180 | head -n 1 "$servers_raw" | tr -d '\n' > "$servers_json"
181 | tail -n +3 "$servers_raw" | base64 -d > "$servers_sig"
182 | [ -n "$pia_pubkey" ] && verify_serverlist
183 |
184 | [ "$list_and_exit" -eq 1 ] && echo "Available location ids:" && jq '.regions | .[] | {name, id, port_forward}' "$servers_json" && cleanup && exit 0
185 |
186 | # Some locations have multiple servers available. Pick a random one.
187 | totalservers=$(jq -r '.regions | .[] | select(.id=="'"$location"'") | .servers.wg | length' "$servers_json")
188 | if ! [[ "$totalservers" =~ ^[0-9]+$ ]] || [ "$totalservers" -eq 0 ] 2>/dev/null; then
189 | echo "Location \"$location\" not found. Run with -a to list valid servers."
190 | fatal_error 3
191 | fi
192 | serverindex=$(( RANDOM % totalservers))
193 | wg_cn=$(jq -r '.regions | .[] | select(.id=="'"$location"'") | .servers.wg | .['$serverindex'].cn' "$servers_json")
194 | wg_ip=$(jq -r '.regions | .[] | select(.id=="'"$location"'") | .servers.wg | .['$serverindex'].ip' "$servers_json")
195 | wg_port=$(jq -r '.groups.wg | .[0] | .ports | .[0]' "$servers_json")
196 |
197 | [ "$(jq -r '.regions | .[] | select(.id=="'"$location"'") | .port_forward' "$servers_json")" == "true" ] && port_forward_avail=1
198 | }
199 |
200 | get_wgconf () {
201 | client_private_key="$(wg genkey)"
202 | if ! client_public_key=$(wg pubkey <<< "$client_private_key"); then
203 | echo "$(date) Error generating Wireguard key pair" && fatal_error
204 | fi
205 |
206 | # https://github.com/pia-foss/desktop/blob/754080ce15b6e3555321dde2dcfd0c21ec25b1a9/daemon/src/wireguardmethod.cpp#L1150
207 |
208 | if [ -z "$pia_cacert" ]; then
209 | echo "$(date) Fetching PIA ca cert"
210 | pia_cacert_tmp=$(mktemp)
211 | # shellcheck disable=SC2086
212 | if ! curl --get --silent --show-error $curl_params --output "$pia_cacert_tmp" "https://raw.githubusercontent.com/pia-foss/desktop/master/daemon/res/ca/rsa_4096.crt"; then
213 | echo "Failed to download PIA ca cert"
214 | fatal_error
215 | fi
216 | pia_cacert="$pia_cacert_tmp"
217 | fi
218 |
219 | if [ -n "$PIA_DIP_TOKEN" ]; then
220 | echo "Registering public key with PIA dedicated ip endpoint; cn: $wg_cn, ip: $wg_ip"
221 | # shellcheck disable=SC2086
222 | curl --get --silent --show-error $curl_params \
223 | --user "dedicated_ip_$PIA_DIP_TOKEN:$wg_ip" \
224 | --data-urlencode "pubkey=$client_public_key" \
225 | --cacert "$pia_cacert" \
226 | --resolve "$wg_cn:$wg_port:$wg_ip" \
227 | "https://$wg_cn:$wg_port/addKey" > "$addkey_response"
228 | else
229 | echo "Registering public key with PIA endpoint; id: $location, cn: $wg_cn, ip: $wg_ip"
230 | # shellcheck disable=SC2086
231 | curl --get --silent --show-error $curl_params \
232 | --data-urlencode "pubkey=$client_public_key" \
233 | --data-urlencode "pt=$(cat "$tokenfile")" \
234 | --cacert "$pia_cacert" \
235 | --resolve "$wg_cn:$wg_port:$wg_ip" \
236 | "https://$wg_cn:$wg_port/addKey" > "$addkey_response"
237 | fi
238 |
239 | [ "$(jq -r .status "$addkey_response")" == "ERROR" ] && [ "$(jq -r .message "$addkey_response")" == "Login failed!" ] && echo "Auth failed" && cat "$addkey_response" && fatal_error 2
240 | [ "$(jq -r .status "$addkey_response")" != "OK" ] && echo "WG key registration failed" && cat "$addkey_response" && fatal_error 4
241 |
242 | peer_ip="$(jq -r .peer_ip "$addkey_response")"
243 | server_public_key="$(jq -r .server_key "$addkey_response")"
244 | server_port="$(jq -r .server_port "$addkey_response")"
245 | pfapi_ip="$(jq -r .server_vip "$addkey_response")"
246 |
247 | echo "Generating $wg_out"
248 |
249 | if [ -z "$dns" ]; then
250 | dns=$(jq -r '.dns_servers[0:2]' "$addkey_response" | grep ^\ | cut -d\" -f2 | xargs echo | sed -e 's/ /,/g')
251 | echo "Using PIA DNS servers: $dns"
252 | elif [ "$dns" = "0" ]; then
253 | echo "Using default container DNS servers"
254 | dns=""
255 | else
256 | echo "Using custom DNS servers: $dns"
257 | fi
258 |
259 | cat < "$wg_out"
260 | [Interface]
261 | PrivateKey = $client_private_key
262 | Address = $peer_ip
263 | DNS = $dns
264 | CONFF
265 |
266 | if [ -n "$mtu" ]; then
267 | echo "Using custom MTU: $mtu"
268 | echo "MTU = $mtu" >> "$wg_out"
269 | fi
270 |
271 | cat <> "$wg_out"
272 |
273 | [Peer]
274 | PublicKey = $server_public_key
275 | AllowedIPs = 0.0.0.0/0
276 | Endpoint = $wg_ip:$server_port
277 | CONFF
278 |
279 | # Store the info needed for port forwarding as comments in the generated config for later use if needed
280 | if [ "$port_forward_avail" -eq 1 ]; then
281 | echo "#cn: $wg_cn" >> "$wg_out"
282 | echo "#pf api ip: $pfapi_ip" >> "$wg_out"
283 | fi
284 |
285 | }
286 |
287 | curl_params="$CURL_OVERRIDE_PARAMS --compressed --retry 5 --retry-delay 5 --max-time 120 --connect-timeout 15"
288 |
289 | port_forward_avail=0
290 | list_and_exit=0
291 |
292 | parse_args "$@"
293 |
294 | # Minimum args needed to run
295 | if [ "$list_and_exit" -eq 0 ]; then
296 | if [ -z "$tokenfile" ] || [ -z "$wg_out" ]; then
297 | usage && exit 0
298 | fi
299 | fi
300 |
301 | servers_raw=$(mktemp)
302 | servers_sig=$(mktemp)
303 | servers_json=$(mktemp)
304 | addkey_response=$(mktemp)
305 |
306 | if [ -n "$PIA_DIP_TOKEN" ]; then
307 | get_dip_serverinfo
308 | # Set env vars PIA_CN, PIA_IP and PIA_PORT to connect to a specific server
309 | elif [ -n "$PIA_CN" ] && [ -n "$PIA_IP" ] && [ -n "$PIA_PORT" ]; then
310 | wg_cn="$PIA_CN"
311 | wg_ip="$PIA_IP"
312 | wg_port="$PIA_PORT"
313 | location="manual"
314 | else
315 | # Otherwise get what we need from the server list
316 | get_servers
317 | fi
318 |
319 | get_wgconf
320 |
321 | if [ "$port_forward_avail" -eq 1 ]; then
322 | echo "Port forwarding is available at this location"
323 | else
324 | echo "Port forwarding is not available at this location"
325 | fi
326 |
327 | echo "Successfully generated $wg_out"
328 |
329 | cleanup
330 | exit 0
--------------------------------------------------------------------------------
/healthcheck.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # By default, only do checks that don't generate any traffic
4 | [[ "$ACTIVE_HEALTHCHECKS" =~ ^[0-1]$ ]] || ACTIVE_HEALTHCHECKS=0
5 | HEALTHCHECK_PING_TARGET="${HEALTHCHECK_PING_TARGET:-www.privateinternetaccess.com}"
6 | [[ "$HEALTHCHECK_PING_TIMEOUT" =~ ^[0-9]+$ ]] || HEALTHCHECK_PING_TIMEOUT=3
7 |
8 | wg show wg0 || exit 1
9 |
10 | if [ "$PORT_FORWARDING" = "1" ]; then
11 | pidof pf.sh || exit 1
12 | fi
13 |
14 | if [ "$ACTIVE_HEALTHCHECKS" = "1" ]; then
15 | # Loop through a list of targets, and fail if none of them respond
16 | # Accept comma separated as well as space separated list
17 | success=0
18 | for target in ${HEALTHCHECK_PING_TARGET//,/ }; do
19 | ping -c 1 -w "$HEALTHCHECK_PING_TIMEOUT" -I wg0 "$target" && success=1 && break
20 | done
21 | [ "$success" = "1" ] || exit 1
22 | fi
--------------------------------------------------------------------------------
/pf_success.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # This script is run once a port has been successfully forwarded
4 | # The port number is passed as the first argument
5 |
6 | [[ "$FIREWALL" =~ ^[0-1]$ ]] || FIREWALL=1
7 |
8 | if [ "$FIREWALL" -eq 1 ]; then
9 | iptables -A INPUT -p tcp -i wg0 --dport "$1" -j ACCEPT
10 | iptables -A INPUT -p udp -i wg0 --dport "$1" -j ACCEPT
11 | echo "$(date): Allowing incoming traffic on port $1"
12 | fi
13 |
14 | # Set env var PF_DEST_IP to forward on to another address
15 | # eg PF_DEST_IP=192.168.1.48
16 | if [ -n "$PF_DEST_IP" ] && [ -n "$FWD_IFACE" ]; then
17 | iptables -t nat -A PREROUTING -p tcp --dport "$1" -j DNAT --to-destination "$PF_DEST_IP:$1"
18 | iptables -t nat -A PREROUTING -p udp --dport "$1" -j DNAT --to-destination "$PF_DEST_IP:$1"
19 | iptables -A FORWARD -i wg0 -o "$FWD_IFACE" -p tcp -d "$PF_DEST_IP" --dport "$1" -j ACCEPT
20 | iptables -A FORWARD -i wg0 -o "$FWD_IFACE" -p udp -d "$PF_DEST_IP" --dport "$1" -j ACCEPT
21 | echo "$(date): Forwarding incoming VPN traffic on port $1 to $PF_DEST_IP:$1"
22 | fi
23 |
24 | # Run another user-defined script/command if defined
25 | [ -n "$PORT_SCRIPT" ] && echo "$(date): Running user-defined command: $PORT_SCRIPT" && eval "$PORT_SCRIPT $1" &
26 |
--------------------------------------------------------------------------------
/run:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | [ -n "$DEBUG" ] && set -o xtrace
4 |
5 | # Check and/or set default options
6 | # Should be 0/1
7 | [[ "$EXIT_ON_FATAL" =~ ^[0-1]$ ]] || EXIT_ON_FATAL=0
8 | [[ "$FIREWALL" =~ ^[0-1]$ ]] || FIREWALL=1
9 | [[ "$PORT_FILE_CLEANUP" =~ ^[0-1]$ ]] || PORT_FILE_CLEANUP=0
10 | [[ "$PORT_FORWARDING" =~ ^[0-1]$ ]] || PORT_FORWARDING=0
11 | [[ "$PORT_PERSIST" =~ ^[0-1]$ ]] || PORT_PERSIST=0
12 | [[ "$PORT_FATAL" =~ ^[0-1]$ ]] || PORT_FATAL=0
13 | # Should be a positive integer
14 | [[ "$KEEPALIVE" =~ ^[0-9]+$ ]] || KEEPALIVE=0
15 | [[ "$META_PORT" =~ ^[0-9]+$ ]] || export META_PORT=443
16 | [[ "$MONITOR_INTERVAL" =~ ^[0-9]+$ ]] || export MONITOR_INTERVAL=60
17 | [[ "$MONITOR_RETRIES" =~ ^[0-9]+$ ]] || export MONITOR_RETRIES=3
18 |
19 | # Maybe also check the following. They are all blank by default.
20 | # LOCAL_NETWORK=
21 | # PIA_CN=
22 | # PIA_IP=
23 | # PIA_PORT=
24 | # PORT_FILE=
25 | # QDISC=
26 | # VPNDNS=
27 | # MTU=
28 |
29 | configdir="/pia"
30 | tokenfile="$configdir/.token"
31 | pf_persistfile="$configdir/portsig.json"
32 |
33 | # Run custom scripts at the appropriate time if present
34 | # We also run custom commands specified by the PRE_UP, POST_UP, PRE_DOWN, and POST_DOWN env vars at the same time
35 | custom_scriptdir="/pia/scripts"
36 | pre_up_script="$custom_scriptdir/pre-up.sh"
37 | post_up_script="$custom_scriptdir/post-up.sh"
38 | pre_down_script="$custom_scriptdir/pre-down.sh"
39 | post_down_script="$custom_scriptdir/post-down.sh"
40 |
41 | sharedir="/pia-shared"
42 | # Set env var PORT_FILE to override where the forwarded port number is dumped
43 | # Might need to handle setting file ownership/permissions too
44 | portfile="${PORT_FILE:-$sharedir/port.dat}"
45 |
46 | pia_cacrt="/rsa_4096.crt"
47 | wg_conf="/etc/wireguard/wg0.conf"
48 |
49 | firewall_init () {
50 | # Block everything by default
51 | ip6tables -P OUTPUT DROP &> /dev/null
52 | ip6tables -P INPUT DROP &> /dev/null
53 | ip6tables -P FORWARD DROP &> /dev/null
54 | iptables -P OUTPUT DROP &> /dev/null
55 | iptables -P INPUT DROP &> /dev/null
56 | iptables -P FORWARD DROP &> /dev/null
57 |
58 | # Allow loopback traffic and input for established connections
59 | iptables -A OUTPUT -o lo -j ACCEPT
60 | iptables -A INPUT -i lo -j ACCEPT
61 | iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
62 |
63 | firewall_temp_add
64 | }
65 |
66 | # This can be used to test that a particular pattern matching module is available
67 | # A bit blunt, but other methods such as using 'iptables-restore --test' seem to pass in some cases
68 | # when the command itself will fail.
69 | iptables_test () {
70 | iptables -A OUTPUT "$@" &> /dev/null || return 1
71 | iptables -D OUTPUT "$@"
72 | return 0
73 | }
74 |
75 | firewall_temp_add () {
76 | # Temporarily allow the following:
77 | # Only include the following when available
78 | iptables_test -p udp -m owner --uid-owner root -j REJECT && local owner="-m owner --uid-owner root"
79 | iptables_test -p tcp -m multiport --sports "$curl_local_port_min":"$curl_local_port_max" -j REJECT && local multiport="-m multiport --sports $curl_local_port_min:$curl_local_port_max"
80 | # DNS queries
81 | if [ -n "$original_extservers" ]; then
82 | # shellcheck disable=SC2086
83 | iptables -A OUTPUT -o "$outgoing_iface_dns" $owner -p udp --dport 53 -d "$original_nameserver" -j ACCEPT
84 | for extserver in $original_extservers; do
85 | [[ "$extserver" =~ "host" ]] && continue
86 | # shellcheck disable=SC2086
87 | iptables -A OUTPUT $owner -p udp --dport 53 -d "$extserver" -j ACCEPT
88 | done
89 | else
90 | # This is for compatibility with older versions of Docker in case custom container
91 | # dns servers are set as we don't know what they're set to
92 | # shellcheck disable=SC2086
93 | iptables -A OUTPUT $owner -p udp --dport 53 -j ACCEPT
94 | fi
95 | # HTTPS to download the server list and access API for generating auth token
96 | # shellcheck disable=SC2086
97 | iptables -A OUTPUT -o "$outgoing_iface" $owner -p tcp --dport 443 $multiport -j ACCEPT
98 | # API access to register the public WireGuard key
99 | # shellcheck disable=SC2086
100 | iptables -A OUTPUT -o "$outgoing_iface" $owner -p tcp --dport 1337 $multiport -j ACCEPT
101 | # Non-default API port if set
102 | # shellcheck disable=SC2086
103 | [ "$META_PORT" -ne 443 ] && iptables -A OUTPUT -i "$outgoing_iface" $owner -p tcp --dport "$META_PORT" $multiport -j ACCEPT
104 | firewall_temp_state=1
105 | }
106 |
107 | firewall_temp_remove () {
108 | iptables_test -p udp -m owner --uid-owner root -j REJECT && local owner="-m owner --uid-owner root"
109 | iptables_test -p tcp -m multiport --sports "$curl_local_port_min":"$curl_local_port_max" -j REJECT && local multiport="-m multiport --sports $curl_local_port_min:$curl_local_port_max"
110 | if [ -n "$original_extservers" ]; then
111 | # shellcheck disable=SC2086
112 | iptables -D OUTPUT -o "$outgoing_iface_dns" $owner -p udp --dport 53 -d "$original_nameserver" -j ACCEPT
113 | for extserver in $original_extservers; do
114 | [[ "$extserver" =~ "host" ]] && continue
115 | # shellcheck disable=SC2086
116 | iptables -D OUTPUT $owner -p udp --dport 53 -d "$extserver" -j ACCEPT
117 | done
118 | else
119 | # shellcheck disable=SC2086
120 | iptables -D OUTPUT $owner -p udp --dport 53 -j ACCEPT
121 | fi
122 | # shellcheck disable=SC2086
123 | iptables -D OUTPUT -o "$outgoing_iface" $owner -p tcp --dport 443 $multiport -j ACCEPT
124 | # shellcheck disable=SC2086
125 | iptables -D OUTPUT -o "$outgoing_iface" $owner -p tcp --dport 1337 $multiport -j ACCEPT
126 | # shellcheck disable=SC2086
127 | [ "$META_PORT" -ne 443 ] && iptables -D OUTPUT -o "$outgoing_iface" $owner -p tcp --dport "$META_PORT" $multiport -j ACCEPT
128 | unset firewall_temp_state
129 | }
130 |
131 | # Alpine 3.19 changed the default iptables backend to iptables-nft
132 | # Check that the host supports this and revert to iptables-legacy if needed
133 | # Can force one or the other using NFTABLES=0/1 env var
134 | nftables_setup () {
135 | # Run an iptables command to see if things are working
136 | [ -z "$NFTABLES" ] && iptables -L &> /dev/null && return
137 | [ "$NFTABLES" = "1" ] && return
138 | # If not, change to legacy
139 | echo "$(date): Using legacy iptables backend"
140 | ln -sf xtables-legacy-multi "$(type -p iptables)"
141 | ln -sf xtables-legacy-multi "$(type -p iptables-save)"
142 | ln -sf xtables-legacy-multi "$(type -p iptables-restore)"
143 | ln -sf xtables-legacy-multi "$(type -p ip6tables)"
144 | ln -sf xtables-legacy-multi "$(type -p ip6tables-save)"
145 | ln -sf xtables-legacy-multi "$(type -p ip6tables-restore)"
146 | if ! iptables -L &> /dev/null; then
147 | echo "$(date): Error reverting to legacy iptables backend"
148 | fatal_error
149 | fi
150 | }
151 |
152 | # Handle shutdown behavior
153 | finish () {
154 | [ -x "$pre_down_script" ] && run_command "$pre_down_script"
155 | [ -n "$PRE_DOWN" ] && run_command "$PRE_DOWN"
156 | [ "$PORT_FORWARDING" -eq 1 ] && pkill -f 'pf.sh'
157 | echo "$(date): Shutting down WireGuard"
158 | # Remove forwarded port number dump file if requested
159 | [ "$PORT_FILE_CLEANUP" -eq 1 ] && [ -w "$portfile" ] && rm "$portfile"
160 | wg-quick down wg0
161 | [ -x "$post_down_script" ] && run_command "$post_down_script"
162 | [ -n "$POST_DOWN" ] && run_command "$POST_DOWN"
163 | exit 0
164 | }
165 |
166 | trap finish SIGTERM SIGINT SIGQUIT
167 |
168 | endpoint_monitor () {
169 | local failures=0
170 | while true; do
171 | if ! ACTIVE_HEALTHCHECKS=1 PORT_FORWARDING=0 /scripts/healthcheck.sh &> /dev/null; then
172 | ((failures++))
173 | else
174 | failures=0
175 | fi
176 | [ "$failures" -ge "$MONITOR_RETRIES" ] && return 1
177 | sleep "$MONITOR_INTERVAL"
178 | done
179 | }
180 |
181 | # All done. Sleep and wait for termination.
182 | now_sleep () {
183 | # If we need to, keep an eye on the port forwarding script and also monitor for unresponsive endpoint
184 | if [ "$RECONNECT" = "1" ]; then
185 | endpoint_monitor &
186 | local endpoint_monitor_pid="$!"
187 | echo "$(date): Started endpoint monitor"
188 | fi
189 |
190 | while [ -n "$pf_pid$endpoint_monitor_pid" ]; do
191 | # shellcheck disable=SC2086
192 | wait -n $endpoint_monitor_pid $pf_pid
193 | local result=$?
194 | # Check which pid terminated
195 | if [ -n "$pf_pid" ] && ! kill -0 "$pf_pid" 2> /dev/null; then
196 | if [ "$PORT_FORWARDING" = "1" ] && [ "$PORT_FATAL" = "1" ] && [ $result -ne 0 ];then
197 | echo "$(date): Port forwarding script failed"
198 | [ -n "$endpoint_monitor_pid" ] && kill "$endpoint_monitor_pid"
199 | fatal_error
200 | fi
201 | echo "$(date): Port forwarding script closed"
202 | unset pf_pid
203 | fi
204 | if [ -n "$endpoint_monitor_pid" ] && ! kill -0 "$endpoint_monitor_pid" 2> /dev/null && [ "$result" -ne 0 ]; then
205 | if [ "$PORT_FORWARDING" = "1" ]; then
206 | [ -n "$pf_pid" ] && kill "$pf_pid"
207 | unset pf_pid
208 | local prevport
209 | [ -r "$portfile" ] && prevport=$(cat "$portfile")
210 | if [ "$FIREWALL" = "1" ] && [ -n "$prevport" ]; then
211 | iptables -D INPUT -p tcp -i wg0 --dport "$prevport" -j ACCEPT
212 | iptables -D INPUT -p udp -i wg0 --dport "$prevport" -j ACCEPT
213 | fi
214 | fi
215 |
216 | echo "$(date): Unresponsive endpoint detected. Regenerating WireGuard config"
217 | [ -n "$PRE_RECONNECT" ] && run_command "$PRE_RECONNECT"
218 |
219 | # Temporarily allow the following to bypass the tunnel
220 | # This appears to be needed when curl binds to $outgoing_addr instead of $outgoing_iface, which seems to be needed for --local-port to work
221 | ip rule add from "$outgoing_addr" sport "$curl_local_port_min-$curl_local_port_max" dport 443 uidrange "$UID-$UID" lookup main pref 30
222 | ip rule add from "$outgoing_addr" sport "$curl_local_port_min-$curl_local_port_max" dport 1337 uidrange "$UID-$UID" lookup main pref 40
223 | [ "$META_PORT" -ne 443 ] && ip rule add from "$outgoing_addr" sport "$curl_local_port_min-$curl_local_port_max" dport "$META_PORT" uidrange "$UID-$UID" lookup main pref 50
224 | for extserver in $original_extservers; do
225 | [[ "$extserver" =~ "host" ]] && continue
226 | ip rule add to "$extserver" dport 53 uidrange "$UID-$UID" lookup main pref 60
227 | done
228 | [ "$FIREWALL" = "1" ] && firewall_temp_add
229 |
230 | gen_configs
231 |
232 | [ "$FIREWALL" = "1" ] && firewall_temp_remove
233 | for extserver in $original_extservers; do
234 | [[ "$extserver" =~ "host" ]] && continue
235 | ip rule del to "$extserver" dport 53 uidrange "$UID-$UID" lookup main pref 60
236 | done
237 | ip rule del from "$outgoing_addr" sport "$curl_local_port_min-$curl_local_port_max" dport 443 uidrange "$UID-$UID" lookup main pref 30
238 | ip rule del from "$outgoing_addr" sport "$curl_local_port_min-$curl_local_port_max" dport 1337 uidrange "$UID-$UID" lookup main pref 40
239 | [ "$META_PORT" -ne 443 ] && ip rule del from "$outgoing_addr" sport "$curl_local_port_min-$curl_local_port_max" dport "$META_PORT" uidrange "$UID-$UID" lookup main pref 50
240 |
241 | local new_privkey new_endpoint new_peerpubkey new_ip old_peerpubkey old_ip
242 | new_privkey=$(grep 'PrivateKey = ' "$wg_conf" | sed 's/PrivateKey = \(.*\)/\1/')
243 | new_endpoint=$(grep 'Endpoint = ' "$wg_conf" | sed 's/Endpoint = \(.*\)/\1/')
244 | new_peerpubkey=$(grep 'PublicKey = ' "$wg_conf" | sed 's/PublicKey = \(.*\)/\1/')
245 | new_ip=$(grep 'Address = ' "$wg_conf" | sed 's/Address = \(.*\)/\1/')
246 | old_peerpubkey=$(wg show wg0 |grep 'peer: ' | sed 's/peer: \(.*\)/\1/')
247 | old_ip=$(ip -o addr show dev wg0 | awk '$3 == "inet" {print $4}')
248 |
249 | wg set wg0 private-key <(echo "$new_privkey") || fatal_error
250 | wg set wg0 peer "$new_peerpubkey" endpoint "$new_endpoint" allowed-ips "0.0.0.0/0" || fatal_error
251 | if [ "$KEEPALIVE" -gt 0 ]; then
252 | wg set wg0 peer "$new_peerpubkey" persistent-keepalive "$KEEPALIVE" || fatal_error
253 | fi
254 | wg set wg0 peer "$old_peerpubkey" remove || fatal_error
255 | # Adding the new address first before removing the old one seems to be needed
256 | ip -4 address add "$new_ip" dev wg0 || fatal_error
257 | ip -4 address del "$old_ip" dev wg0 || fatal_error
258 | echo "$(date): Updated WireGuard endpoint"
259 | echo; wg show wg0; echo
260 |
261 | [ -n "$POST_RECONNECT" ] && run_command "$POST_RECONNECT"
262 |
263 | [ "$PORT_FORWARDING" = "1" ] && pf_start
264 | now_sleep
265 | elif [ -n "$endpoint_monitor_pid" ] && ! kill -0 "$endpoint_monitor_pid" 2> /dev/null && [ "$result" -eq 0 ]; then
266 | unset endpoint_monitor_pid
267 | fi
268 | done
269 |
270 | sleep infinity &
271 | wait $!
272 | }
273 |
274 | # An error with no recovery logic occured. Either go to sleep or exit.
275 | fatal_error () {
276 | echo "$(date): Fatal error"
277 | [ "$firewall_temp_state" = "1" ] && firewall_temp_remove
278 | [ -n "$FATAL_SCRIPT" ] && run_command "$FATAL_SCRIPT"
279 | [ "$EXIT_ON_FATAL" -eq 1 ] && exit 1
280 | sleep infinity &
281 | wait $!
282 | }
283 |
284 | run_command () {
285 | echo "$(date): Running: $1"
286 | eval "$1"
287 | }
288 |
289 | gen_wgconf () {
290 | /scripts/wg-gen.sh -l "$1" -t "$tokenfile" -o "$wg_conf" -k "/RegionsListPubKey.pem" -d "$VPNDNS" -m "$MTU" -c "$pia_cacrt"
291 | return $?
292 | }
293 |
294 | # Get a new auth token
295 | # Unsure how long an auth token will remain valid
296 | get_auth_token () {
297 | [ -r "$USER_FILE" ] && echo "$(date): Reading username from $USER_FILE" && USER=$(<"$USER_FILE")
298 | [ -r "$PASS_FILE" ] && echo "$(date): Reading password from $PASS_FILE" && PASS=$(<"$PASS_FILE")
299 | [ -z "$PASS" ] && echo "$(date): PIA password not set. Unable to retrieve new auth token." && fatal_error
300 | [ -z "$USER" ] && echo "$(date): PIA username not set. Unable to retrieve new auth token." && fatal_error
301 | echo "$(date): Generating auth token"
302 | local token
303 | if ! token=$(/scripts/pia-auth.sh -u "$USER" -p "$PASS" -n "$META_CN" -i "$META_IP" -o "$META_PORT" -c "$pia_cacrt"); then
304 | echo "$(date): Failed to acquire new auth token" && fatal_error
305 | fi
306 | echo "$token" > "$tokenfile"
307 | chmod 600 "$tokenfile"
308 | }
309 |
310 | get_outgoing_iface ()
311 | {
312 | ip route get "$1" | head -1 | sed 's/.*dev \([^ ]*\) .*/\1/'
313 | }
314 |
315 | if [[ $(getpcaps 0) == "0: =ep" ]]; then
316 | echo "$(date): The container appears to be running with elevated privileges. The container generally only requires the NET_ADMIN capability to run."
317 | elif ! getpcaps 0 | grep cap_net_admin >& /dev/null; then
318 | echo "$(date): The container requires the NET_ADMIN capability to run. See the README for more info."
319 | fatal_error
320 | fi
321 |
322 | echo "$(date): Container build info: $BUILDINFO"
323 |
324 | nftables_setup
325 |
326 | [ -x "$pre_up_script" ] && run_command "$pre_up_script"
327 | [ -n "$PRE_UP" ] && run_command "$PRE_UP"
328 |
329 | # Explicitly set both interface and dns servers to use so we can run setup requests again bypassing the tunnel if needed once connected
330 | # Setting the local port also allows more specific temporary firewall rules
331 | # Re-using a single local port for multiple requests fails so a range is used instead: https://github.com/curl/curl/issues/6288
332 | # Setting --interface to an address rather than a name seems to be needed for --local-port to work
333 | # ExtServers contains dns servers used by docker's internal resolver, however this is only available from Docker v26.0
334 | original_nameserver=$(grep -im 1 '^nameserver' /etc/resolv.conf |cut -F2)
335 | original_extservers=$(grep '# ExtServers:' /etc/resolv.conf | sed 's/# ExtServers: \[\(.*\)\]/\1/')
336 | outgoing_iface=$(ip -4 route show default | awk '/default/ {print $5}' | head -1)
337 | outgoing_iface_dns=$(get_outgoing_iface "$original_nameserver")
338 | outgoing_addr=$(ip -4 -o addr show "$outgoing_iface" | awk '{print $4}' | cut -d "/" -f 1)
339 | curl_local_port_min=12300
340 | curl_local_port_max=12310
341 | export CURL_OVERRIDE_PARAMS="--interface $outgoing_addr --dns-interface $outgoing_iface_dns --dns-servers $original_nameserver --local-port $curl_local_port_min-$curl_local_port_max"
342 | echo "$(date): curl options: $CURL_OVERRIDE_PARAMS"
343 |
344 | [ "$FIREWALL" -eq 1 ] && firewall_init
345 |
346 | # Remove previous forwarded port number dump file if requested and present
347 | [ "$PORT_FILE_CLEANUP" -eq 1 ] && [ -w "$portfile" ] && rm "$portfile"
348 |
349 | # LOC is ignored and may be blank if ip/cn/port override vars or a dedicated ip are used
350 | [ -n "$PIA_CN" ] && [ -n "$PIA_IP" ] && [ -n "$PIA_PORT" ] && LOC="manual"
351 | [ -n "$PIA_DIP_TOKEN" ] && LOC="dip"
352 |
353 | # No LOC or specific ip/port/cn supplied
354 | [ -z "$LOC" ] && /scripts/wg-gen.sh -a && fatal_error
355 |
356 | [ ! -r "$tokenfile" ] && get_auth_token
357 |
358 | # Generate wg0.conf
359 | # LOC can be a single location id, or a space or comma separated list
360 | # Multiple location ids are used as fallback if the initial registration fails
361 | gen_configs() {
362 | gen_success=0
363 | for location in ${LOC//,/ }; do
364 | gen_wgconf "$location"
365 | local result=$?
366 | if [ "$result" -eq 2 ]; then
367 | # Reauth and retry if auth failed
368 | # An auth error implies that the location id is valid and the endpoint responsive
369 | rm "$tokenfile"
370 | get_auth_token
371 | gen_wgconf "$location" || fatal_error
372 | elif [ "$result" -eq 3 ]; then
373 | # Location not found
374 | echo "$(date): Location $location not found"
375 | continue
376 | elif [ "$result" -eq 4 ]; then
377 | # Registration failed
378 | echo "$(date): Registration failed"
379 | continue
380 | elif [ "$result" -ne 0 ]; then
381 | echo "$(date): Failed to generate WireGuard config"
382 | fatal_error
383 | fi
384 | gen_success=1
385 | break
386 | done
387 |
388 | if [ "$gen_success" -eq 0 ]; then
389 | echo "$(date): Failed to generate WireGuard config for the selected location/s: $LOC"
390 | fatal_error
391 | fi
392 | }
393 |
394 | gen_configs
395 |
396 | [ "$FIREWALL" -eq 1 ] && firewall_temp_remove
397 |
398 | # Add PersistentKeepalive if KEEPALIVE is set
399 | [ "$KEEPALIVE" -gt 0 ] && echo "PersistentKeepalive = $KEEPALIVE" >> "$wg_conf"
400 |
401 | # Bring up Wireguard interface
402 | echo "$(date): Bringing up WireGuard interface wg0"
403 | wg-quick up wg0 || fatal_error
404 |
405 | # Print out wg interface info
406 | echo
407 | wg
408 | echo
409 |
410 | echo "$(date): WireGuard successfully started"
411 |
412 | # Show a warning if src_valid_mark=1 needs setting, otherwise incoming packets will be dropped
413 | effective_rp_filter="$(sysctl -n net.ipv4.conf.all.rp_filter)"
414 | [ "$(sysctl -n net.ipv4.conf.default.rp_filter)" -gt "$effective_rp_filter" ] && effective_rp_filter="$(sysctl -n net.ipv4.conf.default.rp_filter)"
415 | [ "$effective_rp_filter" -eq 1 ] && [ "$(sysctl -n net.ipv4.conf.all.src_valid_mark)" -ne 1 ] && \
416 | echo "$(date): Warning: Container requires net.ipv4.conf.all.src_valid_mark=1 sysctl to be set when rp_filter is set to strict. See the README for more info."
417 |
418 | # Add qdisc to wg0 if requested
419 | # eg: QDISC=cake bandwidth 20Mbit
420 | # shellcheck disable=SC2086
421 | [ -n "$QDISC" ] && echo "$(date): Adding qdisc to wg0: $QDISC" && tc qdisc add root dev wg0 $QDISC && tc -statistics qdisc show dev wg0
422 |
423 | if [ "$FIREWALL" -eq 1 ]; then
424 | # Allow docker network input/output
425 | for iface in /sys/class/net/*; do
426 | iface="${iface##*/}"
427 | [[ "$iface" = @(lo|wg0) ]] && continue
428 | docker_network="$(ip -o addr show dev "$iface"|
429 | awk '$3 == "inet" {print $4}')"
430 | [ -z "$docker_network" ] && continue
431 | echo "$(date): Allowing network access to $docker_network on $iface"
432 | iptables -A OUTPUT -o "$iface" --destination "$docker_network" -j ACCEPT
433 | iptables -A INPUT -i "$iface" --source "$docker_network" -j ACCEPT
434 | done
435 |
436 | # Allow WG stuff
437 | iptables -A OUTPUT -o wg0 -j ACCEPT
438 | iptables -A OUTPUT -m mark --mark "$(wg show wg0 fwmark)" -j ACCEPT
439 |
440 | echo "$(date): Firewall enabled: Blocking non-WireGuard traffic"
441 | fi
442 |
443 | # Set env var LOCAL_NETWORK=192.168.1.0/24 to allow LAN input/output
444 | # Accept comma separated as well as space separated list
445 | if [ -n "$LOCAL_NETWORK" ]; then
446 | gaddr=$(ip -4 route show default | awk '/default/ {print $3}' | head -1)
447 | [ -z "$VPNDNS" ] && pia_dns=$(grep 'DNS = ' "$wg_conf" | sed 's/DNS = \(.*\)/\1/')
448 | for range in ${LOCAL_NETWORK//,/ }; do
449 | if [ -n "$pia_dns" ]; then
450 | grepcidr "$range" <(echo "$pia_dns") >/dev/null && \
451 | echo "$(date): Warning: LOCAL_NETWORK range $range overlaps with PIA's default dns servers ($pia_dns)" && \
452 | echo "$(date): Consider setting custom dns servers using the VPNDNS env var if there are name resolution issues"
453 | fi
454 | if [ "$FIREWALL" -eq 1 ]; then
455 | echo "$(date): Allowing network access to $range on $outgoing_iface"
456 | iptables -A OUTPUT -o "$outgoing_iface" --destination "$range" -j ACCEPT
457 | iptables -A INPUT -i "$outgoing_iface" --source "$range" -j ACCEPT
458 | fi
459 | echo "$(date): Adding route to $range"
460 | ip route add "$range" via "$gaddr"
461 | done
462 | fi
463 |
464 | # Nat+forward traffic from a specific interface if requested
465 | # eg. FWD_IFACE=eth1
466 | if [ -n "$FWD_IFACE" ]; then
467 | iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE
468 | iptables -A FORWARD -i wg0 -o "$FWD_IFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT
469 | iptables -A FORWARD -i "$FWD_IFACE" -o wg0 -j ACCEPT
470 | echo "$(date): Forwarding traffic from $FWD_IFACE to VPN"
471 | fi
472 |
473 | # Setup port forwarding if requested and available
474 | pf_start () {
475 | [ "$PORT_FORWARDING" -eq 1 ] && pkill -f 'pf.sh'
476 | pf_api_ip=$(grep '#pf api' "$wg_conf"| sed 's/#pf api ip: \(.*\)/\1/')
477 | pf_cn=$(grep '#cn: ' "$wg_conf"| sed 's/#cn: \(.*\)/\1/')
478 | if [ "$PORT_FORWARDING" -eq 1 ] && [ -n "$pf_api_ip" ]; then
479 | echo "$(date): Starting port forward script"
480 | # Try to use a persistent port if requested
481 | if [ "$PORT_PERSIST" -eq 1 ]; then
482 | /scripts/pf.sh -t "$tokenfile" -i "$pf_api_ip" -n "$pf_cn" -p "$portfile" -c "$pia_cacrt" -s "/scripts/pf_success.sh" -r "$pf_persistfile" -f wg0 &
483 | else
484 | /scripts/pf.sh -t "$tokenfile" -i "$pf_api_ip" -n "$pf_cn" -p "$portfile" -c "$pia_cacrt" -s "/scripts/pf_success.sh" -f wg0 &
485 | fi
486 | pf_pid=$!
487 | elif [ "$PORT_FORWARDING" -eq 1 ] && [ -z "$pf_api_ip" ]; then
488 | echo "$(date): Warning: Port forwarding is unavailable on this server. Try a different location."
489 | fi
490 | }
491 |
492 | [ "$PORT_FORWARDING" -eq 1 ] && pf_start
493 |
494 | [ -x "$post_up_script" ] && run_command "$post_up_script"
495 | [ -n "$POST_UP" ] && run_command "$POST_UP"
496 |
497 | # Workaround a NAT bug when using Wireguard behind a particular Asus router by regularly changing the local port
498 | # Set env var CYCLE_PORTS to a space-separated list of ports to cycle through
499 | # Eg: CYCLE_PORTS=50001 50002 50003
500 | # Optionally set CYCLE_INTERVAL to number of seconds to use each port for. Defaults to 180 (3mins)
501 | if [ -n "$CYCLE_PORTS" ]; then
502 | echo "$(date): Changing Wireguard's local port every ${CYCLE_INTERVAL:-180}s"
503 | while true; do
504 | for port in $CYCLE_PORTS; do
505 | wg set wg0 listen-port "$port"
506 | sleep "${CYCLE_INTERVAL:-180}" & wait $!
507 | done
508 | done
509 | fi
510 |
511 | now_sleep
512 |
--------------------------------------------------------------------------------