DEV Community

Cover image for Setting Up a WireGuard VPN Client on Linux
Joshua Rothe
Joshua Rothe

Posted on • Originally published at portfolio.rothellc.com

Setting Up a WireGuard VPN Client on Linux

Full text can also be viewed here.


Setting up a WireGuard VPN for privacy and security involves setting up both server and client side systems. This guide explains how to set up a client side Linux system - with or without Pi-hole DNS filtering on the home network - and then configure the system so that WireGuard settings will switch depending on if the client system is on the home network or not. This is necessary because the WireGuard client will break your network connection if you are on your home network, and there is no need to manually switch your VPN client on and off when automation exists.

This guide assumes a WireGuard VPN server is set up, and port forwarding is configured on the home router. This guide is also written for Linux Mint - while this should also work for most Debian systems, you may need to modify some file paths depending on your distro.


Background

VPNs are a great tool for security and privacy, with key benefits being:

Privacy and Security:

  • Location Privacy: All traffic appears to be from your home IP address. Your actual location is obscured to anyone tracking your browsing activity.
  • Data Protection: On public wireless networks, your VPN tunnel ensures packet sniffers and other malicious actors can't see your activity.
  • Audited: WireGuard is open source and is audited regularly by both organizations and individuals. All of its code is viewable by any developer curious enough to know how it functions.

Functionality:

  • Ad Blocking: If you run a Pi-Hole system at home for ad blocking, you can use that same tool while off your home network.
  • Home Network Access: This secure tunnel to your home network means you can access devices on your home network such as cameras, NAS systems, and IoT devices without needing to expose them to the internet.
  • Speed: WireGuard is fast, faster than most other comparable VPN services.

Practicality:

  • Low Overhead: Works on virtually all modern hardware.
  • Cryptographically Secure: Uses proven, modern cryptographic primitives detailed on the WireGuard homepage.
  • Free: Self explanatory. No risk of subscription fees in the future, either. Open source comes with benefits!

If you do not have a WireGuard VPN server set up and find this interesting, I've worked through the official WireGuard documentation and found it more than sufficient. Make sure this is complete before setting up clients! Windows and MacOS have dedicated programs for clients, but Linux is a bit more complicated; hence this guide.


Prerequisites

  • WireGuard server, configured with port forwarding on your home router.
  • Linux client with sudo access (tested here on Mint/Debian).
  • Basic command line familiarity.

Client Setup

SSH Key Generation

This process should be familiar (for general server access, not WireGuard-specific handshakes), but direction is provided below just in case. On the client, run:

ssh-keygen -t ed25519 -C "your-email@example.com"
Enter fullscreen mode Exit fullscreen mode

Hit enter twice - use default location and no passkey (unless you really want one).

There are two ways to copy your key to the server, the Easy Way and the More Likely Way.


The Easy Way:

The easy way only works if your client can access the server. Since we are generating keys, you should probably have password authentication enabled for SSH. In case you forgot, you modify these lines in /etc/ssh/sshd_config:

PasswordAuthentication no
PubkeyAuthentication yes
PermitRootLogin no
Enter fullscreen mode Exit fullscreen mode

Then on the client side you can simply run:

ssh-copy-id username@server-ip
Enter fullscreen mode Exit fullscreen mode

This copies SSH key settings to the host, if security settings allow it.

The More Likely Way:

Run this on the client:

cat ~/.ssh/id_ed25519.pub
Enter fullscreen mode Exit fullscreen mode

Get this clipboard item to the server however you like, probably using another system that does have SSH access, and append it to ~/.ssh/authorized_keys. No need to disable the server's security requirements this way.


Installation

Run on the client to install necessary dependencies:

sudo apt update
sudo apt install wireguard resolvconf
Enter fullscreen mode Exit fullscreen mode

Once that is done, generate WireGuard keys (these are different than SSH keys - the former are needed to access the WireGuard server for configuration):

cd /etc/wireguard
sudo umask 077
sudo wg genkey | sudo tee privatekey | sudo wg pubkey | sudo tee publickey
Enter fullscreen mode Exit fullscreen mode

Next, create the WireGuard config file on your client using:

sudo vim /etc/wireguard/wg0.conf
Enter fullscreen mode Exit fullscreen mode

And insert the following text to create your client's WireGuard network configuration file. This facilitates the key handshake your client system will do with the server:

[Interface]
PrivateKey = # Paste the contents of /etc/wireguard/privatekey here
Address = 10.0.0.2/24
DNS = 1.1.1.1

[Peer]
PublicKey = # Your server's public key goes here.
Endpoint = your-server-ip:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
Enter fullscreen mode Exit fullscreen mode

Note that you have three items to fill in above. You will need the PublicKey from the server, not from what you have here on the client. So hold off on that for now.

The private key on the client can be acquired using:

sudo cat /etc/wireguard/privatekey
Enter fullscreen mode Exit fullscreen mode

The public key on the client (which will be needed on the server) can be acquired using:

sudo cat /etc/wireguard/publickey
Enter fullscreen mode Exit fullscreen mode

Now SSH into the WireGuard server, and edit the WireGuard config using:

sudo vim /etc/wireguard/wg0.conf
Enter fullscreen mode Exit fullscreen mode

Append the following to the file:

[Peer]
PublicKey = # Paste your client's public key here.
AllowedIPs = 10.0.0.x/32
Enter fullscreen mode Exit fullscreen mode

For AllowedIPs, you will need to replace the x. For my setup, the server was .1, and I had two previous clients taking up .2 and .3, so I used .4. These are the IPs the WireGuard server uses to identify the peers on its network.

Back on the client, edit the client's WireGuard config using:

sudo vim /etc/wireguard/wg0.conf
Enter fullscreen mode Exit fullscreen mode

Remember how we didn't have the server's public key last time? Fix that now:

[Interface]
PrivateKey = # Paste(d) the contents of /etc/wireguard/privatekey here
Address = 10.0.0.2/24
DNS = 1.1.1.1

[Peer]
PublicKey = # Your server's public key goes here.
Endpoint = your-server-ip:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
Enter fullscreen mode Exit fullscreen mode

Three items to check, above. Make sure the Address on your client side matches AllowedIps on the server side.


Dual WireGuard Configurations

If you start WireGuard on your home network now, it will fail. You need to configure something that will turn it off when you are on your home network. Fortunately, we have good tools for this.

On your client side, run:

sudo cp /etc/wireguard/wg0.conf /etc/wireguard/wg0-away.conf
sudo cp /etc/wireguard/wg0.conf /etc/wireguard/wg0-home.conf
Enter fullscreen mode Exit fullscreen mode

Both of these files will become your home and away configurations, which will be copied into wg0.conf when your client detects a network change.

Edit the home configuration using:

sudo vim /etc/wireguard/wg0-home.conf
Enter fullscreen mode Exit fullscreen mode

The only change you need to make below is to AllowedIPs: your-local-ip (e.g. 192.168.1.0/24) is critical here, as you want the initial 192.168.1.* to match the home network IPs your router issues. Also, don't forget to adjust Address if needed.

[Interface]
PrivateKey = # Your laptop's private key.
Address = 10.0.0.2/24
DNS = 1.1.1.1 # Or 192.168.1.xxx for Pi-Hole.

[Peer]
PublicKey = # Your server's public key.
Endpoint = your-server-local-ip:51820
AllowedIPs = your-local-ip/24
PersistentKeepalive = 25
Enter fullscreen mode Exit fullscreen mode

Next, the away config should be edited using:

sudo vim /etc/wireguard/wg0-away.conf
Enter fullscreen mode Exit fullscreen mode

And should contain:

[Interface]
PrivateKey = # Your laptop's private key.
Address = 10.0.0.2/24
DNS = 1.1.1.1 # Or 192.168.1.xxx for Pi-Hole.

[Peer]
PublicKey = # Your server's public key.
Endpoint = your-server-external-ip:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
Enter fullscreen mode Exit fullscreen mode

Note for above, you need your server's external IP. This will be your home router's IP, port forwarded to your VPN server. This was likely set up when the WireGuard server was set up, and you only need a service like this to check your external IP.

As a reminder, these two wg0-* scripts will replace wg0.conf dynamically depending on what network the client is on. This will be triggered by the detection script, which we will now build.


Detection Script

Run:

sudo vim /etc/NetworkManager/dispatcher.d/99-wireguard-auto
Enter fullscreen mode Exit fullscreen mode

Add the following, adjusting the HOME_* values as needed:

#!/bin/bash

INTERFACE=$1
ACTION=$2

# Filter out interfaces we don't care about (loopback, virtual bridges, docker).
case "$INTERFACE" in
    lo|virbr*|docker*|br-*)
        exit 0
        ;;
esac

LOG_FILE="/var/log/wireguard-auto.log"

log_message() {
    echo "$(date): [$INTERFACE/$ACTION] $1" | tee -a "$LOG_FILE"
}

# Only act on interface up/down events for primary connections.
if [ "$ACTION" != "up" ] && [ "$ACTION" != "down" ]; then
    exit 0
fi

# Skip if this is the WireGuard interface itself.
if [ "$INTERFACE" = "wg0" ]; then
    exit 0
fi

log_message "Network change detected on $INTERFACE"

# Let network settle.
sleep 2

# Configuration. Change these as needed.
HOME_NETWORK_GATEWAY="192.168.1.1" # Router's IP.
HOME_WIREGUARD_SERVER="192.168.1.114" # WireGuard server IP.
WG_INTERFACE="wg0"

is_home_network() {
    # Check if home gateway is reachable.
    if ping -c 1 -W 3 "$HOME_NETWORK_GATEWAY" >/dev/null 2>&1; then
        # Double-check by pinging WireGuard server directly.
        if ping -c 1 -W 3 "$HOME_WIREGUARD_SERVER" >/dev/null 2>&1; then
            # Triple-check by seeing if our IP is in the home range.
            local_ip=$(ip route get 8.8.8.8 2>/dev/null | awk '{print $7; exit}')
            if [[ "$local_ip" =~ ^192\.168\.1\. ]]; then
                return 0 # Home.
            fi
        fi
    fi
    return 1 # Away.
}

get_wg_status() {
    if systemctl is-active --quiet wg-quick@wg0; then
        echo "active"
    else
        echo "inactive"
    fi
}

copy_config() {
    local source="$1"
    if [ -f "$source" ]; then
        cp "$source" /etc/wireguard/wg0.conf
        log_message "Copied $source to wg0.conf"
        return 0
    else
        log_message "ERROR: $source not found!"
        return 1
    fi
}

# Only proceed if network connection is present.
if [ "$ACTION" = "up" ]; then
    if is_home_network; then
        log_message "Detected home network."
        current_status=$(get_wg_status)

        if [ "$current_status" = "active" ]; then
            # Check if using away config (away config has AllowedIPs = 0.0.0.0/0).
            if grep -q "AllowedIPs = 0.0.0.0/0" /etc/wireguard/wg0.conf 2>/dev/null; then
                log_message "Switching to home VPN config."
                systemctl stop wg-quick@wg0
                if copy_config "/etc/wireguard/wg0-home.conf"; then
                    systemctl start wg-quick@wg0
                fi
            else
                log_message "Already using home VPN config."
            fi
        else
            log_message "Starting home VPN config."
            if copy_config "/etc/wireguard/wg0-home.conf"; then
                systemctl start wg-quick@wg0
            fi
        fi
    else
        log_message "Detected external network."
        current_status=$(get_wg_status)

        if [ "$current_status" = "active" ]; then
            # Check if we're using the home config (home config has restricted AllowedIPs).
            if grep -q "AllowedIPs = 192.168.1.0/24" /etc/wireguard/wg0.conf 2>/dev/null; then
                log_message "Switching to away VPN config."
                systemctl stop wg-quick@wg0
                if copy_config "/etc/wireguard/wg0-away.conf"; then
                    systemctl start wg-quick@wg0
                fi
            else
                log_message "Already using away VPN config."
            fi
        else
            log_message "Starting away VPN config."
            if copy_config "/etc/wireguard/wg0-away.conf"; then
                systemctl start wg-quick@wg0
            fi
        fi
    fi
elif [ "$ACTION" = "down" ]; then
    # Uncomment the next line if wireguard should stop when network goes down.
    # systemctl stop wg-quick@wg0
    log_message "Network disconnected."
fi

exit 0
Enter fullscreen mode Exit fullscreen mode

Once complete, make it executable:

sudo chmod +x /etc/NetworkManager/dispatcher.d/99-wireguard-auto
Enter fullscreen mode Exit fullscreen mode

Then, add the following (adjust DNS IP as needed) using:

sudo vim /etc/NetworkManager/dispatcher.d/99-wireguard-dns.sh
Enter fullscreen mode Exit fullscreen mode

Add:

#!/bin/bash

INTERFACE=$1
ACTION=$2

if [[ "$INTERFACE" == "wg0" && "$ACTION" == "up" ]]; then
    # Set Pi-hole as DNS server for WireGuard interface.
    resolvectl dns wg0 192.168.1.223 # Change! Pi-Hole DNS address or 1.1.1.1 1.0.0.1 (Cloudflare) or 9.9.9.9 149.112.112.112 (Quad9)
    resolvectl domain wg0 "~."

    # Remove default route from WiFi interface to prioritize VPN DNS.
    resolvectl default-route <replace-me> false

    # Ensure only VPN has default route for DNS.
    resolvectl default-route wg0 true

    # Flush DNS cache to apply changes.
    resolvectl flush-caches

    logger "WireGuard DNS configured: Pi-hole via VPN with WiFi DNS disabled."
fi
Enter fullscreen mode Exit fullscreen mode

To know what to enter in <replace-me>, run ip link show. You need your actual WiFI interface name here.

Make it executable:

sudo chmod +x /etc/NetworkManager/dispatcher.d/99-wireguard-dns.sh
Enter fullscreen mode Exit fullscreen mode

To finalize, restart WireGuard on both machines. For the server first, run:

sudo systemctl restart wg-quick@wg0
Enter fullscreen mode Exit fullscreen mode

For the client, run:

sudo resolvectl default-route wlp4s0 false
sudo systemctl restart wg-quick@wg0
Enter fullscreen mode Exit fullscreen mode

This restarts WireGuard and fixes the initial VPN prioritization issue. Some Linux queries might go through your ISP's DNS instead of the proper tunnel due to how systemd-resolve handles queries.


Troubleshooting

Check that everything is running properly. If Pi-Hole is running, execute the following to verify that it is blocking ad servers (skip if no Pi-Hole is involved):

nslookup doubleclick.net
nslookup googleadservices.com
Enter fullscreen mode Exit fullscreen mode

With Pi-Hole it should return 0.0.0.0.

If DNS is not working with Pi-Hole

Check if Pi-hole is reachable:

ping 192.168.1.xxx
Enter fullscreen mode Exit fullscreen mode

(Change x's as needed).

Verify the DNS script ran:

journalctl -u NetworkManager -f
Enter fullscreen mode Exit fullscreen mode

Restart systemd-resolved:

sudo systemctl restart systemd-resolved
Enter fullscreen mode Exit fullscreen mode

VPN connects, but no internet

Check routing:

ip route show
Enter fullscreen mode Exit fullscreen mode

Script not triggering on network changes

This might be an open-ended issue to debug, but here are some common items to try.

Check if NetworkManager is running:

sudo systemctl status NetworkManager
Enter fullscreen mode Exit fullscreen mode

View logs:

sudo tail -f /var/log/wireguard-auto.log
Enter fullscreen mode Exit fullscreen mode

Conclusion

That should get you a working VPN client! If you notice any issues with this guide, please feel free to reach out or leave a comment.

Top comments (0)