Skip to main content

Building a WireGuard VPN Gateway on an Oracle Cloud VM with WSL2

12 min read
#wireguard #networking #oracle-cloud #wsl2 #vpn #linux

Using an Oracle Cloud static IP as a VPN egress for AWS and office VM access

Sometimes the problem isn’t that you need a VPN to access the Internet.

You need a VPN because your Internet-facing IP keeps changing.

In my case, I needed to access AWS and office VMs from a machine sitting behind a CGNAT connection. The public IP assigned by the ISP wasn’t reliable enough to whitelist in security groups or firewalls.

The solution was to use an Oracle Cloud VM as a WireGuard VPN server.

The final architecture looks like this:

                         Internet
                            │
                            │
                  Oracle Cloud VM
                  Public IP:
                  129.159.231.129
                            │
                     WireGuard wg0
                       10.50.0.1/24
                            │
              ┌─────────────┴─────────────┐
              │                           │
           WSL2 Laptop                 Phone
           10.50.0.2                   10.50.0.3
              │                           │
              └───────────┬───────────────┘
                          │
                    Internet / VMs
                          │
                Source IP becomes
                129.159.231.129

The important result is:

AWS / Office VM
      ↓
sees 129.159.231.129

Instead of seeing my ISP’s changing CGNAT address.


Why WireGuard?

There are many VPN solutions available, but WireGuard is particularly convenient for this kind of setup.

It is:

  • lightweight
  • fast
  • simple to configure
  • available on Linux, Windows, Android and iOS
  • based on public/private key pairs
  • easy to configure for multiple peers

The architecture is also simple enough that troubleshooting doesn’t require a huge VPN stack.


The Environment

The VPN server was an Ubuntu VM running on Oracle Cloud.

The client was Ubuntu running under WSL2.

The important network addresses were:

Oracle VM:
    Private IP: 10.0.0.43
    Public IP: 129.159.231.129

WireGuard network:
    10.50.0.0/24

WireGuard server:
    10.50.0.1

WSL client:
    10.50.0.2

The Oracle public IP was the important part.

The goal was to whitelist:

129.159.231.129/32

on AWS security groups or office firewalls.


Step 1 — Verify WireGuard Support in WSL2

One concern I had before starting was whether WireGuard would actually work inside WSL2.

There is a common misconception that WSL cannot use WireGuard because it doesn’t have the required kernel support.

On a modern WSL2 installation, this isn’t necessarily true.

I tested it directly.

First:

sudo modprobe wireguard

Then:

lsmod | grep wireguard

The module loaded successfully.

I also checked the WireGuard CLI:

which wg
wg --version

and got:

/usr/bin/wg
wireguard-tools v1.0.20210914

Finally, I tested whether WSL could create a WireGuard interface:

sudo ip link add wg-test type wireguard
sudo ip link delete wg-test

That worked.

So before assuming WSL won’t work, test the actual environment.


Step 2 — Generate the Client Keypair

WireGuard uses public/private key pairs.

For the WSL client:

umask 077

wg genkey | tee client-private.key | wg pubkey > client-public.key

The important rule is:

Never share private keys. Only the public key goes into the other side’s configuration.

The WSL client received:

10.50.0.2/24

Step 3 — Configure the Oracle WireGuard Server

The WireGuard server configuration was placed at:

/etc/wireguard/wg0.conf

The basic interface looked like:

[Interface]
Address = 10.50.0.1/24
ListenPort = 51820
PrivateKey = SERVER_PRIVATE_KEY

The WSL client was added as a peer:

[Peer]
PublicKey = CLIENT_PUBLIC_KEY
AllowedIPs = 10.50.0.2/32

Notice that the server uses /32 for the peer.

That’s important.

The server isn’t saying:

10.50.0.0/24 belongs to this client

It is saying:

10.50.0.2 belongs to this specific peer

Step 4 — Oracle Cloud Networking

Having WireGuard listening on the VM isn’t enough.

Oracle Cloud’s network security rules also need to allow WireGuard traffic.

The required inbound traffic was:

UDP 51820

Once that was allowed, packets were reaching the server.

However, the first handshake still didn’t work.

This led to one of the more interesting troubleshooting steps.


Problem #1 — UDP 51820 was reaching the VM but WireGuard wasn’t responding

I used tcpdump on the Oracle VM:

sudo tcpdump -ni ens3 udp port 51820

Packets were clearly arriving:

1.187.214.31.39740 > 10.0.0.43.51820: UDP

But there was no response.

The reason was an existing iptables rule:

REJECT all
reject-with icmp-host-prohibited

The firewall looked roughly like this:

INPUT
1 ACCEPT established
2 ACCEPT ICMP
3 ACCEPT loopback
4 ACCEPT SSH
5 REJECT everything

WireGuard UDP/51820 was therefore reaching the operating system but getting rejected before WireGuard could process it.

The fix was:

sudo iptables -I INPUT 1 -p udp --dport 51820 -j ACCEPT

After that, tcpdump showed traffic in both directions and the WireGuard handshake succeeded.

Lesson

If a VPN handshake doesn’t work:

Don’t immediately assume WireGuard is broken.

Check the entire path:

Client
 ↓
Internet
 ↓
Cloud security rules
 ↓
VM network interface
 ↓
iptables/firewalld
 ↓
WireGuard

tcpdump is extremely useful for determining which layer is actually failing.


Step 5 — Enable IP Forwarding

The Oracle VM isn’t just a VPN endpoint.

It is acting as a router.

Therefore Linux must allow packet forwarding.

Check:

sudo sysctl net.ipv4.ip_forward

The required value is:

net.ipv4.ip_forward = 1

If necessary:

sudo sysctl -w net.ipv4.ip_forward=1

For persistence, create:

/etc/sysctl.d/99-wireguard.conf

with:

net.ipv4.ip_forward=1

Step 6 — Configure Forwarding and NAT

The traffic path needs to be:

wg0 → ens3 → Internet

and return traffic needs to come back:

Internet → ens3 → wg0

I used:

iptables -I FORWARD 1 -i wg0 -o ens3 -j ACCEPT
iptables -I FORWARD 2 -i ens3 -o wg0 \
    -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

Then NAT:

iptables -t nat -A POSTROUTING -o ens3 -j MASQUERADE

The important concept here is that the Oracle VM translates the WireGuard client’s private VPN address into its public IP.

So:

10.50.0.2
    ↓
Oracle NAT
    ↓
129.159.231.129

Problem #2 — The firewall rules were in the wrong order

This was a classic iptables mistake.

Initially, the Oracle FORWARD chain looked like:

1 REJECT all
2 ACCEPT wg0 → ens3
3 ACCEPT ens3 → wg0 RELATED,ESTABLISHED

It looks like forwarding is allowed.

But iptables evaluates rules from top to bottom.

Therefore traffic hit:

REJECT all

before it ever reached the WireGuard forwarding rule.

The result was:

WireGuard handshake       ✅
Ping VPN server           ✅
Internet through VPN      ❌

The fix was simply to put the ACCEPT rules before the REJECT:

sudo iptables -I FORWARD 1 -i wg0 -o ens3 -j ACCEPT

sudo iptables -I FORWARD 2 \
    -i ens3 -o wg0 \
    -m conntrack --ctstate RELATED,ESTABLISHED \
    -j ACCEPT

The final order became:

1 ACCEPT wg0 → ens3
2 ACCEPT ens3 → wg0 RELATED,ESTABLISHED
3 REJECT all

This was a good reminder:

With iptables, a correct rule in the wrong position is effectively an incorrect rule.


Problem #3 — Duplicate iptables rules

During troubleshooting, some forwarding and NAT rules were accidentally duplicated.

For example:

ACCEPT wg0 → ens3
ACCEPT ens3 → wg0
ACCEPT wg0 → ens3
ACCEPT ens3 → wg0

and:

MASQUERADE → ens3
MASQUERADE → ens3

The duplicates weren’t necessary.

They were removed using:

sudo iptables -D FORWARD -i wg0 -o ens3 -j ACCEPT
sudo iptables -D FORWARD -i ens3 -o wg0 \
    -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

sudo iptables -t nat -D POSTROUTING -o ens3 -j MASQUERADE

Lesson

When troubleshooting firewall rules:

iptables -L -n -v --line-numbers

is much more useful than simply looking at the configuration file.

Packet counters also tell you whether a rule is actually being hit.


Step 7 — Test the VPN Before Full Tunneling

Initially, the client was configured with:

AllowedIPs = 10.50.0.0/24

This meant only traffic destined for the WireGuard network used the VPN.

For example:

ping 10.50.0.1

worked.

But Internet traffic still used the normal WSL connection.

Checking the route showed:

54.169.173.187 via 172.19.48.1 dev eth0

And the public IP was still the ISP’s CGNAT address:

1.187.214.31

That was expected.


Step 8 — Switch to a Full Tunnel

The actual goal was to use Oracle as the Internet egress.

So the client configuration was changed to:

AllowedIPs = 0.0.0.0/0

This tells WireGuard that IPv4 traffic should use the VPN.

After bringing the tunnel up:

sudo wg-quick up wg0

the route became:

1.1.1.1 dev wg0 table 51820

while the WireGuard endpoint itself was still routed outside the tunnel.


Problem #4 — Understanding WireGuard’s policy routing

At first glance, a full-tunnel configuration can look strange.

Running:

ip route show table 51820

showed:

default dev wg0 scope link

And:

ip rule

showed:

0:      from all lookup local
32764:  from all lookup main suppress_prefixlength 0
32765:  not from all fwmark 0xca6c lookup 51820
32766:  from all lookup main
32767:  from all lookup default

WireGuard was using a firewall mark:

0xca6c

and a dedicated routing table:

51820

This is part of how wg-quick implements a full tunnel without creating a routing loop.

The important practical test was:

ip route get 129.159.231.129

which returned:

129.159.231.129 via 172.19.48.1 dev eth0

while:

ip route get 1.1.1.1

returned:

1.1.1.1 dev wg0 table 51820

This is exactly what we wanted.

The VPN endpoint must remain reachable through the normal network, while general Internet traffic goes through WireGuard.


Step 9 — Verify the Public Egress IP

This was the most important test:

curl -4 https://checkip.amazonaws.com

With WireGuard enabled:

129.159.231.129

Success.

At this point, the complete path was working:

WSL
 ↓
10.50.0.2
 ↓
WireGuard
 ↓
10.50.0.1
 ↓
Oracle NAT
 ↓
Internet
 ↓
129.159.231.129

This is the IP that can be whitelisted on AWS security groups and office firewalls.


Step 10 — Don’t Automatically Add a Kill Switch

A common WireGuard recommendation is to configure a kill switch so that Internet traffic is blocked whenever the VPN is disconnected.

That isn’t always the right design.

For this particular use case, I wanted normal Internet connectivity to remain available.

The desired behavior was:

VPN UP
   ↓
VM traffic → Oracle static IP
   ↓
129.159.231.129

But if WireGuard goes down:

VPN DOWN
   ↓
Normal WSL networking
   ↓
CGNAT IP

The VM simply won’t accept the connection unless its security group/firewall temporarily allows the CGNAT IP.

This is actually useful operationally.

If the VPN is unavailable, I can temporarily whitelist my current IP rather than losing all Internet connectivity.


Step 11 — Verify the Fallback Behavior

This was tested explicitly.

First:

sudo wg-quick down wg0

Then:

curl -4 https://checkip.amazonaws.com

returned:

1.187.214.31

That’s the normal ISP/CGNAT address.

Then:

sudo wg-quick up wg0

and:

curl -4 https://checkip.amazonaws.com

returned:

129.159.231.129

So the fallback behavior was confirmed.

No complicated fail-closed firewall was necessary.


Step 12 — Run WireGuard Through systemd

Once the VPN was working manually, I wanted it to start automatically.

The configuration was already at:

/etc/wireguard/wg0.conf

So on WSL:

sudo systemctl enable wg-quick@wg0

This created the systemd symlink.

One initial mistake was starting the service while wg0 was already manually running.

The service failed with:

wg-quick: `wg0' already exists

That’s not a WireGuard configuration error.

The interface was simply already up.

The correct sequence was:

sudo wg-quick down wg0
sudo systemctl start wg-quick@wg0

Then:

sudo systemctl status wg-quick@wg0

and:

sudo wg show

should confirm the interface is managed by systemd.

Check whether it is enabled:

systemctl is-enabled wg-quick@wg0

Expected:

enabled

Adding a Phone as Another WireGuard Peer

WireGuard makes adding another device straightforward.

The phone doesn’t need to generate its own keys.

A keypair can be generated on another trusted machine.

For example:

umask 077

wg genkey | tee phone-private.key | \
    wg pubkey > phone-public.key

The phone gets its own VPN address:

10.50.0.3

while WSL remains:

10.50.0.2

The Oracle server configuration then contains another peer:

[Peer]
PublicKey = PHONE_PUBLIC_KEY
AllowedIPs = 10.50.0.3/32

The phone configuration looks like:

[Interface]
PrivateKey = PHONE_PRIVATE_KEY
Address = 10.50.0.3/24
DNS = 1.1.1.1

[Peer]
PublicKey = ORACLE_SERVER_PUBLIC_KEY
Endpoint = 129.159.231.129:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25

The same Oracle public IP can therefore be used as the egress point for both devices.

WSL ────────┐
10.50.0.2  │
            ├── WireGuard ── Oracle ── Internet
Phone ──────┘                    │
10.50.0.3                        ↓
                          129.159.231.129

For phones, a QR code is usually the easiest way to import the configuration into the WireGuard app.


A Note About Private Keys

During troubleshooting, it’s very easy to accidentally expose a private key while running commands such as:

sudo wg showconf wg0

Unlike:

sudo wg show

wg showconf can display the private key.

Never publish that output in a blog post, Git repository, support ticket, or chat.

If a private key has actually been exposed, rotate it.

The safe rule is:

Private key → stays on the device
Public key  → can be shared with the other WireGuard endpoint

Oracle VM Restart Considerations

Because the VPN server is running on an Oracle Cloud VM, the configuration needs to survive reboots.

The WireGuard service can be enabled with:

sudo systemctl enable wg-quick@wg0

The wg0.conf configuration contains the PostUp rules for forwarding and NAT.

Conceptually:

VM boots
   ↓
systemd
   ↓
wg-quick@wg0
   ↓
wg0 created
   ↓
PostUp
   ↓
iptables forwarding + MASQUERADE

The Oracle public IP should also be a reserved/static public IP if it is being used for firewall allowlisting.

That means the whitelist can remain:

129.159.231.129/32

instead of having to change whenever the VM restarts.


Troubleshooting Checklist

If the VPN doesn’t work, troubleshoot it from the bottom up.

1. Is WireGuard running?

sudo wg show

Look for:

latest handshake

If there’s no handshake, don’t troubleshoot Internet routing yet.


2. Can the client reach the VPN server?

ping 10.50.0.1

If this fails, fix the tunnel before looking at NAT.


3. Is UDP 51820 reaching the server?

On Oracle:

sudo tcpdump -ni ens3 udp port 51820

If packets arrive but there is no response, check:

  • iptables
  • firewalld
  • WireGuard configuration
  • Oracle Cloud security rules

4. Is forwarding enabled?

sudo sysctl net.ipv4.ip_forward

It should return:

net.ipv4.ip_forward = 1

5. Are forwarding rules in the right order?

sudo iptables -L FORWARD -n -v --line-numbers

Make sure the WireGuard ACCEPT rules appear before a general REJECT rule.


6. Is NAT configured?

sudo iptables -t nat -L POSTROUTING -n -v --line-numbers

You should see a MASQUERADE rule for the external interface.


7. What route is actually being used?

For the VPN endpoint:

ip route get 129.159.231.129

It should use:

eth0

For normal Internet traffic:

ip route get 1.1.1.1

With the full tunnel active, it should use:

wg0

8. What public IP does the Internet see?

curl -4 https://checkip.amazonaws.com

With VPN:

129.159.231.129

Without VPN:

your normal ISP/CGNAT IP

Final Architecture

After all the troubleshooting, the final setup is surprisingly simple:

                         ┌──────────────────────┐
                         │     Oracle Cloud     │
                         │                      │
                         │  Public IP           │
                         │ 129.159.231.129      │
                         │                      │
                         │  WireGuard            │
                         │  10.50.0.1           │
                         └──────────┬───────────┘
                                    │
                         NAT / MASQUERADE
                                    │
                              Internet
                                    │
                    ┌───────────────┴───────────────┐
                    │                               │
                 WSL2                            Phone
              10.50.0.2                        10.50.0.3
                    │                               │
                    └────────── WireGuard ──────────┘

The practical outcome is:

VPN connected:
    VM sees 129.159.231.129

VPN disconnected:
    normal Internet continues
    VM sees the normal CGNAT IP

This gives you a small, inexpensive VPN gateway that can provide a stable outbound IP for accessing resources that require IP allowlisting, without forcing all of your normal connectivity to depend on the VPN.

The biggest lesson from the setup wasn’t actually how to configure WireGuard.

It was learning to troubleshoot the complete network path:

Cloud firewall
      ↓
Linux firewall
      ↓
WireGuard handshake
      ↓
Routing
      ↓
IP forwarding
      ↓
NAT
      ↓
Internet
      ↓
Target VM security group

When those layers are checked one by one, WireGuard becomes much less mysterious.