Escaping CGNAT: Exposing Self-Hosted Services over IPv6 (No Public IPv4 Required)
I’ve just begun my self-hosting journey.
I repurposed an old laptop and installed Ubuntu Server on it. On top of
that, I spun up several services using Docker:
- Nextcloud – Personal cloud storage
- Vaultwarden – Lightweight password manager
- Pi-hole – DNS-level ad blocking
- Jellyfin – Media streaming server
I installed Ubuntu Server, configured Docker, and had everything running
in a single day. It was smooth, fun, and surprisingly satisfying.
Then came the final step: exposing my services to the public internet.
That’s when things got complicated.
The Problem: CGNAT
My ISP (Jio Fiber) uses CGNAT (Carrier-Grade NAT).
That means:
- My router does not get a public IPv4 address.
- Traditional port forwarding is impossible.
- Devices on the public internet cannot directly reach my server.
In simple terms: I had no public “return address”.
Options I Considered (and Why I Avoided Them)
I explored a few alternatives, but each came with trade-offs:
Buy a Public IPv4 from ISP
→ Extra cost and additional hassleUse a VPN (Tailscale, WireGuard, etc.)
→ Friends and family outside my network would need client setupUse a VPS or reverse proxy (e.g., Cloudflare Tunnel)
→ Introduces dependency and potential privacy concerns
I wanted something simpler and more native.
That’s when I rediscovered IPv6.
Why IPv6 Solves This
CGNAT exists because IPv4 addresses are exhausted.
IPv6 does not have this limitation.
With IPv6:
- Every device can have a globally routable address
- NAT is not required
- Port forwarding is unnecessary
- Direct end-to-end connectivity is restored
Tactical advantages I observed:
- ✅ No port forwarding required
- ✅ Reduced exposure to IPv4 bot noise
- ✅ Typically stable addressing (or use dynv6 if dynamic)
Here’s exactly what I did.
Step 1: Verify IPv6 Connectivity
What this does
Checks whether your ISP provides IPv6.
ip -6 addr show
Look for a global IPv6 address (starts with 2xxx: or 3xxx:).
Ignore fe80:: — those are link-local addresses.
ping6 -c 4 google.com
Validation
If ping6 succeeds → IPv6 works.
If not → Contact your ISP and ask them to enable IPv6.
Step 2: Enable IPv6 in Netplan
What this does
Configures Ubuntu Server to accept IPv6 router advertisements and
DHCPv6.
Edit your Netplan configuration:
sudo nano /etc/netplan/00-installer-config.yaml
Example configuration:
network:
version: 2
ethernets:
eno1: # Replace with your interface name
addresses:
- 192.168.29.100/24
routes:
- to: default
via: 192.168.29.1
nameservers:
addresses: [8.8.8.8, 8.8.4.4]
dhcp6: true
accept-ra: true
Apply changes:
sudo netplan apply
Validation
curl -6 ifconfig.co
If this returns an IPv6 address, you’re good to go.
Step 3: Configure Firewall Rules (Critical)
With IPv6, your server is globally reachable.
That makes firewall configuration extremely important.
Using UFW:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
That’s it. I only exposed HTTP and HTTPS.
⚠️ Do not expose unnecessary ports.
⚠️ I did NOT expose Pi-hole publicly.
Everything runs behind HTTP/HTTPS via reverse proxy.
Step 4: Configure Nginx to Listen on IPv6
Inside your server block:
server {
listen 80;
listen [::]:80;
server_name example.com;
location / {
proxy_pass http://172.18.0.2:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
For HTTPS:
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name example.com;
ssl_certificate /etc/ssl/certs/fullchain.pem;
ssl_certificate_key /etc/ssl/private/privkey.pem;
location / {
proxy_pass http://172.18.0.2:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
The important part:
listen [::]:80;
listen [::]:443 ssl;
That ensures Nginx accepts IPv6 connections directly.
Step 5: Forwarding to Docker Containers
My applications run inside Docker.
Docker uses private IPv4 subnets internally (e.g., 172.18.0.0/16).
To find the container IP:
docker inspect <container_name> | grep IPAddress
Nginx (host) proxies traffic to that internal Docker IP.
Everything up to this point happens on Ubuntu Server.
Next: DNS and router configuration.
Step 6: DNS Configuration (AAAA Record)
To make the service reachable:
Add an AAAA record pointing to your IPv6 address.
Example:
mydomain.dynv6.net → AAAA → 2001:db8:abcd::1234
I used dynv6 because it’s simple and works well for dynamic IPv6
addresses.
Now clients accessing the domain will connect via IPv6.
Step 7: Jio Router IPv6 Firewall Rules
Before opening access:
- Use strong firewall rules
- Install Fail2ban
- Keep services updated
- Use HTTPS only
- Consider rate limiting
Login to your router admin page:
http://192.168.29.1
Go to:
Security → Firewall → IPv6 Firewall Rules
Add rules only for the ports you want (80 and 443).
⚠️ The more ports you expose, the larger your attack surface.
Final Result
Once configured:
- Your services are accessible from any IPv6-enabled network
- No public IPv4 needed
- No CGNAT workaround
- No VPS
- No port forwarding
Just clean, native IPv6 connectivity.
Closing Thoughts
Self-hosting taught me:
- Networking fundamentals matter
- IPv6 is underused but powerful
- CGNAT is frustrating — but not unbeatable
If you’re stuck behind CGNAT, try IPv6 before paying for a VPS.
If you’ve faced similar pain points, drop them in the comments — it
might help someone else.
Good day! 🚀
Top comments (0)