Laurent Godet

A blog with too much yaml

Access Disney+ Streaming Service with WireGuard VPN on GCP

Posted on Sep 15, 2019

Disney just started rolling out their new streaming service, Disney+. The service is currently only available in the Netherlands for free, with an official launch later in September. The catalogue is quite limited at the moment, but seeing as the service is free, we really have no reason to complain.

With all the excitement building around Disney+, I’m sure many people would love to get their hands on the streaming service and start watching a timeless Disney Classic.

Disney+ Login

In the following guide, we’ll set up a Google Cloud instance running in the Netherlands region, install boringtun, a VPN implementation of WireGuard protocol, and tweak the config to route all traffic through the VPN.

As a side note, I should mention that the Disney+ Streaming Service is currently solely aimed at Dutch users.

Requirements

Google Cloud

I assume that you already have access to Google Cloud Console with a dummy project set up for this. If you don’t, feel free to try their Free Tier program, which gives you $300 of free credit to spend over 12 months: https://cloud.google.com/free

# Set the GCP Project ID
$ export GCP_PROJECT_ID=vpn

# Netherlands Region
$ export GCP_ZONE=europe-west4-a

# Change this to your public IP Address
$ export CLIENT_IP_ADDR='<MY-IP-ADDRESS>'

# Log on GCP 
$ gcloud auth login
$ gcloud config set project $GCP_PROJECT_ID

$ gcloud config set compute/zone $GCP_ZONE

Create a Service Account that allows SSH access

$ gcloud iam service-accounts create ssh-account \
    --project $GCP_PROJECT_ID \
    --display-name "ssh-account"

Create a network to run the workload on

$ gcloud compute networks create vpn-net \
    --bgp-routing-mode=regional \
    --project $GCP_PROJECT_ID

Add firewall rules to access the instance

# Allow SSH access
$ gcloud compute firewall-rules create allow-ssh \
    --project $GCP_PROJECT_ID \
    --network vpn-net --allow tcp:22 \
    --source-ranges "$CLIENT_IP_ADDR/32"

# Allow VPN tunneling (WireGuard)
$ gcloud compute firewall-rules create allow-wireguard \
    --project $GCP_PROJECT_ID \
    --network vpn-net --allow udp:51820 \
    --source-ranges "$CLIENT_IP_ADDR/32"

Spin up an f1-micro instance running on vpn-net network, with ubuntu bionic distribution. The ssh-account service account previously created gets attached to the instance in order for us to SSH on the box

$ gcloud compute instances create vpn \
    --project $GCP_PROJECT_ID \
    --network vpn-net \
    --service-account ssh-account@$PROJECT_ID.iam.gserviceaccount.com  \
    --scopes https://www.googleapis.com/auth/cloud-platform \
    --zone $GCP_ZONE \
    --can-ip-forward \
    --image ubuntu-minimal-1804-bionic-v20190911 \
    --image-project ubuntu-os-cloud \
    --machine-type f1-micro

Wait a few seconds for the instance to become reachable, then you can SSH on it

$ gcloud compute ssh vpn \
    --project $GCP_PROJECT_ID \
    --zone $GCP_ZONE

Introducing WireGuard and BoringTun

Now that we’re connected on the instance, let’s install all the necessary tools to create a VPN tunnel

WireGuard VPN Logo

# All following commands as run as sudo
vpn:~$ apt update

# Essential for Cargo crates
vpn:~$ apt install -y \
    vim \
    gcc \
    build-essential \
    software-properties-common

# If you, like me, need to do some network debugging
vpn:~$ apt install -y \
    dns-utils \
    iputils-ping

# Install wiregard
vpn:~$ add-apt-repository ppa:wireguard/wireguard -y
vpn:~$ apt update && apt install -y wireguard

# Install rust
vpn:~$ curl https://sh.rustup.rs -sSf | sh -s -- -y
vpn:~$ source $HOME/.cargo/env

# Let's fetch the latest version of BoringTun
# The current release (v0.2.0) throws `mismatched types` errors
vpn:~$ cargo install \
    --git https://github.com/cloudflare/boringtun

Use WireGuard keytools to generate keys

vpn:~$ wg genkey | tee privatekey | wg pubkey > publickey

vpn:~$ ls -ll
-rw------- 1 root root 45 Sep 13 11:26 privatekey
-rw------- 1 root root 45 Sep 13 11:26 publickey

Create a WireGuard config with the following

vpn:~$ cat /etc/wireguard/wg0.conf

[Interface]
Address = 172.168.2.1 # server ip address
PrivateKey = 4IXGgjoTa/HJJVoFS1ofmGZTTt5c3Q= # server public key
ListenPort = 51820 # server port

[Peer]
PublicKey = MOhoJcQaWggUeZ6nbYorIBTTV64ZvVY= # client public key
AllowedIPs = 172.168.2.3/32 # client ip address

Great, we can now bring up the tunnel using boringtun

vpn:~$ WG_QUICK_USERSPACE_IMPLEMENTATION=boringtun WG_SUDO=1 wg-quick up wg0
[#] ip link add wg0 type wireguard
[#] wg setconf wg0 /dev/fd/63
[#] ip -4 address add 172.168.2.1 dev wg0
[#] ip link set mtu 1380 up dev wg0
[#] ip -4 route add 172.168.2.3/32 dev wg0

Let’s verify that WireGuard runs as expected

vpn:~$ wg show
interface: wg0
  public key: 5bjWGUZ5hI...
  private key: (hidden)
  listening port: 51820

peer: MOhoJcQaWgx...
  allowed ips: 172.168.2.3/32

Set up the Client side

I will use macOS’ client as an example, but you can download the client that suits you the best, the steps should be pretty similar. https://www.wireguard.com/install/

As well as the GUI, I’ve also installed the CLI toolkit

$ brew install wireguard-tools

Create a new Empty Tunnel and populate with the following settings

[Interface]
PrivateKey = 4MfFvqx8egPq... # Client private key
ListenPort = 21841
Address = 172.168.2.3/32 # Client IP address

[Peer]
PublicKey = 5bjWGUZ5hjX... # Server public key
AllowedIPs = 172.168.2.0/24 # Traffic for these CIDRS will go over the VPN
Endpoint = xxx.xxx.xxx.xxx:51820 # Server public key

Activate the tunnel, and make sure that the connection is established

$ ping 172.168.2.1
PING 172.168.2.1 (172.168.2.1): 56 data bytes
64 bytes from 172.168.2.1: icmp_seq=0 ttl=64 time=14.222 ms
64 bytes from 172.168.2.1: icmp_seq=1 ttl=64 time=16.634 ms
64 bytes from 172.168.2.1: icmp_seq=2 ttl=64 time=17.173 ms
3 packets transmitted, 3 packets received, 0.0% packet loss

Route all Traffic through WireGuard

Even though the tunnel is fully set up, we still can’t reach other websites via WireGuard

$ nslookup www.google.com. 172.168.2.1
;; connection timed out; no servers could be reached
...

Let’s instruct WireGuard to NAT all traffic on the main interface.
Add the following PostUp and PostDown commands to the Server config so that it looks like:

# Bring the VPN down
vpn:~$ WG_QUICK_USERSPACE_IMPLEMENTATION=boringtun WG_SUDO=1 wg-quick down wg0
# Full Server side config
vpn:~$ cat /etc/wireguard/wg0.conf

[Interface]
Address = 172.168.2.1 # server ip address
PrivateKey = 4IXGgjoT... # server public key
ListenPort = 51820 # server port

PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o ens4 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o ens4 -j MASQUERADE

[Peer]
PublicKey = MOhoJcQaWg... # client public key
AllowedIPs = 172.168.2.3/32 # client ip address
# Bring the VPN back up
vpn:~$ WG_QUICK_USERSPACE_IMPLEMENTATION=boringtun WG_SUDO=1 wg-quick up wg0

NB: I use ens4 as this is the default interface on ubuntu 18.04 instances, make sure you set the correct interface

Ensure IP forwarding in set up

vpn:~$ echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf && \
       echo "net.ipv4.conf.all.proxy_arp = 1" >> /etc/sysctl.conf

vpn:~$ sysctl -p /etc/sysctl.conf

Last but not least, update AllowedIPs parameter in the Client’s config to route all traffic through the VPN. Here is what the full config looks like:

[Interface]
PrivateKey = 4MfFvqx8egPq...
ListenPort = 21841
Address = 172.168.2.3/32

DNS = 172.168.2.1 # Set the DNS server as the WireGuard instance, needed for later
MTU = 1300 # I use a smaller value as I had issues with the default MTU

[Peer]
PublicKey = 5bjWGUZ5hjX...
AllowedIPs = 0.0.0.0/0, ::/0 # Route all traffic through
Endpoint = xxx.xxx.xxx.xxx:51820

PersistentKeepalive = 25 # Keep the connection up

Re-activate the connection to take changes into account.

Everything looks fine, but we are still unable to resolve DNS. Let’s install unbound, an excellenet lightweight DNS resolver.

vpn:~$ apt-get install unbound unbound-host

# Fetch latest hints instead of using the builtin ones
vpn:~$ curl --output /var/lib/unbound/root.hints https://www.internic.net/domain/named.cache
# Edit unbound's config
vpn:~$ cat /etc/unbound/unbound.conf

server:
  num-threads: 4
  root-hints: "/var/lib/unbound/root.hints"

  # Use the root servers key for DNSSEC
  auto-trust-anchor-file: "/var/lib/unbound/root.key"

  # Respond to DNS requests on all interfaces
  interface: 0.0.0.0
  max-udp-size: 3072

  # ACLS
  access-control: 0.0.0.0/0                 refuse
  access-control: 127.0.0.1                 allow
  access-control: 172.168.2.0/24            allow
  private-address: 172.168.2.0/24

  # Hide DNS Server info
  hide-identity: yes
  hide-version: yes

  # Limit DNS Fraud and use DNSSEC
  harden-glue: yes
  harden-dnssec-stripped: yes
  harden-referral-path: yes

  # Add an unwanted reply threshold to clean the cache and avoid when possible a DNS Poisoning
  unwanted-reply-threshold: 10000000

  # Have the validator print validation failures to the log.
  val-log-level: 1

  # Config for RRsets and messages in the cache
  cache-min-ttl: 1800 
  cache-max-ttl: 14400
  prefetch: yes
  prefetch-key: yes
# Set the right permissions and enable the service
vpn:~$ chown -R unbound:unbound /var/lib/unbound
vpn:~$ systemctl enable unbound

# Stop systemd-resolved and let unbound take over
vpn:~$ systemctl disable systemd-resolved && systemctl stop systemd-resolved
vpn:~$ systemctl start unbound

Check the logs to make sure unbound is properly started

vpn:~$ journalctl -fu unbound
-- Logs begin at Fri 2019-09-13 10:45:21 UTC. --
Sep 13 vpn systemd[1]: Starting Unbound DNS server...
Sep 13 vpn package-helper[15855]: /var/lib/unbound/root.key has content
Sep 13 vpn package-helper[15855]: success: the anchor is ok
Sep 13 vpn systemd[1]: Started Unbound DNS server.
Sep 13 unbound[15868]: [15868:0] info: start of service (unbound 1.6.7).

Finally, check that DNS resolving works as expected on the client side

$ curl -IL --silent https://disneyplus.com
HTTP/1.1 200 OK
Content-Length: 51850
Content-Type: text/html; charset=utf-8
Server: nginx/1.12.1
Strict-Transport-Security: max-age=15552000; includeSubDomains
Connection: keep-alive

Disney+ Header

Head over to https://disneyplus.com/nl/ and enjoy.
If your browser is set to a different location than Netherlands, I suggest updating it.

References

https://git.zx2c4.com/WireGuard/about/src/tools/man/wg-quick.8
https://www.wireguard.com/netns/#routing-all-your-traffic
https://www.ckn.io/blog/2017/11/14/wireguard-vpn-typical-setup
https://cloud.google.com/vpn/docs/concepts/mtu-considerations
https://gist.github.com/Overbryd/ab15ee86c58260cb6d0be634a4c58057
https://nlnetlabs.nl/documentation/unbound/unbound.conf
https://github.com/cloudflare/boringtun