A suite to assist establishing a wireguard tunnel to PIA, a VPN service provider, under a systemd-networkd Linux network stack using PIA's new REST API.
Also includes pia-portforward
, a utility for setting up, activating, and
maintaining a forwarded port. This utility can also notify rtorrent and/or
transmission of the port number using their disinct RPC interfaces,
which will allow them to receive incoming peer requests.
According to their documentation, PIA requires you to refresh the port
forwarding assignment every few minutes. The pia-portforward
utility can do
this with the --refresh
flag.
One final utility, pia-listregions
, will show you, on your terminal, a
ranked (by ping time) list of PIA servers, with some possibly-salient info
about them, with the goal of helping you choose the 'best' region to connect to
if you are otherwise indifferent about their geography.
This code is only lightly tested with my own private set up. It may or may not work for you, and if it does, some assembly will be required by you. PRs are welcome, especially if they would cover any more general use cases.
Private Internet Access is, IMHO, a solid VPN provider.1 In their latest generation infrastructure, they designed a system for use by their mobile and desktop apps, which allow wireguard tunnels with randomly generated, frequently rotated private keys. Unfortunately their Linux app, though I haven't tried it, doesn't feel right for my use case, wherein I run the VPN on my firewall, a Linux box running systemd-networkd, in the manner described below. Luckily, however, PIA published their REST API, which is pretty simple to use; powered by that, we can dynamically configure our systemd-networkd borne VPN tunnel and routing rules with this tool suite.
There are a couple of ways to use a VPN. One way is to set it up on your workstation, laptop, or mobile device, so that your traffic is encrypted on your device, through your lan and then the internet to the VPN service provider. This is "end to edge" encryption, and is the best protection a VPN can provide. For most users, this is easy and affords plenty of protection, and for that PIA has nice desktop and mobile apps that are simple to use, but do require installing them and setting them up. PIA are putting out apps for more and more devices, but, unfortunately, they can't keep up with all the IoT devices, smart home hubs, smart TV's, etc. As a result, we have to live with some unencrypted traffic, and this segment is growing very fast in terms of number of devices and the number of security issues arising from them.
Another way to use a VPN, available to you only if you control your network
infrastructure, is to set up your LAN's firewall to connect to the VPN service
and route all (or some selection) of the outgoing LAN traffic through the VPN.
This way, any devices on the LAN can benefit from the VPN without configuring
those devices at all. Note that this is "edge to edge" encryption, which is
a bit weaker since there is part of the path, the first part on your LAN, where
traffic is unencrypted. I'm okay with this, because I have physical control of
all apparatus from "end to edge". If you use any one else's wifi access point, or
connect your access point to someone else's switch, this would not be the case for
you. Stick with the VPN app. This project is for people who, like me, are
okay with this depth-for-breadth trade off, and who also use systemd-networkd
on their firewall.2
PIA also has a dynamic port forwarding feature that allows you to run a server on or behind your VPN endpoint. This suite can help you with that.
Because of the dynamic nature of your enpoint public and virtual IP addresses, Wireguard keys, and forwarded ipv4 port, it requires some tooling to set up. Read on for how.
Tool | Description |
---|---|
piawgcli | Similar approach, but generates a wireguard-cli configuration file instead of systemd-networkd |
Official | Bash scripts that accomplish a similar goal using plain-jane networking commands, like ip , wg-quick and so on. Also has a nice list of links to other alternatives. |
go install github.com/jdelkins/pia-tools/cmd/pia-setup-tunnel@latest
go install github.com/jdelkins/pia-tools/cmd/pia-portforward@latest # optional
go install github.com/jdelkins/pia-tools/cmd/pia-listregions@latest # optional
-
Set up
/etc/systemd/network/<interface>.netdev.tmpl
and/etc/systemd/network/<interface>.network.tmpl
template files. These templates use the Go packagetext/template
to replace tokens with data received from PIA when requesting the tunnel to be set up. For example:/etc/systemd/network/<interface>.netdev.tmpl
[NetDev] Name={{ .Interface }} Kind=wireguard [WireGuard] PrivateKey={{ .PrivateKey }} [WireGuardPeer] PublicKey={{ .ServerPubkey }} AllowedIPs=0.0.0.0/0 Endpoint={{ .ServerIp }}:{{ .ServerPort }} PersistentKeepalive=25
/etc/systemd/network/<interface>.network.tmpl
{{- $if := .Interface -}} {{- $gw := .ServerVip -}} [Match] Name={{ $if }} Type=wireguard [Network] Address={{ .PeerIp }}/32 {{- range .DnsServers }} DNS={{ . }}%{{ $if }} {{- end }} [Route] Destination=0.0.0.0/0 Gateway={{ $gw }} GatewayOnLink=yes [Route] Destination={{ $gw }}/32 Scope=link {{ range .DnsServers -}} [Route] Destination={{ . }}/32 Gateway={{ $gw }} {{ end -}}
The above examples should be pretty self-explanatory; if not, you should read up on
systemd-networkd
and/ortext/template
. For info on what other template fields are available (though the above examples demonstrate (I think) all of the useful ones), check out the Tunnel struct in thepia
package. The template processing package includes sprig, which provides a number of additional template functions, should they come in handy. -
mkdir /var/cache/pia
and set the directory permissions as restrictive as you can, probablyroot:root
and mode0700
. This directory will hold.json
files to cache information including your personal access tokens and wireguard private keys. These files do not store your PIA username or password, but should still be treated as private. -
Run
pia-setup-tunnel --username <user> --password <pass> --ifname <interface>
as root to create the.network
and.netdev
files corresponding to the templates created above. -
Inspect the generated files. If everything looks okay, run
systemctl restart systemd-networkd
as root to reload your network stack and activate the tunnel.
That's it, you should have a VPN as your ipv4 default route (asssuming you
configured the .network
file as such). Use something like curl -4 icanhazip.com
to verify that the ip address is coming from PIA. Every few days
or weeks, repeat steps 3 and 4 to establish a new tunnel. They are good for
some period of time, but I recommend replacing it once a week for privacy
reasons.
If you also want incoming traffic on a single TCP port and a single UDP port (both with the same port number) to be forwarded to your VPN endpoint, then follow the procedure below.
Note ☞ The following steps are not necessary if you are simply setting up a tunnel on a gateway. This tool is to enable running a server behind (or on) the gateway that accepts incoming connections to be accessible through the VPN.
-
Make sure your wireguard tunnel to PIA is up and running per the above procedure. The following step presumes that you have a working route to PIA's Wireguard endpoint virtual IP configured and working. It also assumes that, as part of running the above procedure, the
pia-setup-tunnel
tool saved some information in the file/var/cache/pia/<interface>.json
. We will read that file and add to it as part of the next step. -
For bittorrent users: the thing with bittorrent, like ftp, is that some layer 3 info (the port number by which your server is reachable) is communicated in the layer 7 protocol (announce messages). This layer transgression requires communicating some layer 3 info to your bittorrent server using some facility provided by the server. Most popular bittorrent servers that I've looked into have an RPC interface, which may need to be configured/enabled (see below).
The
pia-portforward
tool can help communicating this port number to two popular bittorrent apps, namely rtorrent and transmission. (I haven't gotten arround to implementing a similar feature for qbittorrent, the single most popular bittorrent server on seedboxes, because I personally don't use it and no one has asked, but I believe the RPC capability is there and it should be be simple to add.)If you are running rtorrent or transmission as the server behind the gateway, run, respectively,
pia-portforward --ifname <interface> --rtorrent http://<rtorrent-ip>:<rtorrent-port>
orpia-portforward --ifname <interface> --transmission <transmission-ip>
. This will request a forwarding port from PIA, activate it, and then inform rtorrent or transmission, respectively, about the port.-
In the case of rtorrent, you may have to configure your instance to accept XML-RPC queries on the
/RPC2
endpoint. Note ☞ Don't include the/RPC2
URL component in the--rtorrent
parameter, as this is added automatically. -
In the case of transmission, if your instance is configured with a username and password, provide those with the
--transmission-username
and--transmission-password
parameters
-
-
If you don't have a bittorrent server running (or just don't want to use the forwarded port for that), then just leave off the
--rtorrent
and--transmission
options. You're on your own to parse/var/cache/pia/<interface>.json
to obtain the assigned port and do something with it, such as setting up a DNAT firewall rule. Most protocols (http, for example) don't require the server to know the port number. (The server process has to listen to a configured port, but, with nat firewalls, this doesn't have to be the port that the client connects to). Such traffic can be handled cleanly on the firewall by dnatting traffic delivered to PIA's forwarded port to a static server port, locally on the firewall or elsewhere on the LAN side. You could, for example:nft add chain inet filter pia_portfoward '{type nat hook prerouting priority -100; policy accept}' nft flush chain inet filter pia_portforward nft add rule inet filter pia_portforward tcp dport $(jq .PFSig.port /var/cache/pia/pia.json) dnat ip to 192.168.0.80:80
...which would create a nftables rule to forward incoming traffic on your assigned port (retrieved from the cache file by the
$(jq .PFSig.port /var/cache/pia/pia.json)
part) to an internal webserver running on192.168.0.80
at port 80. This example assumes your VPN wireguard interface is namedpia
.Note ☞ The example above sets up the mechanism for forwarding connections. Assuming you don't run a default-accept firewall (hopefully you don't), then to accually permit such connections, you would also have to also add a rule to accept them, typically somewhere in the
forward
chain in nftables. This is not a lesson in firewall design, so again, you're on your own: the possibilies are numerous once you can get the forwarded port number. -
Every 15 minutes or so (using systemd timers or similar), run
pia-portforward --ifname <interface> --refresh
in order to refresh the port forwarding assignment using your cached authentication token. If you don't do this, PIA will eventually reclaim the port for another customer, and traffic meant for you could be delivered somewhere else! You don't want that. My testing suggests that PIA is pretty conservative in this area, so I think most reasonable failure cases (e.g. your ISP goes down for a few hours, so you can't refresh) should be fine, but you definitely want to keep refreshing the assignment as long as you're intending to use it.
The pia-listregions
tool attempts to send an "unprivileged" ping via UDP. If this is not permitted by default on your system,
run the binary as root or else it must be enabled with a sysctl command like the following:
sudo sysctl -w net.ipv4.ping_group_range="0 2147483647"
Footnotes
-
I suspect it would be trivial to adapt this tool suite to another wireguard-affine network stack, especially if it is configurable with text files. Heck, the official wireguard client is written in go if I understand, so you could probably, almost as easily, directly set up the tunnels with library calls. I'm not likely to write that code though since I am happy with
systemd-networkd
and I like having as much of my networking configuration handled by it as possible. Check out the Alternatives though if you have a different networking preference. ↩