- LAN, private and Tailscale networks remain accessible and are not routed over VPN. No special configuration required.
- Supports split horizon DNS automatically, if
systemd-resolved
is in use. - Supports running as systemd unit (natively and as podman container)
- Supports roaming clients
Note
For old OpenVPN based container's documentation, See here.
Warning
gVisor runtime is NOT supported!
Images are published at ghcr.io/tprasadtp/protonwire.
-
If using Debian 11 (Buster) or later, Raspberry Pi OS (Buster) or later, Fedora, ArchLinux, Linux Mint 20.x or later, RHEL 9 or later, Alma Linux 9 or later, CentOS 9 Stream, Ubuntu 20.04 or later have the required kernel module built-in.
-
Kernel versions 5.6 or later.
-
If NONE of the above conditions can be satisfied, install WireGuard. Your distribution might already package DKMS module or provide signed kernels with WireGuard built-in. Visit https://www.wireguard.com/install/ for more info.
Note
If running as a container, Wireguard MUST be installed on the host, not the container.
-
To check current kernel version run,
uname -r
- Log in to ProtonVPN and go to Downloads → WireGuard configuration.
- Enter a name for the key, and select features to enable like NetShield and VPN Accelerator & click create.
- Generated config might look something like below,
[Interface] # Key for <name> # VPN Accelerator = on PrivateKey = KLjfIMiuxPskM4+DaSUDmL2uSIYKJ9Wap+CHvs0Lfkw= Address = 10.2.0.2/32 DNS = 10.2.0.1 [Peer] # NL-FREE#128 PublicKey = jbTC1lYeHxiz1LNSJHQMKDTq6sHgcWxkBwXvt7GWo1E= AllowedIPs = 0.0.0.0/0 Endpoint = 91.229.23.180:51820
- Only thing needed from the above config is
PrivateKey
. - See https://protonvpn.com/support/wireguard-configurations/ for more info.
- CLI arguments will always take precedence over environment variables.
- Environment variables takes precedence over any config file.
- If private key is not specified via CLI or environment variable, it is searched
in following locations.
/etc/protonwire/private-key
/run/secrets/protonwire-private-key
/run/secrets/protonwire/private-key
${CREDENTIALS_DIRECTORY}/private-key
(Only if$CREDENTIALS_DIRECTORY
is set)
Name | Default/Required | Description |
---|---|---|
PROTONVPN_SERVER |
REQUIRED | (String) ProtonVPN server to connect to. |
WIREGUARD_PRIVATE_KEY |
- | (String) Wireguard Private key |
IPCHECK_URL |
https://protonwire-api.vercel.app/v1/client/ip | (String) URL to check client IP. |
IPCHECK_INTERVAL |
60 |
(Integer) Interval between internal health-checks in seconds. Set this to 0 to disable IP checks. |
SKIP_DNS_CONFIG |
false | (Boolean) Set this to 1 or true to skip configuring DNS. |
KILL_SWITCH |
false | (Boolean) Enable KillSwitch (Experimental and can cause issues) |
Warning
Environment variables starting with
__PROTONWIRE
are reserved for internal use.
This should be server name like NL#1
(or NL-1
) or domain name like,
node-nl-01.protonvpn.net
(recommended). Automatic server selection
by setting PROTONVPN_SERVER
to P2P
, RANDOM
and other methods are
NOT SUPPORTED. See this for more info.
Warning
- Script cannot validate if specified server is available under your plan. It is user's responsibility to ensure that server specified is available under your subscription and supports required features, like P2P, Streaming etc. Use
--p2p
,--streaming
,--secure-core
flags to enable client side validations.
Warning
This feature is experimental and is NOT covered by semver compatibility guarantees.
Kill-Switch is not a hard kill-switch but more of an "internet" kill-switch. LAN addresses, Link-Local addresses and CGNAT(also Tailscale) addresses remain reachable. Unlike most VPN containers, kill-switch is implemented via routing policies, routing priorities and custom route tables rather than firewall rules.
- Kill-switch WILL NOT be disabled during reconnects.
- Kill-switch WILL NOT be disabled when running
protonwire disconnect
unless--kill-switch
flag is ALSO specified. - Kill-switch is NOT reliable when upgrading the protonwire package. This is because binary itself may change during upgrade and it might include breaking changes. This only applies to native packages as containers are immutable and re-created during upgrades.
- Using kill-switch with systemd unit AND using
protonwire
to manually disable kill-switch will lead to kill-switch being re-created during service restarts.
ProtonVPN WireGuard Client Usage: protonwire [OPTIONS...] or: protonwire [OPTIONS...] c|connect [SERVER] or: protonwire [OPTIONS...] d|disconnect or: protonwire [OPTIONS...] check or: protonwire [OPTIONS...] disable-killswitch or: protonwire [OPTIONS...] help Options: -k, --private-key FILE|KEY Wireguard private key or file containing private key --container Run as container (Cannot be used with --systemd) --systemd Run as systemd service (Cannot be used with --container) --metadata-url URL Server metadata endpoint URL --check-interval INT IP check interval in seconds (default 60) --check-url URL IP check endpoint URL --skip-dns-config Skip configuring DNS. (Useful for Kubernetes and Nomad) --kill-switch Enable killswitch (Experimental) --p2p Verify if specified server supports P2P --streaming Verify if specified server supports streaming --tor Verify if specified server supports Tor --secure-core Verify if specified server supports secure core -q, --quiet Show only errors -v, --verbose Show debug logs -h, --help Display this help and exit --version Display version and exit Examples: protonwire connect nl-1 Connect to server nl-1 protonwire d --kill-switch Disconnect from current server and disable kill-switch protonwire verify [SERVER] Check if connected to a server Files: /etc/protonwire/private-key WireGuard private key Environment: WIREGUARD_PRIVATE_KEY WireGuard private key or file PROTONVPN_SERVER ProtonVPN server name IPCHECK_INTERVAL Custom IP check interval in seconds (default 60) IPCHECK_URL IP check endpoint URL (must be secure) SKIP_DNS_CONFIG Set to '1' to skip configuring DNS KILL_SWITCH Set to '1' to enable killswitch (Experimental) DEBUG Set to '1' to enable debug logs
- Script supports
healthcheck
command. By default, when running as a service script will keep checking everyIPCHECK_INTERVAL
(default=60) seconds using theIPCHECK_URL
api endpoint. To disable healthchecks entirely setIPCHECK_INTERVAL
to0
- Use
protonwire healthcheck --silent --container
as theHEALTHCHECK
command. Same can be used as liveness probe and readiness probe for Kubernetes.
-
Pull docker image (if required)
docker pull ghcr.io/tprasadtp/protonwire:latest
-
Run the VPN container. Assuming that a container which needs to be routed via VPN, listening on container port
80
and you wish to map it to host port8000
,docker run \ -it \ --rm \ --init \ --publish 8000:80 \ --name protonwire \ --cap-add NET_ADMIN \ --env PROTONVPN_SERVER=<server-name-or-dns> \ --sysctl net.ipv4.conf.all.rp_filter=2 \ --mount type=tmpfs,dst=/tmp \ --mount type=bind,src=<absolute-path-to-key-file>,dst=/etc/protonwire/private-key,readonly \ ghcr.io/tprasadtp/protonwire:latest
Warning
- To publish additional ports from other containers using this VPN, it MUST be done
on the
protonwire
container! --sysctl
and--cap-add
flags are important! without these, container cannot create or manage WireGuard interfaces or routing.- docker rootless should also work just fine for most users, but is considered experimental.
- To publish additional ports from other containers using this VPN, it MUST be done
on the
-
To use VPN in other container(s), use
--net=container:protonwire
flag. For example, we can run caddy to proxyhttps://ip.me/
via VPN. Visiting http://localhost:8000, orcurl http://localhost:8000
should show VPN's country and IP address.docker run \ -it \ --rm \ --net=container:protonwire \ --name=protonwire-demo \ caddy:latest \ caddy reverse-proxy \ --change-host-header \ --from :80 \ --to https://ip.me:443
Note
There are no port mappings done here! It should be done on the VPN container!
If entire stack is in a single compose file, then network_mode: service:protonwire
on the services which should be routed via VPN. If the VPN stack is NOT in same compose file use network_mode: container:<protonwire-container-name>
.
As an example, run caddy web-server, proxying https://ip.me, via VPN using the compose config given below. Once the stack is up, visiting the http://localhost:8000, or curl -s http://localhost:8000
should show VPN's country and IP address.
version: '2.3'
services:
protonwire:
container_name: protonwire
# Use semver tags or sha256 hashes of manifests.
# using latest tag can lead to issues when used with
# automatic image updaters like watchtower.
image: ghcr.io/tprasadtp/protonwire:latest
init: true
restart: unless-stopped
environment:
# Quote this value as server name can contain '#'.
PROTONVPN_SERVER: "nl-free-127.protonvpn.net"
# Set this to 1 to show debug logs for issue forms.
DEBUG: "1"
# Set this to 1 to enable kill-switch.
KILL_SWITCH: "1"
# NET_ADMIN capability is mandatory!
cap_add:
- NET_ADMIN
# sysctl net.ipv4.conf.all.rp_filter is mandatory!
# net.ipv6.conf.all.disable_ipv6 disables IPv6 as protonVPN does not support IPv6.
# 'net.*' sysctls are not required on application containers,
# as they share network stack with protonwire container.
sysctls:
net.ipv4.conf.all.rp_filter: 2
net.ipv6.conf.all.disable_ipv6: 1
volumes:
- type: tmpfs
target: /tmp
- type: bind
source: private.key
target: /etc/protonwire/private-key
read_only: true
ports:
- 8000:80
# This is sample application which will be routed over VPN
# Replace this with your preferred application(s).
caddy_proxy:
image: caddy:latest
network_mode: service:protonwire
command: |
caddy reverse-proxy \
--change-host-header \
--from :80 \
--to https://ip.me:443
Note
- It is essential to expose/publish port(s) on protonwire container, instead of application container.
- SHOULD NOT run the container as privileged. Adding capability
CAP_NET_ADMIN
AND definedsysctls
should be sufficient.- Value for
PROTONVPN_SERVER
must be enclosed within quotes as server name can contain '#'
Warning
- podman versions older than v4 (one included in Debian 11/Ubuntu 22.04 repos) are NOT supported. It might be possible to use it to run the container, but some flags and commands are missing(
--sysctl
flag on pod create,podman secret
commands etc.) and thus cannot be supported, but may be used without those features.
-
Create a podman secret for private key
sudo podman secret create protonwire-private-key <PRIVATE_KEY>
-
Run protonwire container
sudo podman run \ -it \ --rm \ --name protonwire \ --init \ --tz local \ --tmpfs /tmp \ --cap-add=NET_ADMIN \ --sysctl=net.ipv4.conf.all.rp_filter=2 \ --secret private-key,mode=600 \ --env PROTONVPN_SERVER=<SERVER-NAME> \ --publish 8000:80 \ ghcr.io/tprasadtp/protonwire:latest
Warning
- To publish additional ports from other containers using this VPN (usually done via argument
-p host_port:container_port
), it MUST be done on theprotonwire
container! --sysctl
and--cap-add
flags are important! without these, container cannot create/manage WireGuard interface.mode=600
in secret mounting is important as script refuses to use private key with insecure permissions.- podman rootless should also work just fine for most users, but is considered experimental.
- To publish additional ports from other containers using this VPN (usually done via argument
-
To use VPN in other container(s), use
--net=container:protonwire
flag. For example, we can run caddy to proxyhttps://ip.me
via VPN. Visiting http://localhost:8000, orcurl http://localhost:8000
should show VPN's country and IP address.sudo podman run \ -it \ --rm \ --name=protonwire-demo \ --net=container:protonwire \ caddy:latest \ caddy reverse-proxy \ --change-host-header \ --from :80 \ --to https://ip.me:443
podman-generate-systemd can be used to generate systemd unit files for the entire pod.
Following dependencies are in addition to WireGuard support in Kernel. See https://www.wireguard.com/install/ for more info. This step is only required if running as systemd unit outside of containers.
-
If running on Ubuntu, Linux Mint, Elementary OS and other Ubuntu based derivatives etc.
- If using
systemd-resolved
(default),sudo apt-get install curl jq procps iproute2 libcap2-bin policykit-1 util-linux wireguard-tools
- Otherwise,
sudo apt-get install curl jq procps iproute2 libcap2-bin policykit-1 util-linux wireguard-tools openresolv
- If using
-
If running on Debian, Raspberry Pi OS, and other Debian based derivatives etc
- If using
systemd-resolved
(NOT default),sudo apt-get install curl jq procps iproute2 libcap2-bin policykit-1 util-linux wireguard-tools
- Otherwise,
sudo apt-get install curl jq procps iproute2 libcap2-bin policykit-1 wireguard-tools openresolv
- If using
-
If running on CentOS-Stream, Fedora 34+, Amazon Linux 2022, RHEL 9, Rocky Linux 9, Alma Linux 9
- If using
systemd-resolved
(default),sudo dnf install curl jq procps-ng libcap iproute polkit util-linux wireguard-tools
- Otherwise,
sudo dnf install curl jq procps-ng libcap iproute polkit util-linux wireguard-tools openresolv
- If using
-
If running on CentOS 8, RHEL 8, Rocky Linux 8, Alma Linux 8
-
If using
systemd-resolved
(NOT default),sudo dnf install curl jq procps-ng libcap iproute polkit util-linux wireguard-tools
-
Otherwise,
sudo dnf install curl jq procps-ng libcap iproute polkit util-linux wireguard-tools openresolv
-
- Install DEB or RPM packages from releases.
- Alternatively, clone this repository and run
sudo make install
- To connect to a server,
sudo protonwire -k <KEY_FILE> connect <SERVER>
- To disconnect from server
sudo protonwire disconnect
- To check/verify connection
sudo protonwire check
Note
Add
--debug
flag to see debug logs.
Provides rich systemd integration. Connected server kill-switch state is displayed with
systemctl status protonwire
.
vagrant@debian-minimal:~$ systemctl status protonwire --no-pager ● protonwire.service - ProtonVPN Wireguard Client Loaded: loaded (/usr/local/lib/systemd/system/protonwire.service; disabled; vendor preset: enabled) Active: active (running) since Wed 2023-04-12 21:17:31 UTC; 2min 47s ago Docs: man:protonwire(1) https://github.com/tprasadtp/protonvpn-docker Main PID: 7298 (protonwire) Status: "Connected to nl-free-127.protonvpn.net (via 185.107.56.83, with KillSwitch)" IP: 36.4K in, 11.6K out Tasks: 2 (limit: 2336) Memory: 2.3M CPU: 2.302s CGroup: /system.slice/protonwire.service ├─7298 /bin/bash /usr/local/bin/protonwire connect --systemd --logfmt journald └─8236 sleep 60
- MUST have
CAP_NET_ADMIN
capability - MUST set
NotifyAccess
toall
- MUST have access to
org.freedesktop.resolve1.*
, if usingsystemd-resolved
. - MUST NOT use
DynamicUser
. See systemd/systemd#22737
-
By default unit will load environment variables from files ending with
.env
extension from/etc/protonwire/
. This is done by systemd not the unit executable/user. SeeEnvironmentFile
in systemd.exec(5) for more info. -
If
systemd-creds
is available (requires systemd version 250 or above), use drop-in units to supply credentials. see this for more info. -
If
systemd-creds
is not available, save key to in/etc/protonwire/wireguard-private-key
or one of the search paths.- Create
/etc/protonwire
if it does not existsudo mkdir -p /etc/protonwire
- Create private key file, alternatively copy existing key file to this location.
systemd-ask-password | sudo tee -a /etc/protonwire/private-key
- Allow
systemd-network
group to access,sudo chown root:systemd-network /etc/protonwire/private-key
- Ensure ony
root
can write to file, members of groupsystemd-network
can read the file and others have no access to file.sudo chmod 640 /etc/protonwire/private-key
Script will refuse to use key file, if its is readable by others.
If running as non-root user(default), ensure unit's user has access to the key file. Using
SupplementaryGroup=systemd-network
and givingsystemd-network
group read access to key file. - Create
-
For non sensitive settings, use environment files(
.env
) in/etc/protonwire/
They are loaded automatically be the default unit.# /etc/protonwire/settings.env PROTONVPN_SERVER="nl-free-127.protonvpn.net"
-
Reload systemd
sudo systemctl daemon-reload
-
Enable protonwire service via
sudo systemctl enable protonwire
-
Start protonwire service via
sudo systemctl start protonwire
-
Stop VPN service via
Warning
Units bound to protonwire unit will also be stopped.
sudo systemctl stop protonwire
-
Check status of VPN service via
systemctl status protonwire
-
To check logs, use
journalctl -u protonwire
. -
Disable VPN service via
sudo systemctl disable --now protonwire sudo protonwire disable-ks
- Systemd watchdog feature is supported and enabled if
NOTIFY_SOCKET
andWATCHDOG_USEC
are set. IPCHECK_INTERVAL
or--check-interval
, with non zero value cannot be used with watchdog as it creates conflicts.WatchdogSec
cannot be less than 20 seconds.- Default watchdog signal(
SIGABRT
) cannot be used with containers if with--init
flag. MUST setWatchdogSignal=SIGTERM
as bothtini
(docker) andcatatonit
(podman) do not forward this signal to their children.
Note
Requires systemd version 244 or later.
- Split horizon DNS is only supported with
systemd-resolved
AND when NOT running in a container. - It depends on
systemd-resolved
to be up and running and/etc/resolv.conf
to be in stub resolver mode.nss-resolve
is buggy as most statically compiled programs (especially Go) may break DNS resolution for link specific domains. - It also requires specifying routing domains and/or search domains for local/other-vpn networks, via DHCP options or via
resolvectl
- By default script will set default routing domain on VPN interface.
- Run the command below for status of routing domains.
resolvectl status --no-pager
- Disable
systemd-resolved
integration by setting environment variableSKIP_DNS_CONFIG
to1
or via--skip-dns-config
CLI flag.
Depend on protonwire
unit by adding ALL the properties below to [Unit]
section in
dependent units. See systemd.unit(5) for more info.
This setup ensures that service depending on VPN will be ONLY started when protonwire
is activated. (Dependent units still have to be enabled) If for some reason protonwire service becomes un-healthy and exits, BindsTo
ensures that dependent unit will be stopped.
If system package already provides a systemd unit file for the service, use drop-in units to configure dependencies.
Note
Don't forget to run
sudo systemctl daemon-reload
upon updating/installing unit files.
See Troubleshooting and FAQ
Building requires goreleaser
(v1.9+), and docker
with buildx
plugin.
make docker