🇨🇱 - Computer Engineer
Self-hosting applications at home is a great way to have full control over your data and privacy. However, ISP services often have your home network behind a Carrier-Grade NAT (CGNAT), which changes the IP address of the home network, making it impossible to access the home server from the outside using the public IP address.
This guide will show you how to bypass CGNAT with a virtual machine at a cloud provider as a router, using Wireguard VPN.
Network Address Translation (NAT) is a technique used to allow multiple devices to share a single IP address on a network.
Carrier-Grade NAT (CGNAT) is a large-scale NAT implementation used by Internet Service Providers (ISPs) to conserve IPv4 addresses. With CGNAT, your ISP assigns a single public IP address to multiple customers, essentially putting your home network behind two layers of NAT:
This double-NAT situation creates significant problems for home users who want to:
Traditional port forwarding doesn't work with CGNAT because you don't have a dedicated public IP address that you can configure. Your router may think it has a public IP, but it's actually a private IP within the ISP's network.
The increasing adoption of CGNAT is driven by the global shortage of IPv4 addresses. While IPv6 would solve this problem, its adoption has been slow, making workarounds like the one described in this guide necessary for many home users.
The solution to bypass CGNAT involves setting up a virtual machine (VM) in the cloud that acts as an intermediary router between the internet and your home network. Here's how it works:
Cloud VM with Public IP: You set up a VM at a cloud provider that has a real, dedicated public IP address.
Wireguard VPN Tunnel: You establish a secure VPN tunnel between your home server and the cloud VM using Wireguard, which is fast, modern, and secure.
Port Forwarding: The cloud VM is configured to forward specific ports (like 80 for HTTP, 443 for HTTPS) to your home server through the VPN tunnel.
NAT Rules: The VM uses Network Address Translation (NAT) rules to route traffic between the internet and your home network.
This setup effectively gives your home server a presence on the internet with a real public IP address (the one belonging to your cloud VM). When someone connects to your cloud VM's IP address or domain name, the traffic is securely tunneled to your home server.
The advantages of this approach include:
Let's see how to set this up step by step.
The first step is to create a virtual machine at a cloud provider.
There are several options available, the best one being Oracle Cloud, because their free tier allows you to keep a VM running for free. The VM.Standard.E2.1.Micro shape, with 1 OCPU (2 vCPUs) and 1 GB of RAM, is more than enough for this purpose.
However, I'm going to use Digital Ocean, because I also need a DNS server for more than one domain, which is not possible with Oracle within the free tier, and I also feel comfortable with their interface and support. For this guide, I'm going to use a Droplet with 512 MB of RAM, 1 CPU core and 10 GB of storage.
After creating the VM, we need to generate a DNS record (A record) for the domain name in case of using one, pointing it to the public IP address of the VM.
After setting up the VM and connecting to it via SSH as the root user, the first thing to do is to update the packages and the system.
apt update apt upgrade -y
After that, we need to create a new user for the VM with sudo privileges.
adduser <username> usermod -aG sudo <username>
Then we switch to the new user and generate an SSH key for this user, if not done when creating the VM.
su - <username> ssh-keygen -t ed25519 -C "your_email@example.com" -f ~/.ssh/id_ed25519 -N "" cat ~/.ssh/id_ed25519.pub >> ~/.ssh/authorized_keys
We then retrieve the private key and save it in a safe place.
cat ~/.ssh/id_ed25519
After that, we need to disable password authentication, enable key-based authentication, disable root login and restart the SSH service.
sudo sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config sudo sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config sudo sed -i 's/#PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config sudo systemctl restart sshd
Before starting the Wireguard configuration, we need to change the home server's SSH port to a non-standard port, to avoid conflicts with the default port 22, which is the port that will be used to connect to the VM. This is necessary because we need to differentiate between the SSH connection to the VM and the SSH connection to the home server.
sudo sed -i 's/#Port 22/Port 9098/' /etc/ssh/sshd_config sudo systemctl restart sshd
We arbitrarily chose port 9098, but you can choose any other port that is not in use.
Wireguard is a fast, secure and easy to configure VPN. We are going to use it to create a VPN between the VM and the local network, which we will use to redirect all the traffic to the VM.
The first step is to enable IP forwarding at the VM:
sudo sysctl -w net.ipv4.ip_forward=1
and then we need to make it persistent across reboots by adding the following line to the /etc/sysctl.conf
file:
net.ipv4.ip_forward=1
After that, we need to install the Wireguard package, at both the VM and the home server.
sudo apt install -y wireguard wireguard-tools
Then, on both servers, we need to generate a private and public key for the Wireguard interface.
sudo su - wg genkey | tee /etc/wireguard/private.key > /dev/null wg pubkey < /etc/wireguard/private.key | tee /etc/wireguard/public.key > /dev/null
The private key is saved in the /etc/wireguard/private.key
file and the public key is saved in the /etc/wireguard/public.key
file.
Now, we need to create the Wireguard configuration file at both servers. The contents of the /etc/wireguard/wg0.conf
file at the home server are the following:
[Interface] PrivateKey = <contents of /etc/wireguard/private.key> Address = 10.1.0.2/32 [Peer] PublicKey = <contents of /etc/wireguard/public.key of the VM> AllowedIPs = 0.0.0.0/0 Endpoint = <public IP of the VM>:55108
Similarly, the contents of the /etc/wireguard/wg0.conf
file at the VM are the following:
[Interface] PrivateKey = <contents of /etc/wireguard/private.key> ListenPort = 55108 Address = 10.1.0.1/32 PreUp = iptables -t nat -A PREROUTING -d <public IP of the VM> -p tcp --match multiport --dport 80,443,9098 -j DNAT --to-destination 10.1.0.2 PreUp = iptables -t nat -A POSTROUTING -s 10.1.0.2/32 -j SNAT --to-source <public IP of the VM> PostDown = iptables -t nat -D PREROUTING -d <public IP of the VM> -p tcp --match multiport --dport 80,443,9098 -j DNAT --to-destination 10.1.0.2 PostDown = iptables -t nat -D POSTROUTING -s 10.1.0.2/32 -j SNAT --to-source <public IP of the VM> [Peer] PublicKey = <contents of /etc/wireguard/public.key of the home server> AllowedIPs = 10.1.0.2/32
Some breakdown of the configuration:
[Interface]
refers to the machine that is using this configuration. We set the private key and the address of the Wireguard interface.
[Peer]
refers to the machine that will be connected to this Wireguard interface. We set the public key and the allowed IPs.
PrivateKey
and PublicKey
are the private and public keys of the Wireguard interface that we generated in the previous steps.
Address
is the IP address of the Wireguard interface. This is arbitrary, as long as it's not in the same subnet as the local network of the home server. Both servers must have the same subnet, in this case 10.1.0.0/24
.
ListenPort
is the port that Wireguard will listen on for incoming connections.
PreUp
and PostDown
are commands that are executed before the interface is brought up and after it is brought down, respectively. In this case, we're using iptables to set up NAT rules:
AllowedIPs
defines which traffic should be routed through the Wireguard tunnel. For the VM, we only want traffic destined for the home server (10.1.0.2/32). For the home server, we set it to 0.0.0.0/0 to route all traffic through the VM.
Finally, we can start the Wireguard interface on both servers and enable it to start automatically on boot:
sudo systemctl start wg-quick@wg0 sudo systemctl enable wg-quick@wg0
We can try to access the home server from the outside using telnet
:
telnet <public IP of the VM> 80
In case of having a domain name, you can also use it:
telnet <domain name> 80
Regarding SSH, we can use the domain name to connect to the VM:
ssh -i <path to the private key> <username>@<domain name or public IP of the VM>
And we can also use the domain name to connect to the home server, using the custom port we defined earlier:
ssh -i <path to the private key> <username>@<domain name or public IP of the VM> -p 9098
Be sure to protect the SSH connection to the home server, as it will be exposed to the public internet. Use a strong password or key-based authentication, and consider using fail2ban
to prevent brute force attacks.
We now have a VM in the cloud that is acting as a router, allowing us to access our home network from anywhere using its public IP address, despite the carrier-grade NAT changing the IP of the home network. Self-hosted services at home can now be accessed from the outside world.
This setup provides a cost-effective way to bypass CGNAT restrictions while maintaining security through encrypted VPN tunnels. It's particularly useful for those who want to self-host services but are limited by their ISP's network configuration.
Credits to this blog post for the NAT and CGNAT diagrams.