The IPv6 DDNS Adventure: When CGNAT Forces Your Hand


My IPv4 DDNS had been working perfectly for years until I switched ISPs. The new provider enforces CGNAT with no way out – no opt-out option, no static IP available. My public IPv4 was now shared with other customers, making direct access impossible.

I suppose it’s justifiable as the pool of IPv4 addresses keeps shrinking. It’s amazing how humanity managed to nearly deplete all available IPv4 addresses (2³² = 4,294,967,296 theoretically) in just a few decades!

My thoughts naturally turned to IPv6. There’s practically no good reason to enforce IPv6 CGNAT at all! Even the common practice of assigning dynamic IPv6 addresses for every connected device seems, in my opinion, purely designed to encourage consumers to pay for “business level” or “premium level” internet. With about 3.4 × 10³⁸ addresses available – an astronomically large number – that’s roughly 4.8 × 10²⁸ addresses for every person on Earth given our current population (which is actually stagnating, even showing trends of reduction).

So IPv6 wasn’t optional anymore – it was IPv6 or no external access at all.

The Setup

But first, why do I need to access my personal PC from the internet?! I’ve got Ubuntu WSL running on my Windows machine, and I can only execute Claude Code on it. Exposing an SSH connection to it would be ideal for my workload.

Setting expectations helps in limiting and securing the exposure of my personal home machine to the wide and wild world out there. Don’t underestimate that while you think you’re just accessing your home security camera systems or your Plex server, someone else might have been accessing them too – for as long as you have!

First things first, I needed to find a free IPv6 DDNS service. A quick Google search showed me plenty of options like DuckDNS, No-IP, and Dynu. I ended up choosing freemyip.com for its quick getting-started process, no requirement for signing up, and multiple easy ways to update my IPv6. Perfect for the automated script I had in mind.

And since I’m on my vibe-coding journey, it was only natural to ask Claude to help me out.

First Try: WSL2 Directly

Ideally, the update script should execute periodically inside my WSL instance, which is literally just an Ubuntu VM running on my Windows machine. But there were a few hurdles to overcome.

First of all, IPv6 support is “supposedly working”, but in reality, it’s not quite stable – at least not with the current WSL 2.5.9.0. I ran into multiple networking issues. The WSL GitHub issues page is full of discussions about IPv6 problems, and I encountered NAT issues that many others have reported. This pointed me towards trying the Mirrored network mode, which seemed to work better for me, but there’s no way to directly access the WSL instance in that networking mode.

Interestingly, Claude, without the “correct” context, happily suggested all the changes I could make to my WSL Ubuntu to “make it work”. I did tell Claude I was configuring my WSL instance which is Ubuntu. I guess that wasn’t specific enough then!

update_ipv6.sh - Initial WSL cron job attempt
#!/bin/bash

# Check if verbose flag is provided
VERBOSE=false
if [ "$1" = "--verbose" ]; then
    VERBOSE=true
fi

TOKEN="[YOUR_TOKEN_HERE]"
DOMAIN="[YOUR_DOMAIN]"

# Get current IPv6 address
if [ "$VERBOSE" = true ]; then
    echo "Getting current IPv6 address..."
    IPV6=$(curl -s https://ipv6.icanhazip.com)
    echo "Current IPv6: $IPV6"
else
    IPV6=$(curl -s https://ipv6.icanhazip.com)
fi

# Check if we got a valid IPv6 address
if [ ! -z "$IPV6" ]; then
    # Build the URL with verbose parameter if needed
    if [ "$VERBOSE" = true ]; then
        URL="https://freemyip.com/update?token=$TOKEN&domain=$DOMAIN&myip=$IPV6&verbose=yes"
        echo "Updating freemyip.com with URL: $URL"
        echo "Response:"
        curl "$URL"
        echo ""
    else
        curl -s "https://freemyip.com/update?token=$TOKEN&domain=$DOMAIN&myip=$IPV6" >/dev/null 2>&1
    fi
else
    if [ "$VERBOSE" = true ]; then
        echo "Error: Could not retrieve IPv6 address"
    fi
fi

Accessing via Windows Host

Well, I needed a script to update my Windows PC’s IPv6 to freemyip.com then. I asked Claude for a PowerShell script, and it quickly wrote one for me. But I wanted it to execute periodically every 20 minutes. I don’t know why I picked that number, but for whatever reason, it seemed to be a sweet spot for my new ISP – I could see my IPv6 was dynamically reassigned sometimes twice within a 30-minute window!

As expected, the first version of Claude’s PowerShell script “worked”, in the sense that it updated freemyip.com correctly. But it tried a “smart” approach by getting the IPv6 from the current local network interface and using that value, rather than using an external service to get what IPv6 “outsiders” see us as.

The problem is my PC’s local network interface always reports a different IPv6 value from the public value the world sees. That’s probably expected behaviour since IPv6 provides a much bigger pool of addresses, so no IPv6-enabled host should worry too much about clashing addresses. From my quick research, a host just needs a network prefix package (either in /56 or /64 spaces) and can pick a random address, then use a neighbour discovery package to ensure it’s not clashing on the local network.

I had to instruct Claude to use external IPv6 all the time, by getting it from a free service like ipify.org or icanhazip.com. And now my DDNS reports the same external IPv6 I saw. Great!

Installing the DDNS update script as a task to run every 20 minutes proved to be not as straightforward in Windows as a crontab in Linux. Of course, I didn’t do it manually – I told Claude to write me a script to install my script as a task. And it failed miserably.

While the first version of the script “worked” in the sense that it created the Windows task correctly, the task itself couldn’t start. Due to Windows restrictions on user privileges, Claude couldn’t help with that. I spent half an hour but just couldn’t be bothered to spend more time “fixing” all the privileges, policies, group policies, etc. for my single home PC. Plus, the way Microsoft thinks all personal users should have a personal Microsoft account to log in to their personal PC, with “hacky” interfaces hiding the admin interfaces for managing users on local PC, just made it a bigger-than-few-minutes hurdle to overcome.

Well, I had a super coder/scripter at my command, so I told Claude to “fix it”, and it suggested installing the script as a Windows Service. It ran wild after that, providing me with at least four versions with broken code (broken syntax, unclosed quoted strings, etc.). It couldn’t seem to recognise where it went wrong and fix itself. I’m not sure if this called for a “Subject Matter Expert” to step in.

Skimming through the code, I could tell which lines were having problems, and just by prompting Claude to fix line XX and XX, it fixed itself! Maybe this is another form of “hallucination”? It thought whatever it wrote couldn’t be broken syntactically?

Windows Service didn’t work either. To be fair, the script worked if it is executed manually in a PowerShell terminal. It was some configuration in Windows preventing the script from executing. I asked Claude to think harder and fix the issue – of course, I pasted what I saw in the Event Viewer log. And it kind of had an “ah-ha” moment, then proceeded to rewrite the script back to a Task Scheduler task, but this time it worked!

Here’s the final working PowerShell script that successfully created a task to update my IPv6 every 20 minutes to freemyip.com. You can download the complete script: IPv6DDNSService.ps1

Here comes the last hurdle:

Come the World of Firewalls and Port Forwarding

I’m no stranger to port forwarding and firewall configuration, but it still took me about an hour to get everything working the way I wanted.

First things first, I couldn’t ping my IPv6 DDNS, but shortly after I found out I needed ping -6 (to route through IPv6). Then I needed to set up SSH in my WSL, and I wanted OTP for the SSH connection. I’d done this a few times with my personal VPS but hadn’t bothered to keep documentation of how I did it.

Setting up OpenSSH with 2FA

Setting up SSH with Google Authenticator 2FA was straightforward - I just asked Claude for the commands and it walked me through installing openssh-server and libpam-google-authenticator, configuring PAM, and updating the SSH daemon settings. The whole process took about 5 minutes, and everything worked on the first try.

If you need to do something similar, just ask your AI assistant for a step-by-step guide - it’s often quicker and more tailored than hunting through Google!

Handle Ubuntu 22.10+ systemd socket activation

Here’s where I hit a snag! Starting from Ubuntu 22.10, OpenSSH uses systemd socket activation by default, which caused some confusion during my WSL setup when restarting sshd kept failing. Instead of the traditional service management, SSH is now managed through systemd sockets.

If you’re running into issues (like I did), check out this Ubuntu Discourse thread that explains the change.

For me I don’t really need to save 3MiB per intance, I have plenty, so I just disabled ssh.socket

Terminal window
systemctl disable --now ssh.socket
rm -f /etc/systemd/system/ssh.service.d/00-socket.conf
rm -f /etc/systemd/system/ssh.socket.d/addresses.conf
systemctl daemon-reload
systemctl enable --now ssh.service

Firewall Configuration

Now I needed to make sure my WSL2 instance was accessible from my Windows host. No matter what I tried, even though the WSL2 instance reported an IPv6 value internally, there was no way to connect to it using IPv6 – all attempts failed. I started to get frustrated. I couldn’t admit failure after all this work, right?

I was ready to pay for a static IPv4, but my ISP didn’t even make it an option for my “home internet plan”. Its AI chatbot suggested I needed to upgrade to a “premium business plan” to avoid CGNAT and get a static IP.

A thought ran through my brain: if IPv6 doesn’t work, maybe IPv6 to IPv4 would? A quick Google search confirmed my thought – it should work. I told Claude, and it gave me the correct netsh command to execute on my Windows host to forward a port from my external IPv6 to my internal IPv4 of the WSL instance!

The last steps were, of course, opening up the Windows Firewall and the router’s firewall for the port to work. Mentally, I’m aware that my network topology is quite simple: a local router connecting to the ISP’s network via NBN, and the router then assigns IPs to my local devices. So there were only two more hops I needed to configure. It still took some time though, since there’s no easy way to verify at which firewall the external connection was blocked. And Claude couldn’t help with that anyway.

Here’s the complete firewall configuration process I followed:

1. First - WSL Ubuntu UFW

Terminal window
# Enable UFW if not already enabled
sudo ufw enable
# Allow SSH on your chosen port
sudo ufw allow 22/tcp # or whatever port you chose
# Check the status
sudo ufw status verbose

2. Second - Windows Port Forwarding

Since WSL2 doesn’t directly support IPv6, we need to forward IPv6 traffic to WSL’s IPv4 address:

Terminal window
# Run PowerShell as Administrator
# Get WSL2 IP address
$wslIP = (wsl hostname -I).Split()[0]
Write-Host "WSL IP: $wslIP"
# Set up port forwarding from Windows IPv6 to WSL IPv4
# This forwards port 2222 on IPv6 to port 22 on WSL
netsh interface portproxy add v6tov4 listenport=2222 listenaddress=:: connectport=22 connectaddress=$wslIP
# Verify the port proxy is set up
netsh interface portproxy show all

3. Third - Windows Firewall Rules

Create firewall rules to allow incoming connections:

Terminal window
# Run PowerShell as Administrator
# Create inbound rule for IPv6 SSH access
New-NetFirewallRule -DisplayName "SSH IPv6 Access" `
-Direction Inbound `
-Protocol TCP `
-LocalPort 2222 `
-Action Allow `
-Profile Any `
-RemoteAddress Any
# Alternative method using netsh (if New-NetFirewallRule doesn't work)
netsh advfirewall firewall add rule name="SSH IPv6 Access" `
dir=in action=allow protocol=TCP localport=2222 `
profile=any remoteip=any

Or just create an Inbound rule on Windows Firewall Screenshot of Windows Defender Firewall with Advanced Security

4. Fourth - Router Configuration

In my case, I’m using an ASUS router with Asuswrt-Merlin firmware. Every router is different, but here are the general steps:

  1. Log into router’s admin panel (usually 192.168.1.1 or 192.168.0.1)
  2. Navigate to IPv6 under Advanced Settings
  3. Note down the LAN IPv6 Address and the Prefix Length
  4. Add a new rule in Firewall, also under Advanced Settings
    • Service Name: SSH Access (or any descriptive name)
    • Protocol: TCP
    • Port Range: 2222 (or your chosen port)
    • Local IP: Sommething like this e522:6e65:4b34:3ab1::1/56
  5. Apply/Save the settings

A note on my IPv6 range choice: You might wonder why I opened a whole /56 subnet instead of a single IPv6 address. Here’s the thing – Windows has this “helpful” feature called IPv6 Privacy Extensions that generates new temporary addresses every 24 hours. I could disable it and use a static IPv6, but I went with the lazy option: open the port for my entire subnet range.

Is it the most secure approach? Definitely not. But since my Windows PC is the only one device using IPv6 on my home network (everything else is still happily on IPv4), the risk is minimal. Plus, it saves me from future troubleshooting sessions when Windows decides to play musical chairs with its IPv6 addresses.

Troubleshooting Tips:

  • If connection times out, check each firewall layer one by one
  • Use tcpdump or wireshark to see where packets are being dropped
  • Temporarily disable Windows Firewall to isolate issues
  • Check if your ISP blocks common ports (some block 22, try 2222 or higher)
  • Ensure your router has IPv6 enabled and properly configured

In the end, after configuring all four firewall layers, everything finally worked! As I write this, I’m SSH’d into my WSL instance from a coffee shop, typing away in nvim through Termius. The IPv6 DDNS updates reliably every 20 minutes, and my connection has been rock solid.

This whole adventure took me from IPv4 nostalgia through IPv6 confusion to a working solution. If you’re facing similar CGNAT woes, I hope this helps you find your own path through the maze of firewalls and port forwarding. Sometimes the solution isn’t pretty, but hey – if it works, it works!

Key Takeaways

  • IPv6 is increasingly becoming the only option for home users due to IPv4 depletion and CGNAT
  • WSL2’s IPv6 support is still a bit wonky, but there are workarounds
  • Sometimes the solution isn’t fixing what’s broken, but finding an alternative path (IPv6 to IPv4 forwarding)
  • Having an AI assistant is great, but knowing when to guide it is crucial
  • Document your setup – future you will thank present you!