Assign IPv6 GUAs and ULAs simultaneously in your Unifi home network

03/18/2024

Recently, I was frustrated to experience how often my ISP-provided IPv6 prefix delegation changes. For most people, this is not an issue. But for a homelabber like me who is attempting to follow Microsoft’s best practices regarding IPv6, getting IPv6 working in my LAN without causing a headache, to me, is a fun challenge!

The problem: Your ISP changes your delegated IPv6 address space

By default, modern operating systems now prefer to use IPv6 over IPv4. One place this can be problematic is when attempting to join a client to your domain. If your DCs do not have static IPv6 addresses set, and your client’s v6 DNS is not pointing to your DC’s static IPv6 address, you will encounter an error that the domain cannot be found. The moment your ISP’s router reboots, or your router reboots or updates, you might find you’ve been given a new prefix delegation, and now your DNS servers have invalid static Globally Unique IPv6 addresses (GUAs), and therefore now your DNS is broken. Scouting around online, forums are filled with folks saying “just disable IPv6”, which is not a good solution, especially as an increasing number of sites and services are accessible only via IPv6.

While this IPv6 preference can be modified with a registry key change and pushed to domain clients via a GPO, I encountered several tcpip.sys blue screen errors after setting this registry key on one of my Windows 10 laptops, so I do not dabble with that option anymore.

The solution: Unique Local Addressing (ULA)

ULAs in IPv6 are somewhat similar to private IP addressing in v4, although there are notable distinctions. Generally speaking, ULAs in enterprise environments are unnecessary, since an enterprise will likely have a static prefix delegation they can depend on practically forever. ULAs are also currently depreferenced, meaning that other types of IP addressing will take priority. In particular, Link-local, GUA, and IPv4 addresses will all take precedence over ULAs, though there is a proposal to change this behavior.

Nevertheless, we can use ULAs similar to how we use our private IP ranges, such as the common 192.168.1.1/24 range. Although the Local Unicast range is technically fc00::/7, RFC 4193 states that ULAs should use the fd00::/8 range instead, since fc00::/8 could be used for other purposes in the future.

Using my home lab environment as an example, this means that I can assign the IP address 192.168.7.100 to my Domain Controller for IPv4 DNS, and I can assign an address such as fdea:861f:ab6e:aaaa::100 for IPv6 DNS. The proper structure of a ULA prefix is this: FDxx:xxxx:xxxx::/48, where each x is individually a randomly generated hexadecimal character. From the /48, you can subnet it however you wish. Here is a nifty tool to generate a unique ULA prefix.

Computer configuration

In my case, I am using the subnets fdaa:aaaa:aaaa:aaa1::/64, fdaa:aaaa:aaaa:aaa2::/64, and fdaa:aaaa:aaaa:aaa3::/64. The unique 40-bit portion was totally randomly generated, I swear 😎

Here we can see the config on one of my DNS servers:

“Obtain IPv6 address automatically” is set so that my fdaa: address can be self-generated via SLAAC. I will never have to worry about this address changing.

DNS is set the same way that IPv4 DNS is set: Secondary DNS server as the Preferred, and localhost as the Alternate DNS server. The secondary DNS server and all other clients, including smartphones which connect to my WiFi, also get their IPv6 addresses via SLAAC.

Unifi router configuration

Here is where things can get tricky, especially if you’re using a Unifi USG router which is managed by a Unifi Controller. In order to assign both a GUA and ULA in such a situation, unfortunately we cannot use the Unifi Controller UI, and we cannot simply edit the configuration on the router directly and then commit the change – any customizations you make in the CLI will be overwritten by the Unifi Controller.

In order to overcome this limitation, we must create a special file on the Unifi Controller called config.gateway.json. See this article for more details.

In my case, my Unifi Controller is running on an Ubuntu VM. Here is a cat dump of my entire .json file:

trevor@ubuntu1:/usr/lib/unifi/data/sites/emnlsz8l$ cat config.gateway.json
{
   "interfaces":{
      "ethernet":{
         "eth0":{
            "address":[
               "fdaa:aaaa:aaaa:aaa1::1/64",
               "192.168.7.1/24"
            ],
            "ipv6":{
               "router-advert":{
                  "prefix":{
                     "fdaa:aaaa:aaaa:aaa1::/64":{
                        "autonomous-flag":"true",
                        "on-link-flag":"true",
                        "preferred-lifetime":"14400",
                        "valid-lifetime":"86400"
                     }
                  }
               }
            }
         },
         "eth1":{
            "address":[
               "fdaa:aaaa:aaaa:aaa2::1/64",
               "192.168.8.1/24"
            ],
            "ipv6":{
               "router-advert":{
                  "prefix":{
                     "fdaa:aaaa:aaaa:aaa2::/64":{
                        "autonomous-flag":"true",
                        "on-link-flag":"true",
                        "preferred-lifetime":"14400",
                        "valid-lifetime":"86400"
                     }
                  }
               }
            }
         },
         "eth2":{
            "dhcpv6-pd":{
               "prefix-only":"''",
               "rapid-commit": "disable"
            }
         },
         "eth3":{
            "address":[
               "fdaa:aaaa:aaaa:aaa3::1/64",
               "10.11.111.1/24"
            ],
            "ipv6":{
               "router-advert":{
                  "prefix":{
                     "fdaa:aaaa:aaaa:aaa3::/64":{
                        "autonomous-flag":"true",
                        "on-link-flag":"true",
                        "preferred-lifetime":"14400",
                        "valid-lifetime":"86400"
                     }
                  }
               }
            }
         }
      }
   }
}

*Side note: I used the JSON formatter here to ensure the syntax was valid.

Above, eth0, eth1, and eth3 are my LAN ports. eth2 is my WAN port. Note that each LAN port has both a private IPv4 address assigned as well as a ::1 ULA. Each interface is also sending a router advertisement with the A flag to its clients, which autoconfigure themselves with a SLAAC address.

The WAN interface also has prefix-only set and rapid-commit disabled to make my ISP happy. I only happened to notice it was an issue when i issued the command show dhcpv6-pd log on my router.

Note that this is NOT the full configuration of my interfaces, since the rest of the config is handled in the Unifi Controller UI. The config.gateway.json file only overwrites or creates the sections of the actual config that already exist or don’t yet exist; it does not replace the entire config.

Here’s what one of the LAN interfaces looks like (I’ve removed some autogenerated firewall entries that are irrelevant):

 ethernet eth0 {
     address fdaa:aaaa:aaaa:aaa1::1/64
     address 192.168.7.1/24
     description LAN
     ipv6 {
         dup-addr-detect-transmits 1
         router-advert {
             managed-flag false
             max-interval 600
             name-server fe80::b6fb:e4ff:fe82:37c4
             other-config-flag false
             prefix ::/64 {
                 autonomous-flag true
                 on-link-flag true
                 preferred-lifetime 14400
                 valid-lifetime 86400
             }
             prefix fdaa:aaaa:aaaa:aaa1::/64 {
                 autonomous-flag true
                 on-link-flag true
                 preferred-lifetime 14400
                 valid-lifetime 86400
             }
             radvd-options "DNSSL tlu.techlevelup.net {};"
             send-advert true
         }

And here is what the WAN port config looks like:

 ethernet eth2 {
     address dhcp
     description WAN
     dhcp-options {
         client-option "retry 60;"
         default-route-distance 1
         name-server no-update
     }
     dhcpv6-pd {
         no-dns
         pd 0 {
             interface eth0 {
                 prefix-id 1
             }
             interface eth1 {
                 prefix-id 2
             }
             interface eth3 {
                 prefix-id 3
             }
             prefix-length 56
         }
         prefix-only
         rapid-commit disable
     }

Since each interface on the router has a ::1 address in each fdaa: subnet, network activity can flow freely between subnets. For example, my Windows 11 PC on the fdaa:aaaa:aaaa:aaa1::/64 subnet can communicate with my Network-attached storage device in the fdaa:aaaa:aaaa:aaa2::/64 network: