Wireguard Server on macOS

Update: March 11, 2020

This is a revision of the first guide originally published back in August 30, 2019.

This revision contains a myriad of improvements suggested by lifepillar and would not exist without their help. Lifepillar has my heartfelt thanks for their tremendous contribution to this site and the Wireguard community.

The original guide remains available here. Please note that the original guide is no longer the recommended approach and remains available for historical preservation purposes only.

Notice: May 12, 2020

Please be aware that at present the guide's traffic routing instructions route through IPv4 traffic only. That means IPv6-exclusive hosts will be inaccessible to connected peers configured with this guide. An update's in progress to include more comprehensive traffic routing instructions to properly route both IPv4 and IPv6 traffic.

Many thanks to Jeremy Quinn for detecting this oversight.

After months of false starts and dead ends, I’m happy to report my Wireguard VPN server is successfully running on macOS.

Wireguard is a relatively new VPN protocol, entering the scene just three short years ago in 2016. Compared to the ancient VPN alternatives like IPSec and OpenVPN, Wireguard’s simplicity and speed quickly earned it the attention and praise of various tech communities like Hacker News and Lobste.rs. Everywhere you look are Wireguard threads filled with enthusiastic comments urging others to give Wireguard a try.

The best part is, they’re right! Compared to the existing lineup of VPN protocols, Wireguard’s small codebase, blazingly fast speed, and relatively simple setup put Wireguard in a league of its own as the first truly modern VPN protocol. There’s just one tiny niggle: the protocol is so young there might be no documentation or guides for your platform of choice.

Unfortunately, that’s the situation I found myself in with macOS when attempting to set up my Wireguard server. Despite extensively reading what’s currently the finest Wireguard documentation available and following its relevant example to a tee, Wireguard would simply not function as expected on macOS. The best I could do was establish a direct connection to the server with Wireguard, but all attempts to access the LAN or surf the web timed out and failed.

Despite numerous attempts over the months, my Google-fu yielded no results, either. While there were plenty of help articles from others setting up Wireguard on macOS, every single one of them was for setting up a Wireguard peer on macOS. This peer was always intended to only connect to a Wireguard server on more common platforms like Ubuntu or CentOS, never the other way around.

However, from my on-and-off research over these past few months I’ve finally cobbled together a solution that’s working. Thus, I intend to fill this hole in the community’s growing collection of documentation and setup guides: here’s what you need to do to get a Wireguard server running on macOS with full traffic routing and LAN access.

  1. Pick an unused IPv4 subnet range where Wireguard and the attached clients will reside. This way, we can ensure that Wireguard won't encounter unexpected IP collisions with other users or services on the local network. For this guide, I'm using 10.0.10.0/24, which expands to cover all IPs from 10.0.10.0 to 10.0.10.255.
  2. Decide a port to host Wireguard on (the standard port is 51820). If your router doesn’t have this port opened and forwarding to your Wireguard server's local IP yet, do so now. For the sake of brevity, specific instructions for how to do this on your particular router won't be covered here. For assistance, Google your Internet provider’s name and "setup port forwarding".
  3. To access other clients on the LAN and route internet traffic for web browsing while on the VPN, we need to create a firewall rule to let macOS know how to route that traffic. To do so, we'll use macOS's built-in packet filter utility, pfctl. There's a number of possible ways to do this, but to make our changes resilient to macOS updates and as unobtrusive to the rest of the system as possible, we'll dynamically add and remove the rule by leveraging a couple bash scripts. Later, we'll configure Wireguard to automatically execute these with its PostUp and PostDown directives on startup and shutdown, respectively.
    1. Create /usr/local/etc/wireguard/postup.sh with the following contents. Remember to replace 10.0.10.0/24 with the subnet range you picked earlier. Note that the Ethernet network device en0 is assumed, and you should replace it if you're serving Wireguard over WiFi or another network device:
      #!/bin/sh
      
      # 1) Preparing the directory which we'll persist the pf token
      #    generated by (2) in. That token can then be used by our
      #    postdown.sh script to remove the routing rule in the
      #    event Wireguard is shut down on the server.
      mkdir -p /usr/local/var/run/wireguard
      chmod 700 /usr/local/var/run/wireguard
      
      # 2) Dynamically add the NAT rule, enable the firewall, increase
      #    its reference count (-E) and persist the reference token
      #    generated by the command into pf_wireguard_token.txt,
      #    which postdown.sh will reference when Wireguard is shut
      #    down.
      echo 'nat on en0 from 10.0.10.0/24 to any -> (en0)' | \
          pfctl -a com.apple/wireguard -Ef - 2>&1 | \
          grep 'Token' | \
          sed 's%Token : \(.*\)%\1%' > /usr/local/var/run/wireguard/pf_wireguard_token.txt   
    2. Create /usr/local/etc/wireguard/postdown.sh with the following contents:
      #!/bin/sh
      
      # 1) Fetch the pf reference token that was generated on
      #    Wireguard startup with postup.sh
      TOKEN=`cat /usr/local/var/run/wireguard/pf_wireguard_token.txt`
      
      # 2) Remove the reference (and by extension, the pf rule that
      #    generated it). Adding and removing rules by references
      #    like this will automatically disable the packet filter
      #    firewall if there are no other references left, but will
      #    leave it up and intact if there are.
      pfctl -X ${TOKEN} || exit 1
      rm -f /usr/local/var/run/wireguard/pf_wireguard_token.txt
      
    3. Make both scripts executable:
      $ chmod u+x /usr/local/etc/wireguard/*.sh
      
  4. Install the Wireguard CLI with the following Homebrew command:
    $ brew install wireguard-tools
    
    Note that the command line interface for Wireguard is not optional on the server, the Wireguard App on the App Store is not sufficient for managing a Wireguard server due to macOS's sandbox restrictions. (Specifically, the PostUp and PostDown directives—which this configuration relies on—are not supported in the App Store's Wireguard App).
  5. Generate a number of private and public key pairs for Wireguard to use. This number should be equal to the number of peers you want plus one for your server. You can use the following commands to accomplish this, which will spit the keys into your current directory:
    $ umask 077 # Ensure credentials don't leak from possible race condition.
    $ wg genkey | tee privatekey | wg pubkey > publickey
    For example, if you have just a single peer you intend to connect to your Wireguard VPN with, you should have two public and private keys pairs: one pair for your peer, and another pair for your server. Please ensure you keep these files in a safe and secure place, such as an encrypted container. If someone nasty gets them, they have complete remote access to your server!
  6. Create your Wireguard server configuration file. First, decide a location and filename (I chose /usr/local/etc/wireguard/coordinates.conf), then enter in your configuration using the following template as your guide:
    [Interface]
    # Substitute with the subnet you chose for Wireguard earlier.
    Address = 10.0.10.0/24
    # Substitute with your *server's* private key
    PrivateKey = XXX
    # If you chose a different port earlier when setting up port
    # forwarding on your router, update the port here to match.
    ListenPort = 51820
    # This prevents IPv4 & IPv6 DNS leaks when browsing the web on the
    # VPN. I chose Cloudflare's public DNS servers, but feel free to use
    # whatever provider you prefer.
    DNS = 1.1.1.1, 1.0.0.1, 2606:4700:4700::1111
    # This ensures our peers continue to report their Wireguard-
    # assigned IPs while connected to the VPN. This is required for
    # their traffic to get routed correctly by the firewall rules we
    # crafted earlier with pf.
    PostUp = /usr/sbin/sysctl -w net.inet.ip.forwarding=1
    PostUp = /usr/sbin/sysctl -w net.inet6.ip6.forwarding=1
    # Adds the firewall routing rule on Wireguard server startup
    PostUp = /usr/local/etc/wireguard/postup.sh
    # Removes the firewall routing rule on Wireguard server shutdown
    PostDown = /usr/local/etc/wireguard/postdown.sh
    
    [Peer]
    # Substitute with *this peer*'s public key
    PublicKey = XXX
    # Chose a unique IP within the Wireguard subnet you defined earlier
    # that this particular peer will use when connected to the VPN.
    AllowedIPs = 10.0.10.10/32
    
    # Follow the same steps as the [Peer] template above for each
    # additional peer you wish to connect to the VPN with.
    
  7. Install Wireguard on each of your peers and create their configuration files using the following template as your guide:
    [Interface]
    # This MUST match the "AllowedIPs" IP you assigned to this peer in
    # the server's config.
    Address = 10.0.10.10/32
    # Substitute with *this peer's* private key.
    PrivateKey = XXX
    # This prevents IPv4 & IPv6 DNS leaks when browsing the web on the
    # VPN. I chose Cloudflare's public DNS servers, but feel free to use
    # whatever provider you prefer.
    DNS = 1.1.1.1, 1.0.0.1, 2606:4700:4700::1111
    
    [Peer]
    # Substitute with your *server's* public key
    PublicKey = XXX
    # Your Wireguard server's public IP. If you chose a different port
    # earlier when setting up port forwarding on your router, update the
    # port here to match.
    Endpoint = XXX.XXX.XXX.XXX:51820
    # Informs Wireguard to forward ALL traffic through the VPN.
    AllowedIPs = 0.0.0.0/0, ::/0
    # If you're be behind a NAT, this will keep the connection alive.
    PersistentKeepalive = 25
    
  8. Test your Wireguard setup by starting the service on your macOS server with wg-quick up /usr/local/etc/wireguard/coordinates.conf (remembering to change the path to where you persisted your server's configuration file, if necessary). Once you've confirmed the server started successfully, take a peer that's not currently on the LAN (such as an iPhone using cellular) and enable Wireguard VPN on it. With both the server and peer's Wireguard services enabled, run through the following tests to ensure everything's configured correctly:
    • Confirm SSH access to your macOS server's local IP address using an app like Prompt 2. This will validate that your peer and server were able to establish a successful connection with one another.
    • Confirm SSH access to other computers on your Wireguard server's LAN using an app like Prompt 2. This will validate that your server's Wireguard subnet is set up correctly and that Wireguard is properly assigning your peer its predefined, local IP address.
    • Finally, confirm access to the Internet by loading a web page or two. This will validate that your packet filter firewall rule is successfully routing traffic requests from the peer.
  9. Once all the checks above come back clean, you are the proud admin of a macOS Wireguard server. All that's left to do now is set up Wireguard as a launch daemon so it automatically starts at system boot. We'll do so by writing a small launchd plist which will inform macOS how to start the service on system boot.
    1. Create a file at /Library/LaunchDaemons/com.wireguard.server.plist with the following contents, remembering to substitute the path and filenames as necessary:
      <?xml version="1.0" encoding="UTF-8"?>
      <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
      <plist version="1.0">
          <dict>
              <key>Label</key>
              <string>org.wireguard.server</string>
              <key>ProgramArguments</key>
              <array>
                  <string>/usr/local/bin/wg-quick</string>
                  <string>up</string>
                  <string>/usr/local/etc/wireguard/coordinates.conf</string>
              </array>
              <key>KeepAlive</key>
              <true/>
              <key>RunAtLoad</key>
              <true/>
              <key>LaunchOnlyOnce</key>
              <true/>
              <key>StandardErrorPath</key>
              <string>/usr/local/var/log/wireguard.err</string>
              <key>EnvironmentVariables</key>
              <dict>
                  <key>PATH</key>
                  <string>/usr/local/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
              </dict>
          </dict>
      </plist>
    2. Inform launchd of the new plist you just created with the following commands:
      $ sudo launchctl enable system/com.wireguard.server
      $ sudo launchctl bootstrap system /Library/LaunchDaemons/com.wireguard.server.plist
  10. Finally, restart your macOS server. If your launch daemon was configured and loaded correctly, Wireguard should already be running.

Congratulations :tada:! You’re now among the dozen people crazy enough to serve Wireguard on macOS. Please feel free to reach out on Twitter or via email if you have feedback or corrections, I’d love to hear from you.

Research Materials

The following help articles and documentation from fellow enthusiasts were invaluable in my research and referenced heavily. Many thanks to them for their contributions: