Tag Archives: ipv6

Configuring CenturyLink IPv6 (6rd) on a UniFi UXG-Pro

If you’re using CenturyLink Fiber with a UniFi device, you may have noticed that getting IPv6 working isn’t quite as straightforward as enabling it in the UniFi Network application.

While UniFi supports IPv6, CenturyLink’s legacy network still relies on a technology called 6rd (IPv6 Rapid Deployment) rather than native IPv6 in many areas. Unfortunately, the UXG-Pro doesn’t provide a native 6rd configuration option through its management interface.

After spending some time digging through various GitHub repositories, UniFi community posts, and the underlying Linux kernel configuration, I was able to get IPv6 working using a manually configured tunnel.

This blog post is more of a reflection of how I was able to get IPv6 working with CenturyLink, rather than a supported guide that will give you IPv6 and guarantee it will work indefinitely. I’ve tried to explain why and what is going on where applicable to capture a working solution at least short-term. Yay for access to IPv6 only resources now!

My network setup

For reference, here is the environment I’m working with at the time I’m writing this post:

ComponentConfiguration
Internet providerCenturyLink Fiber
ISP networkCENTURYLINK-US-LEGACY-QWEST
OrganizationCenturyLink Communications, LLC
Network topologyCenturyLink Fiber → ONT → UXG-Pro
GatewayUbiquiti UniFi UXG-Pro
UXG-Pro firmware5.1.26
UniFi Network applicationOfficial – 10.6.106
WAN authenticationPPPoE
WAN VLAN201
Public IPv4Static assignment via PPPoE
IPv6 technology6rd
CenturyLink 6rd prefix2602::/24
CenturyLink 6rd relay205.171.2.64

One important distinction is that I’m using a static public IPv4 address. Since CenturyLink’s 6rd implementation derives your IPv6 prefix directly from your public IPv4 address, having a static assignment makes the configuration considerably easier.

That said, most residential customers probably don’t have a static public IPv4 address. I have a script later in this guide that calculates the correct IPv6 prefix from your current WAN IPv4 address, so the tunnel configuration can work with either static or dynamic assignments, but there is one additional consideration with dynamic addresses: when your public IPv4 changes, your LAN’s IPv6 prefix also needs to change. I’ll explain that limitation later.

Before digging in, what is IPv6 6rd and how does it work?

6rd stands for IPv6 Rapid Deployment and is defined in RFC 5969.

The technology allows an ISP to deliver IPv6 connectivity across an existing IPv4 network without having to deploy native IPv6 throughout its entire infrastructure.

Instead of your router sending IPv6 packets directly over an IPv6-enabled WAN connection, the router encapsulates IPv6 packets inside IPv4 packets and sends them to a designated ISP relay.

The relay then forwards the traffic to the native IPv6 Internet.

Here’s roughly what that looks like:

LAN devices
    |
    | Native IPv6
    |
UniFi UXG-Pro
    |
    | IPv6 encapsulated in IPv4
    | (SIT tunnel / IPv4 protocol 41)
    |
CenturyLink IPv4 network
    |
    |
CenturyLink 6rd Border Relay
205.171.2.64
    |
    | Native IPv6
    |
IPv6 Internet

In my environment, CenturyLink uses the following settings:

ParameterValue
6rd IPv6 prefix2602::/24
IPv4 mask length0
Border relay IPv4205.171.2.64
Tunnel MTU1472

Note: these settings are specific to the CenturyLink legacy deployment I’m using and shouldn’t automatically be assumed for every CenturyLink or Quantum Fiber connection.

How CenturyLink calculates your IPv6 prefix

The interesting part is how CenturyLink assigns IPv6 addresses.

With a /24 6rd prefix and an IPv4 mask length of zero, the router’s entire 32-bit public IPv4 address is embedded directly into the IPv6 prefix.

That gives us:

24-bit IPv6 prefix
+ 32-bit public IPv4 address
----------------------------
56-bit IPv6 prefix

In other words, each public IPv4 address maps to its own /56 IPv6 prefix. A /56 provides 256 individual /64 subnets, which is more than enough for a typical home network.

For example, you could use one /64 for your default LAN, another for IoT devices, and additional subnets for guest or isolated networks.

It’s worth noting that 6rd isn’t NAT64 or an IPv4-to-IPv6 address translator. Your devices receive globally routable IPv6 addresses. The UXG-Pro simply transports their IPv6 packets across CenturyLink’s IPv4 infrastructure.

This also means you can host services over IPv6, provided you configure the appropriate firewall rules and the CenturyLink relay allows inbound connectivity.

Why the existing UniFi 6rd scripts didn’t work

While researching how to configure this, I came across the following GitHub repository: https://github.com/msimerson/unifi

The repository includes a script for configuring 6rd on UniFi gateways. Its README describes testing on a UXG-Max and indicates that the approach should work on the UXG-Pro and other UniFi gateways.

Unfortunately, that wasn’t the case with my UXG-Pro running firmware 5.1.26.

The repository relies on native Linux 6rd tunnel functionality, including a command similar to:

ip tunnel 6rd dev 6rd 6rd-prefix 2602::/24

When I ran the command, the router returned:

6rd: ioctl 89f9 failed: Invalid argument

To understand why, I checked the kernel configuration directly:

zcat /proc/config.gz 2>/dev/null | grep IPV6_SIT

The output was:

CONFIG_IPV6_SIT=y
# CONFIG_IPV6_SIT_6RD is not set

This tells us two important things:

  • CONFIG_IPV6_SIT=y means the Linux kernel supports IPv6-in-IPv4 tunneling using SIT interfaces.
  • CONFIG_IPV6_SIT_6RD is not set means the kernel was built without the specialized 6rd functionality.

This explains why the repository’s native 6rd command fails on my gateway, despite the README suggesting compatibility with the UXG-Pro.

The kernel supports the basic encapsulation needed to transport IPv6 inside IPv4, but it doesn’t support configuring the additional 6rd parameters with ip tunnel 6rd.

The workaround is to use a standard SIT tunnel with an explicitly configured CenturyLink relay.

Because CenturyLink provides a known border relay, we can manually create the tunnel, assign our derived IPv6 address, and configure routing without relying on the kernel’s native 6rd functionality.

Step 1: Connect to the UXG-Pro and identify your WAN interface

First, SSH into your UXG-Pro and access its debug shell using the Debug option from the UniFi Network console. Once you have shell access open, confirm that your PPPoE interface has an IPv4 address:

ip -4 addr show dev ppp0

In my environment, ppp0 is the PPPoE interface that receives my static public IPv4 address.

You should see something similar to:

inet YOUR_PUBLIC_IPV4 peer ... scope global ppp0

If you’re not using PPPoE, your IPv4 address may be assigned to a different interface.

You can list all IPv4 addresses using:

ip -4 addr show

Once you’ve identified the appropriate interface, you can extract its IPv4 address directly:

ip -4 -o addr show dev ppp0 |
    awk 'NR==1 {split($4,a,"/"); print a[1]}'

We’ll use this address when creating the tunnel.

Next, verify that the kernel supports SIT tunneling:

zcat /proc/config.gz 2>/dev/null | grep IPV6_SIT

As explained above, my UXG-Pro supports SIT but doesn’t have native 6rd functionality enabled.

Step 2: Calculate your CenturyLink IPv6 prefix

Before creating the tunnel, we need to determine which IPv6 addresses CenturyLink will route to us.

Rather than manually calculating these values every time, we can derive them from the current public IPv4 address.

For illustration, consider the reserved documentation IPv4 address:

192.0.2.1

Converting its individual octets into hexadecimal gives:

IPv4 octetHexadecimal
192C0
000
202
101

When combined with CenturyLink’s 2602::/24 prefix, this produces:

2602:00c0:0002:0100::/56

Which can be shortened to:

2602:c0:2:100::/56

From that /56, we can assign addresses to our tunnel and LAN networks.

PurposeExample
Customer 6rd prefix2602:c0:2:100::/56
Tunnel IPv6 address2602:c0:2:100::1/128
Default LAN subnet2602:c0:2:101::/64
Default LAN gateway2602:c0:2:101::1

The important thing to remember is that your IPv6 prefix isn’t arbitrary. It needs to be derived from your public IPv4 address.

Automatically calculate your IPv6 addresses

Instead of converting each octet manually, we can use Linux’s awk command.

First, obtain the current WAN IPv4 address:

WAN4=$(ip -4 -o addr show dev ppp0 |
    awk 'NR==1 {split($4,a,"/"); print a[1]}')

Next, calculate the /56 prefix:

PREFIX=$(echo "$WAN4" | awk -F. '{
    printf "2602:%x:%x:%x::/56\n",
        $1, ($2 * 256) + $3, $4 * 256
}')

echo "$PREFIX"

This produces the same /56 prefix that CenturyLink derives from your public IPv4 address.

However, because the 6rd prefix is /24, the precise bit placement matters. The first IPv4 octet occupies the low byte of the second IPv6 hextet, while the next two octets occupy the third hextet.

The final IPv4 octet occupies the high byte of the fourth hextet.

For our example, the result is:

2602:c0:2:100::/56

We can then select the first /64 for the tunnel and use additional /64 networks for our LANs and VLANs.

For example:

2602:c0:2:100::/64    Tunnel
2602:c0:2:101::/64    Default LAN
2602:c0:2:102::/64    IoT
2602:c0:2:103::/64    Guest
2602:c0:2:104::/64    Servers

We’ll automate these calculations in the restore script later in the article.

Step 3: Configure the IPv6 firewall

I strongly recommend configuring the tunnel manually before attempting to automate anything to prove the 6rd tunnel will work with your CenturyLink service. First I would start with the firewall configuration.

Unlike typical residential IPv4 networks, IPv6 doesn’t require NAT for devices to reach the Internet. Each client can receive its own globally routable IPv6 address. This means we need to ensure inbound firewall rules deny inbound traffic from the internet.

I added two rules to restrict unsolicited inbound IPv6 traffic arriving through the tunnel.

Block unsolicited inbound connections to LAN clients

ip6tables -I FORWARD 1 -i cl6rd \
    -m conntrack ! --ctstate ESTABLISHED,RELATED \
    -j DROP

Here’s what the command does:

ParameterDescription
-I FORWARD 1Inserts the rule at the beginning of the forwarding chain
-i cl6rdMatches packets arriving through the tunnel
-m conntrackEnables connection-state matching
! --ctstate ESTABLISHED,RELATEDMatches packets that aren’t part of an established or related connection
-j DROPDrops matching traffic

Essentially, devices on the LAN can initiate IPv6 connections, but unsolicited inbound connections arriving through the tunnel are dropped.

Protect the UXG-Pro itself

Next, add a similar rule for traffic destined directly to the gateway:

ip6tables -I INPUT 1 -i cl6rd \
    -m conntrack ! --ctstate ESTABLISHED,RELATED \
    -j DROP

This protects services running directly on the UXG-Pro.

You can inspect the rules using:

ip6tables -L INPUT -n -v
ip6tables -L FORWARD -n -v

These are deliberately restrictive inbound rules, not a complete IPv6 firewall policy. If you want to host publicly accessible services over IPv6, you’ll need to add specific inbound exceptions.

Note: you should also make sure your overall IPv6 firewall policy doesn’t block required ICMPv6 functionality, particularly Packet Too Big messages used for Path MTU Discovery.

Step 4: Manually configure the 6rd tunnel

These steps will let us verify that the Linux kernel UniFi is using supports the workaround, CenturyLink’s relay is reachable, and IPv6 connectivity actually works. Once everything is functional, we can move on to the script.

Create the SIT tunnel

First, capture the current public IPv4 address:

WAN4=$(ip -4 -o addr show dev ppp0 |
    awk 'NR==1 {split($4,a,"/"); print a[1]}')

Next, create the tunnel:

ip tunnel add cl6rd mode sit \
    local "$WAN4" \
    remote 205.171.2.64 \
    ttl 64

Here’s what the command does:

ParameterDescription
ip tunnel add cl6rdCreates a tunnel interface named cl6rd
mode sitEnables IPv6 encapsulation inside IPv4
local "$WAN4"Uses the current public IPv4 address
remote 205.171.2.64Sets CenturyLink’s border relay
ttl 64Sets the encapsulating IPv4 packet’s time-to-live

Unlike native 6rd configuration, we’re explicitly specifying where the IPv6-in-IPv4 packets need to go.

Enable the tunnel interface

Next, bring the tunnel online and set its MTU:

ip link set dev cl6rd mtu 1472 up

The MTU is important because the tunnel introduces additional overhead. A typical Ethernet connection supports an MTU of 1500 bytes. PPPoE adds 8 bytes of overhead, leaving 1492 bytes for the IPv4 packet. SIT encapsulation adds another 20-byte IPv4 header.

That gives us:

1500 - 8 (PPPoE) - 20 (IPv4 tunnel) = 1472

This is the appropriate tunnel MTU for my PPPoE configuration. If you’re using a connection without PPPoE, the appropriate MTU may differ.

Assign an IPv6 address to the tunnel

Next, calculate the tunnel IPv6 address from the public IPv4 address:

TUNNEL_IP=$(echo "$WAN4" | awk -F. '{
    printf "2602:%x:%x:%x::1/128\n",
        $1, ($2 * 256) + $3, $4 * 256
}')

Then assign it:

ip -6 addr add "$TUNNEL_IP" dev cl6rd

The /128 assigns a single IPv6 address to the tunnel interface.

We’re not advertising a subnet over this interface. The tunnel serves as the gateway’s connection to CenturyLink’s IPv6 infrastructure.

Configure IPv6 routing

Next, tell Linux to send globally routable IPv6 traffic through the tunnel:

ip -6 route add 2000::/3 dev cl6rd

The 2000::/3 prefix covers the currently allocated global unicast IPv6 address space.

This provides a route for public IPv6 destinations without overriding local and link-local IPv6 routes.

You can confirm the route was added by running:

ip -6 route show

You should see something similar to:

2000::/3 dev cl6rd metric 1024 pref medium

At this point, the tunnel is configured and we should have connectivity!

Step 5: Verify IPv6 connectivity from the UXG-Pro

Before configuring the LAN, let’s confirm that the gateway can reach IPv6 destinations through CenturyLink.

Start by checking the tunnel interface:

ip -d link show cl6rd
ip -6 addr show dev cl6rd

Next, ping Cloudflare’s IPv6 DNS server:

ping -6 -c 3 2606:4700:4700::1111

If everything is working, you should receive replies.

You can also use curl to make an outbound IPv6 connection:

curl -6 --max-time 10 https://api64.ipify.org

This should return the public IPv6 address used for the request.

The -6 argument forces curl to use IPv6 rather than silently falling back to IPv4.

If both commands work, we’ve confirmed that the gateway can successfully transport IPv6 traffic over CenturyLink’s IPv4 infrastructure.

If these tests fail, I would stop here and troubleshoot the tunnel before making changes to the LAN.

Step 6: Configure IPv6 on the UniFi LAN

Now that the tunnel works, the next step is to configure UniFi to advertise IPv6 addresses to devices on our LAN.

Open the UniFi Network application and navigate to:

Settings → Networks → Default

Under IPv6 configuration, enter the following settings:

SettingValue
Interface TypeStatic
IPv6 AddressYour calculated LAN gateway address
Netmask64
Gateway IPAutomatically calculated by UniFi
Link Local IPLeave unchanged
AdvancedManual
Client Address AssignmentSLAAC
Auto DNS ServerUnchecked
Router Advertisement (RA)Enabled
RA PriorityMedium

For DNS, I am using Cloudflare’s public IPv6 DNS servers. Enter both a primary and secondary server address:

DNSIPv4IPv6
Primary1.1.1.12606:4700:4700::1111
Secondary1.0.0.12606:4700:4700::1001

Using the example /56 calculated earlier, our default LAN gateway would be:

2602:c0:2:101::1

With a netmask of 64, that gives us:

2602:c0:2:101::/64

The important thing is that the LAN uses a /64 within the /56 derived from our public IPv4 address.

What is SLAAC?

SLAAC stands for Stateless Address Autoconfiguration.

When enabled, devices connected to the network can automatically configure their own IPv6 addresses based on the prefix advertised by the router.

This means we don’t necessarily need a DHCPv6 server to hand out individual IPv6 addresses.

What are Router Advertisements?

Router Advertisements, or RAs, tell clients which IPv6 prefix they should use and which router serves as their default gateway.

With Router Advertisements enabled, devices on the default LAN should begin configuring IPv6 addresses within the subnet configured in UniFi.

Depending on the operating system, you may need to disconnect and reconnect a device or wait for the next Router Advertisement.

Step 7: Configure multiple LANs and VLANs

One advantage of CenturyLink’s /56 allocation is that we aren’t limited to a single IPv6 subnet.

A /56 provides 256 individual /64 networks, allowing us to use different IPv6 subnets for our default LAN, IoT devices, guest network, servers, or any other VLANs we’ve configured.

For example, using the fictional IPv4 address from earlier:

NetworkSubnet IDIPv6 subnet
Default LAN012602:c0:2:101::/64
IoT022602:c0:2:102::/64
Guest032602:c0:2:103::/64
Servers042602:c0:2:104::/64

Each network would use an IPv6 gateway address ending in ::1.

For example, the IoT network would use:

2602:c0:2:102::1/64

And the Servers network would use:

2602:c0:2:104::1/64

You can configure each of these networks in UniFi using the same Static IPv6, SLAAC, and Router Advertisement settings described above.

UniFi continues to manage the individual LAN interfaces and VLANs.

The 6rd tunnel itself doesn’t need a separate interface or route for each VLAN. The gateway simply forwards their outbound IPv6 traffic through the same tunnel.

Later in the article, we’ll configure the restore script with a list of LANs and VLANs so it can calculate and validate the expected prefix for each network.

One important distinction: the script won’t create VLANs or change the IPv6 settings managed by UniFi. It will calculate the correct addresses and warn if the actual interface configuration doesn’t match.

Step 8: Test IPv6 from a LAN device

Now that the tunnel is configured and UniFi is advertising the IPv6 subnet, connect a device to your default LAN.

The device should receive a globally routable IPv6 address within the /64 assigned to that network.

You can check this on Windows using:

ipconfig /all

Or on Linux using:

ip -6 addr

Next, try pinging Cloudflare over IPv6:

ping -6 2606:4700:4700::1111

For a more comprehensive test, navigate to:

https://test-ipv6.com

This is one of my preferred ways of validating an IPv6 configuration because it checks whether your client can successfully communicate with IPv6 destinations and identifies common connectivity issues.

With the configuration described above, I’m receiving a perfect 10/10 score on test-ipv6.com.

This confirms that IPv6 connectivity is working properly from my LAN devices through the CenturyLink 6rd tunnel.

If your UXG-Pro can reach IPv6 destinations but your LAN devices cannot, double-check the LAN prefix, Router Advertisements, IPv6 forwarding, and firewall configuration.

A 10/10 score confirms client IPv6 connectivity, but it doesn’t independently validate unsolicited inbound traffic if you’re planning to host services.

Step 9: Create a script to automatically configure the tunnel

Now that we’ve confirmed the configuration works manually, we can combine the commands into a reusable script.

Rather than hardcoding the public IPv4 address or IPv6 tunnel address, the script will calculate everything from the IPv4 address assigned to the WAN interface.

This makes the tunnel configuration usable with both static and dynamic IPv4 assignments.

There are several things I wanted the script to handle:

  1. Wait for the WAN interface to receive an IPv4 address.
  2. Calculate the CenturyLink /56 IPv6 prefix automatically.
  3. Derive the appropriate tunnel IPv6 address.
  4. Install the IPv6 firewall rules without creating duplicates.
  5. Create the tunnel if it doesn’t exist.
  6. Recreate the tunnel if its IPv4 endpoints have changed.
  7. Restore the IPv6 route.
  8. Test IPv6 connectivity through the tunnel.
  9. Calculate the expected IPv6 addresses for multiple LANs and VLANs.
  10. Validate that the configured LAN interfaces use the correct IPv6 addresses.
  11. Display clear status messages and warnings.

For additional protection, the script also supports an optional expected IPv4 address.

If you have a static public IPv4 address, you can populate that setting to prevent unexpected tunnel changes.

If you have a dynamic assignment, leave it blank.

Identify your LAN and VLAN interface names

Before creating the script, we’ll need to identify the Linux interfaces associated with our UniFi networks.

You can list the available interfaces using:

ip -br link show

You can also view interfaces with IPv6 addresses:

ip -6 -br addr show

Depending on your UniFi configuration, the LAN interfaces may be named something like br0, br10, or another bridge or VLAN interface.

Don’t assume the interface name corresponds to the VLAN ID. Verify the actual interface name on your gateway.

For example, you might discover the following mapping:

UniFi networkLinux interfaceSubnet ID
Default LANbr01
IoTbr102
Guestbr203
Serversbr304

The interface names above are examples. You’ll need to use the interface names from your own gateway.

The subnet ID is the final eight bits of your assigned /56 and can range from 0 to 255.

I’ll reserve subnet ID 0 for the tunnel and use IDs 1 through 255 for LANs and VLANs.

Create the restore script

SSH into the UXG-Pro and run the following commands.

Before running the script, review the WAN_IF, EXPECTED_WAN4, and LAN_NETWORKS settings.

mkdir -p /mnt/data/centurylink6rd

cat > /mnt/data/centurylink6rd/restore.sh <<'EOF'
#!/bin/sh
set -eu

#######################################
# CenturyLink 6rd Configuration
#######################################

WAN_IF="ppp0"
RELAY="205.171.2.64"
TUNNEL="cl6rd"
MTU="1472"

# Optional static IPv4 protection.
# Leave empty for dynamic IPv4 assignments.
EXPECTED_WAN4=""

# LAN/VLAN configuration:
# Format: Interface Subnet_ID Network_Name
#
# Replace these examples with your actual
# Linux interface names and network IDs.
#
# Use subnet IDs 1-255.
# Subnet ID 0 is reserved for the tunnel.

LAN_NETWORKS="
br0 1 Default
br10 2 IoT
br20 3 Guest
br30 4 Servers
"

#######################################
# Wait for WAN IPv4
#######################################

n=0

while :; do
    WAN4=$(ip -4 -o addr show dev "$WAN_IF" 2>/dev/null |
        awk 'NR==1 {split($4,a,"/"); print a[1]}')

    [ -n "$WAN4" ] && break

    n=$((n + 1))

    [ "$n" -ge 60 ] && {
        echo "[ERROR] WAN IPv4 timeout" >&2
        exit 1
    }

    sleep 2
done

# Validate IPv4 octets.
echo "$WAN4" | awk -F. '
    NF != 4 { exit 1 }
    {
        for (i = 1; i <= 4; i++) {
            if ($i !~ /^[0-9]+$/ ||
                $i < 0 || $i > 255) exit 1
        }
    }
' || {
    echo "[ERROR] Invalid WAN IPv4" >&2
    exit 1
}

echo "[OK] WAN IPv4 detected"

# Optional static IPv4 check.
if [ -n "$EXPECTED_WAN4" ] &&
   [ "$WAN4" != "$EXPECTED_WAN4" ]; then

    echo "[ERROR] Unexpected WAN IPv4" >&2
    exit 1
fi

#######################################
# Calculate CenturyLink IPv6 Prefix
#######################################

# CenturyLink uses:
# 6rd prefix: 2602::/24
# IPv4 mask length: 0
#
# The 32-bit IPv4 address is embedded
# after the first 24 bits.

BASE=$(echo "$WAN4" | awk -F. '{
    printf "2602:%x:%x:%x",
        $1, ($2 * 256) + $3, $4 * 256
}')

PREFIX="${BASE}::/56"
TUNNEL_IP="${BASE}::1/128"

echo "[OK] 6rd prefix calculated"

#######################################
# Configure IPv6 Firewall
#######################################

ip6tables -C FORWARD -i "$TUNNEL" \
    -m conntrack ! --ctstate ESTABLISHED,RELATED \
    -j DROP 2>/dev/null ||
ip6tables -I FORWARD 1 -i "$TUNNEL" \
    -m conntrack ! --ctstate ESTABLISHED,RELATED \
    -j DROP

ip6tables -C INPUT -i "$TUNNEL" \
    -m conntrack ! --ctstate ESTABLISHED,RELATED \
    -j DROP 2>/dev/null ||
ip6tables -I INPUT 1 -i "$TUNNEL" \
    -m conntrack ! --ctstate ESTABLISHED,RELATED \
    -j DROP

#######################################
# Create or Restore SIT Tunnel
#######################################

# Recreate if the IPv4 endpoints changed.
if ip link show dev "$TUNNEL" \
    >/dev/null 2>&1; then

    if ! ip tunnel show "$TUNNEL" |
        grep -Fq "remote $RELAY local $WAN4"; then

        echo "[WARN] Tunnel endpoints changed"
        echo "[INFO] Recreating tunnel"

        ip -6 route del 2000::/3 \
            dev "$TUNNEL" 2>/dev/null || true

        ip tunnel del "$TUNNEL"
    fi
fi

# Create the tunnel if missing.
if ! ip link show dev "$TUNNEL" \
    >/dev/null 2>&1; then

    ip tunnel add "$TUNNEL" mode sit \
        local "$WAN4" \
        remote "$RELAY" \
        ttl 64
fi

ip link set dev "$TUNNEL" mtu "$MTU" up

# Ensure the expected tunnel address exists.
ip -6 -o addr show dev "$TUNNEL" |
    awk '{print $4}' |
    grep -Fqx "$TUNNEL_IP" ||
    ip -6 addr add "$TUNNEL_IP" dev "$TUNNEL"

echo "[OK] Tunnel configured"

#######################################
# Restore IPv6 Routing
#######################################

ip -6 route replace 2000::/3 dev "$TUNNEL"

echo "[OK] IPv6 route installed"

#######################################
# Verify IPv6 Connectivity
#######################################

if ping -6 -I "$TUNNEL" -c 3 -W 3 \
    2606:4700:4700::1111 >/dev/null 2>&1; then

    echo "[OK] IPv6 connectivity verified"
else
    echo "[WARN] IPv6 connectivity test failed"
fi

#######################################
# Validate LAN/VLAN IPv6 Addresses
#######################################

echo ""
echo "LAN/VLAN validation:"

# Extract the first three IPv4 octets'
# embedded hextets once for subnet math.

IPV6_PART=$(echo "$WAN4" | awk -F. '{
    printf "2602:%x:%x",
        $1, ($2 * 256) + $3
}')

IPV6_FOURTH_BASE=$(echo "$WAN4" |
    awk -F. '{print $4 * 256}')

printf '%s\n' "$LAN_NETWORKS" |
while read -r IFACE SUBNET NAME; do

    [ -n "${IFACE:-}" ] || continue

    # Validate the subnet ID.
    case "$SUBNET" in
        *[!0-9]*|"")
            echo "[WARN] $NAME - Invalid subnet ID"
            continue
            ;;
    esac

    if [ "$SUBNET" -lt 1 ] ||
       [ "$SUBNET" -gt 255 ]; then

        echo "[WARN] $NAME - Subnet ID must be 1-255"
        continue
    fi

    # Calculate the expected LAN /64.
    FOURTH=$((IPV6_FOURTH_BASE + SUBNET))
    FOURTH_HEX=$(printf '%x' "$FOURTH")

    LAN_BASE="${IPV6_PART}:${FOURTH_HEX}"
    LAN_PREFIX="${LAN_BASE}::/64"
    LAN_GATEWAY="${LAN_BASE}::1"

    # Check whether the interface exists.
    if ! ip link show dev "$IFACE" \
        >/dev/null 2>&1; then

        echo "[WARN] $NAME ($IFACE) - Interface not found"
        echo "       Expected: $LAN_GATEWAY/64"
        continue
    fi

    # Compare the configured gateway address.
    if ip -6 -o addr show dev "$IFACE" |
        awk '{print $4}' |
        grep -Fqx "${LAN_GATEWAY}/64"; then

        echo "[OK] $NAME ($IFACE) - prefix matches"
    else
        echo "[WARN] $NAME ($IFACE) - IPv6 prefix does not match"
        echo "       Expected: $LAN_PREFIX"
        echo "       Gateway:  $LAN_GATEWAY/64"
    fi
done

#######################################
# Summary
#######################################

echo ""
echo "CenturyLink IPv6 tunnel configured."
echo ""
echo "WAN IPv4: $WAN4"
echo "6rd Prefix: $PREFIX"
echo "Tunnel IPv6: $TUNNEL_IP"
echo ""
echo "Note: LAN prefix warnings do not"
echo "automatically modify UniFi settings."
EOF

chmod 700 /mnt/data/centurylink6rd/restore.sh

How the script works

There are a few important improvements over the individual commands used earlier.

Automatically detecting the WAN IPv4 address

Rather than hardcoding an IPv4 address, the script retrieves the current address directly from the WAN interface:

WAN4=$(ip -4 -o addr show dev "$WAN_IF" 2>/dev/null |
    awk 'NR==1 {split($4,a,"/"); print a[1]}')

This works with either static or dynamically assigned WAN IPv4 addresses, provided WAN_IF points to the correct interface.

The script also waits up to approximately two minutes for the interface to receive an address.

Automatically deriving the IPv6 prefix

The script converts the public IPv4 address into the appropriate hexadecimal values and combines them with CenturyLink’s 2602::/24 prefix.

This produces the correct /56 without requiring a manually entered IPv6 address.

It then calculates the tunnel address and the expected LAN/VLAN prefixes.

Handling static IPv4 assignments

If you have a static IPv4 address, you can optionally populate:

EXPECTED_WAN4="YOUR_STATIC_PUBLIC_IPV4"

This provides an additional safety check.

If the public IPv4 address changes unexpectedly, the script exits rather than creating the tunnel using an address you weren’t expecting.

Since I have a static IPv4 assignment, this is the setting I would use in my environment.

Handling dynamic IPv4 assignments

If your public IPv4 address is dynamically assigned, leave this variable empty:

EXPECTED_WAN4=""

Every time the script runs, it retrieves the current WAN IPv4 address and recalculates the appropriate IPv6 prefix.

If the tunnel already exists but its IPv4 endpoints no longer match, the script deletes and recreates it.

Deleting the old tunnel also removes the IPv6 address associated with that tunnel interface.

Keep in mind that this happens when the script executes. It doesn’t continuously monitor the WAN connection on its own.

Avoiding duplicate firewall rules

The script uses ip6tables -C to check whether the firewall rules already exist before adding them.

This allows the script to be executed multiple times without inserting duplicate copies of the same rules.

Restoring the route

Instead of adding a new route every time, the script uses:

ip -6 route replace 2000::/3 dev "$TUNNEL"

This makes the routing configuration repeatable.

Testing IPv6 connectivity

The script also runs a connectivity test:

ping -6 -I "$TUNNEL" -c 3 -W 3 \
    2606:4700:4700::1111

This attempts to ping Cloudflare’s IPv6 DNS server through the tunnel.

If the test succeeds, the script displays:

[OK] IPv6 connectivity verified

If it fails, it displays:

[WARN] IPv6 connectivity test failed

A failed ping doesn’t necessarily mean the tunnel is configured incorrectly, so the script reports a warning rather than deleting or recreating the tunnel automatically.

Validating multiple LANs and VLANs

The script also includes a configurable list of LAN interfaces and subnet IDs.

For example:

LAN_NETWORKS="
br0 1 Default
br10 2 IoT
br20 3 Guest
br30 4 Servers
"

Each entry tells the script which Linux interface to check and which /64 subnet should be assigned to it. For each entry, the script calculates the expected IPv6 subnet and gateway address. It then checks whether that gateway address is actually configured on the corresponding Linux interface.

For example, if the IoT network is configured correctly, you’ll see:

[OK] IoT (br10) - prefix matches

If the address doesn’t match, you’ll see something like:

[WARN] IoT (br10) - IPv6 prefix does not match
       Expected: 2602:c0:2:102::/64
       Gateway:  2602:c0:2:102::1/64

This is a read-only validation step.

The script doesn’t modify UniFi’s LAN configuration or automatically change the addresses assigned to your VLANs.

Also, this check verifies the expected gateway address on the interface. It doesn’t independently verify that clients are receiving the correct Router Advertisements.

Run the script

Once you’ve saved the script and updated the appropriate variables, execute:

/mnt/data/centurylink6rd/restore.sh

If everything is configured correctly, you’ll see output similar to:

[OK] WAN IPv4 detected
[OK] 6rd prefix calculated
[OK] Tunnel configured
[OK] IPv6 route installed
[OK] IPv6 connectivity verified

LAN/VLAN validation:
[OK] Default (br0) - prefix matches
[OK] IoT (br10) - prefix matches
[WARN] Guest (br20) - IPv6 prefix does not match
       Expected: 2602:c0:2:103::/64
       Gateway:  2602:c0:2:103::1/64
[OK] Servers (br30) - prefix matches

CenturyLink IPv6 tunnel configured.

WAN IPv4: 192.0.2.1
6rd Prefix: 2602:c0:2:100::/56
Tunnel IPv6: 2602:c0:2:100::1/128

Note: LAN prefix warnings do not
automatically modify UniFi settings.

The output uses documentation addresses for illustration. Your actual values will be derived from your public IPv4 address. The important thing is that the script distinguishes between configuring the tunnel and validating the LAN networks.

Note: a LAN mismatch won’t prevent the tunnel from being configured, but the warning will tell you which UniFi network needs attention.

Step 10: Handling dynamic IP changes and reboots

There are two separate problems to consider when automating the tunnel:

  1. Restoring the configuration after a gateway reboot.
  2. Handling changes to the public IPv4 address.

The script provides the configuration and restoration logic, but it doesn’t automatically execute when either event occurs.

Static IPv4 configuration

For my environment, this is relatively straightforward.

Since CenturyLink provides me with a static public IPv4 address, my /56 IPv6 prefix remains consistent.

Once UniFi is configured with the appropriate static IPv6 addresses on the LAN and VLAN interfaces, those addresses shouldn’t need to change during normal operation.

If the tunnel disappears after a reboot or reprovisioning event, I can restore it by executing:

/mnt/data/centurylink6rd/restore.sh

The script will recreate the tunnel, restore the firewall rules and route, and validate the LAN configurations.

Dynamic IPv4 configuration

If you have a dynamic public IPv4 address, there is an additional consideration.

Since CenturyLink derives the /56 prefix directly from the public IPv4 address, a new IPv4 assignment results in a different IPv6 prefix.

The script already handles recalculating the prefix and rebuilding the tunnel when it’s executed.

However, changing the tunnel alone isn’t enough. UniFi also needs to update the IPv6 addresses and Router Advertisements for each configured LAN or VLAN. Otherwise, devices will continue using IPv6 addresses derived from the previous public IPv4 assignment.

For now, the script deliberately leaves these UniFi-managed settings alone.

Instead, it displays a warning for each LAN interface that doesn’t match the newly calculated prefix to provide a clear indication of which networks need to be updated.

A fully unattended solution for dynamic IPv4 assignments would require an additional mechanism to detect WAN changes, execute the script, and update the managed UniFi LAN configuration. That could potentially be accomplished using the UniFi Network API, but that’s outside the scope of this script.

What about automatically restoring the tunnel after a reboot?

One limitation worth mentioning is that simply saving a script under /mnt/data doesn’t mean it will execute after a reboot.

The script provides a reusable way to configure the tunnel, but it doesn’t install a startup service or boot hook.

You would still need to integrate it with a startup mechanism verified to work on your particular UniFi firmware version.

I also wouldn’t assume arbitrary files or custom startup configurations survive every UniFi OS firmware update.

For now, the script makes it straightforward to restore the configuration manually without re-entering each command.

Troubleshooting common problems

Here are a few things I’d check if you’re having trouble getting this working.

The native 6rd command returns “Invalid argument”

If you’re following another guide and receive:

6rd: ioctl 89f9 failed: Invalid argument

Check the kernel configuration:

zcat /proc/config.gz 2>/dev/null | grep IPV6_SIT

If you see:

CONFIG_IPV6_SIT=y
# CONFIG_IPV6_SIT_6RD is not set

Your kernel supports standard SIT tunneling but not the specialized 6rd functionality.

Use the fixed-relay SIT approach described in this article rather than the native ip tunnel 6rd commands.

The tunnel exists, but IPv6 doesn’t work

Start by checking the tunnel:

ip tunnel show cl6rd

Then verify the IPv6 address and route:

ip -6 addr show dev cl6rd
ip -6 route show

Make sure the tunnel’s local IPv4 endpoint matches the current public IPv4 assigned to your WAN interface.

Also verify that the remote endpoint is CenturyLink’s relay:

205.171.2.64

You can inspect the tunnel’s packet counters:

ip -s link show dev cl6rd

If needed, use tcpdump to check whether encapsulated traffic is passing through the WAN:

tcpdump -ni ppp0 'ip proto 41'

SIT uses IPv4 protocol 41.

Seeing outbound traffic but no inbound responses may indicate a problem with upstream routing, filtering, or relay connectivity.

IPv6 works on the UXG-Pro but not LAN devices

First, verify that your LAN clients are receiving IPv6 addresses within the expected /64.

Next, confirm that Router Advertisements are enabled in UniFi Network.

You can also check whether IPv6 forwarding is enabled:

sysctl net.ipv6.conf.all.forwarding

A value of 1 indicates that forwarding is enabled.

The restore script’s LAN validation output should also help identify whether the configured IPv6 addresses match the prefixes derived from the current public IPv4 address.

If the gateway is working but clients aren’t, focus on the LAN prefix, Router Advertisements, and firewall configuration.

The script reports a LAN prefix mismatch

For example:

[WARN] IoT (br10) - IPv6 prefix does not match

This means the script didn’t find the expected IPv6 gateway address on the configured Linux interface.

First, verify that you’ve entered the correct interface name in the LAN_NETWORKS configuration.

You can list the interfaces again using:

ip -br link show

Next, inspect the IPv6 addresses assigned to that interface:

ip -6 addr show dev br10

If the interface name is correct but the address doesn’t match, check the IPv6 configuration for that network in UniFi.

Keep in mind that the script doesn’t modify the LAN address automatically. It only reports the expected configuration.

IPv6 works, but some websites time out

This may indicate an MTU or Path MTU Discovery problem.

Verify that the tunnel is configured with an MTU of 1472:

ip link show dev cl6rd

Also make sure that your overall IPv6 firewall configuration isn’t blocking ICMPv6 Packet Too Big messages.

IPv6 relies on these messages to determine the maximum usable packet size along a network path.

Can I host services over IPv6?

Yes. The IPv6 addresses assigned to your LAN clients are globally routable.

Unlike a typical IPv4 residential network using NAT, you don’t need IPv6 NAT or traditional port forwarding to make a service publicly accessible.

However, the firewall rules in this guide deliberately block unsolicited inbound connections.

If you want to host a web server, VPN server, or another service over IPv6, you’ll need to configure an explicit inbound firewall exception for that destination and port.

I’d also recommend assigning servers stable IPv6 addresses rather than relying on temporary privacy addresses.

Since the IPv6 prefix is derived from your public IPv4 address, having a static public IPv4 assignment makes maintaining DNS records and inbound firewall rules much easier.

Remove the tunnel and revert the changes

If you want to remove the tunnel configuration, first delete the IPv6 route:

ip -6 route del 2000::/3 dev cl6rd

Next, delete the tunnel:

ip tunnel del cl6rd

Remove the associated firewall rules:

ip6tables -D FORWARD -i cl6rd \
    -m conntrack ! --ctstate ESTABLISHED,RELATED \
    -j DROP
ip6tables -D INPUT -i cl6rd \
    -m conntrack ! --ctstate ESTABLISHED,RELATED \
    -j DROP

Finally, disable or revert the static IPv6 configuration under your LANs and VLANs in UniFi Network.

Final thoughts

It’s a bit surprising that in 2026 we’re still dealing with 6rd to get IPv6 connectivity through an ISP’s fiber connection.

The UXG-Pro supports IPv6, but the lack of a native 6rd configuration option combined with the missing CONFIG_IPV6_SIT_6RD kernel functionality makes this more complicated than it probably should be.

Fortunately, at least with my CenturyLink configuration, it’s possible to work around the issue using standard Linux SIT tunneling, an explicitly configured relay, and some IPv6 routing. With my static IPv4 assignment, UXG-Pro firmware 5.1.26, and UniFi Network 10.6.106, I’ve confirmed that the manually configured tunnel works and produces a 10/10 score on test-ipv6.com.

The restore script also removes the need to manually calculate IPv6 addresses, making it easier to configure the tunnel for either static or dynamically assigned IPv4 addresses. It also supports calculating and validating multiple LAN or VLAN prefixes, which is helpful if your UniFi environment has separate networks for devices, IoT, guests, or servers and you want to quickly add a new segment to your network.

For users with dynamic IPv4 assignments, there’s still additional work required to automate LAN prefix updates whenever the WAN IPv4 address changes. The script handles the tunnel configuration and provides diagnostic warnings, but it doesn’t modify UniFi-managed LAN settings in favor of making the configurations through Unifi’s supported interfaces.

Hopefully this helps anyone else trying to get CenturyLink IPv6 working on a UniFi UXG-Pro without spending hours troubleshooting the same missing kernel functionality.

Aside on IPv6 and IPv4 resources. With IPv6 becoming more widely available, your devices may begin using it automatically when connecting to websites and services. But what happens when both IPv4 and IPv6 are available? That’s where Happy Eyeballs comes in. Yes, “Happy Eyeballs” is its real name!

Happy Eyeballs is an official IETF standards-track algorithm defined in RFC 8305, designed to make IPv4 and IPv6 connectivity work together seamlessly. Happy Eyeballs allows applications to attempt connections over both IPv6 and IPv4, using timed attempts to establish a connection quickly without leaving users waiting on a broken or slow network path.

What does this mean for you? Websites that support IPv6 will increasingly use it automatically, while IPv4-only services will continue working as usual. For most users, the transition from v4 to v6 will feel seamless, with more internet traffic moving to IPv6 without any noticeable difference.

References